Skip to content

Commit 8a847d7

Browse files
committed
review feedback
1 parent 63e450b commit 8a847d7

File tree

2 files changed

+139
-113
lines changed

2 files changed

+139
-113
lines changed

docs/shell-completion.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Shell Completion for slcli
22

3-
The SystemLink CLI supports shell completion for bash, zsh, fish, and PowerShell shells.
3+
The SystemLink CLI supports shell completion for bash, zsh, fish, and PowerShell shells with dynamic command discovery.
4+
5+
## Dynamic Completion
6+
7+
- **PowerShell**: Commands extracted from CLI structure at generation time
8+
- **Bash/Zsh/Fish**: Uses Click's built-in completion system
9+
- Always up-to-date with current CLI commands
10+
- Automatically stays current when new commands are added
411

512
## Quick Installation
613

slcli/completion_click.py

Lines changed: 131 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -3,147 +3,163 @@
33
import os
44
import subprocess
55
from pathlib import Path
6-
from typing import Optional
6+
from typing import Dict, List, Optional
77

88
import click
99

1010

11-
def generate_powershell_completion() -> str:
12-
"""Generate PowerShell completion script for slcli."""
13-
return """
14-
# PowerShell completion for slcli
15-
Register-ArgumentCompleter -Native -CommandName slcli -ScriptBlock {
11+
def get_cli_commands_dynamically() -> Dict[str, List[str]]:
12+
"""Extract commands and subcommands from the CLI structure dynamically.
13+
14+
Returns:
15+
Dict mapping command names to lists of their subcommands.
16+
"""
17+
try:
18+
# Import the main CLI group to inspect its structure
19+
from slcli.main import cli
20+
21+
commands = {}
22+
23+
# Get top-level commands
24+
commands[""] = list(cli.commands.keys())
25+
26+
# Get subcommands for each command
27+
for cmd_name, cmd_obj in cli.commands.items():
28+
if isinstance(cmd_obj, click.Group):
29+
commands[cmd_name] = list(cmd_obj.commands.keys())
30+
else:
31+
commands[cmd_name] = []
32+
33+
return commands
34+
except Exception:
35+
# Fallback to hardcoded commands if introspection fails
36+
return {
37+
"": [
38+
"completion",
39+
"login",
40+
"logout",
41+
"notebook",
42+
"template",
43+
"user",
44+
"workflow",
45+
"workspace",
46+
],
47+
"notebook": ["list", "get", "create", "update", "delete", "run"],
48+
"template": ["list", "get", "create", "update", "delete"],
49+
"user": ["list", "get", "create", "update", "delete"],
50+
"workflow": ["list", "get", "create", "update", "delete", "run"],
51+
"workspace": ["list", "get", "create", "update", "delete"],
52+
"completion": [],
53+
}
54+
55+
56+
def generate_powershell_completion_dynamic() -> str:
57+
"""Generate PowerShell completion script using dynamic command discovery."""
58+
commands_dict = get_cli_commands_dynamically()
59+
60+
# Build the command arrays as PowerShell code
61+
top_level_commands = commands_dict.get("", [])
62+
63+
powershell_script = f"""
64+
# PowerShell completion for slcli (dynamically generated)
65+
Register-ArgumentCompleter -Native -CommandName slcli -ScriptBlock {{
1666
param($wordToComplete, $commandAst, $cursorPosition)
1767
1868
# Get all arguments passed so far
19-
$arguments = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.Value }
69+
$arguments = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object {{ $_.Value }}
2070
21-
# Function to get available commands
22-
function Get-SlcliCommands {
23-
param($subCommand = $null)
24-
25-
$commands = @()
26-
27-
if (-not $subCommand) {
28-
# Top-level commands
29-
$commands += @('completion', 'login', 'logout', 'notebook', 'template', 'user', 'workflow', 'workspace')
30-
} else {
31-
switch ($subCommand) {
32-
'notebook' { $commands += @('list', 'get', 'create', 'update', 'delete', 'run') }
33-
'template' { $commands += @('list', 'get', 'create', 'update', 'delete') }
34-
'user' { $commands += @('list', 'get', 'create', 'update', 'delete') }
35-
'workflow' { $commands += @('list', 'get', 'create', 'update', 'delete', 'run') }
36-
'workspace' { $commands += @('list', 'get', 'create', 'update', 'delete') }
37-
'completion' { $commands += @() }
38-
}
39-
}
40-
41-
return $commands
71+
# Dynamically defined command structure
72+
$topLevelCommands = @({', '.join(f"'{cmd}'" for cmd in top_level_commands)})
73+
74+
# Subcommand mappings
75+
$subCommands = @{{"""
76+
77+
# Add subcommand mappings
78+
for cmd, subcmds in commands_dict.items():
79+
if cmd and subcmds: # Skip empty command key and empty subcommand lists
80+
subcmd_list = ", ".join(f"'{sub}'" for sub in subcmds)
81+
powershell_script += f"\n '{cmd}' = @({subcmd_list})"
82+
83+
powershell_script += """
4284
}
4385
44-
# Function to get available options
86+
# Function to get available options dynamically by calling slcli --help
4587
function Get-SlcliOptions {
4688
param($command, $subCommand = $null)
4789
4890
$options = @()
4991
50-
# Global options
51-
$options += @('--help', '-h', '--version')
52-
53-
# Command-specific options
54-
if ($command -eq 'completion') {
55-
$options += @('--shell', '--install')
56-
} elseif ($command -eq 'login') {
57-
$options += @('--url', '--api-key')
58-
} elseif ($subCommand) {
59-
switch ($subCommand) {
60-
'list' { $options += @('--format', '-f', '--filter', '--take', '--sortby', '--order') }
61-
'get' { $options += @('--id', '--name', '--email', '--format', '-f') }
62-
'create' {
63-
switch ($command) {
64-
'user' { $options += @('--first-name', '--last-name', '--email', '--niua-id', '--accepted-tos', '--policies', '--keywords', '--properties') }
65-
default { $options += @('--name', '--description') }
66-
}
67-
}
68-
'update' { $options += @('--id', '--name', '--description') }
69-
'delete' { $options += @('--id') }
92+
try {
93+
# Build command to get help for
94+
$helpCmd = @('slcli')
95+
if ($command) { $helpCmd += $command }
96+
if ($subCommand) { $helpCmd += $subCommand }
97+
$helpCmd += '--help'
98+
99+
# Get options from help output
100+
$helpOutput = & $helpCmd[0] $helpCmd[1..($helpCmd.Length-1)] 2>$null
101+
if ($LASTEXITCODE -eq 0) {
102+
$options = $helpOutput |
103+
Select-String -Pattern "^\\s+(--?\\w+(?:-\\w+)*)" |
104+
ForEach-Object { $_.Matches[0].Groups[1].Value }
105+
return $options
70106
}
107+
} catch {
108+
# Fallback to basic options
71109
}
72110
73-
return $options
74-
}
75-
76-
# Function to get option values
77-
function Get-SlcliOptionValues {
78-
param($option)
111+
# Fallback hardcoded options
112+
$options += @('--help', '-h')
79113
80-
switch ($option) {
81-
'--format' { return @('table', 'json') }
82-
'-f' { return @('table', 'json') }
83-
'--shell' { return @('bash', 'zsh', 'fish', 'powershell') }
84-
'--order' { return @('ascending', 'descending') }
85-
'--sortby' {
86-
# This would depend on the command context
87-
return @('name', 'created', 'updated', 'firstName', 'lastName', 'email', 'status')
88-
}
89-
default { return @() }
114+
if (-not $command) {
115+
$options += @('--version')
90116
}
117+
118+
return $options
91119
}
92120
93-
# Parse current command context
94-
$currentCommand = $null
95-
$currentSubCommand = $null
96-
$lastOption = $null
121+
# Determine what to complete based on current position
122+
$completions = @()
97123
98-
for ($i = 0; $i -lt $arguments.Count; $i++) {
99-
$arg = $arguments[$i]
124+
if ($arguments.Count -eq 0) {
125+
# Complete top-level commands
126+
$completions = $topLevelCommands | Where-Object { $_ -like "$wordToComplete*" }
127+
} elseif ($arguments.Count -eq 1) {
128+
$firstArg = $arguments[0]
100129
101-
if ($arg.StartsWith('--') -or $arg.StartsWith('-')) {
102-
$lastOption = $arg
103-
} elseif (-not $currentCommand) {
104-
$currentCommand = $arg
105-
} elseif (-not $currentSubCommand) {
106-
$currentSubCommand = $arg
107-
} else {
108-
$lastOption = $null
109-
}
110-
}
111-
112-
# If we're completing an option value
113-
if ($lastOption) {
114-
$values = Get-SlcliOptionValues -option $lastOption
115-
$values | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
116-
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
130+
if ($firstArg.StartsWith('-')) {
131+
# Complete global options
132+
$completions = Get-SlcliOptions | Where-Object { $_ -like "$wordToComplete*" }
133+
} elseif ($subCommands.ContainsKey($firstArg)) {
134+
# Complete subcommands
135+
$completions = $subCommands[$firstArg] | Where-Object { $_ -like "$wordToComplete*" }
117136
}
118-
return
119-
}
120-
121-
# If we're completing an option
122-
if ($wordToComplete.StartsWith('-')) {
123-
$options = Get-SlcliOptions -command $currentCommand -subCommand $currentSubCommand
124-
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
125-
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_)
137+
} elseif ($arguments.Count -eq 2) {
138+
$command = $arguments[0]
139+
$subCommand = $arguments[1]
140+
141+
if ($subCommand.StartsWith('-')) {
142+
# Complete command options
143+
$completions = Get-SlcliOptions $command | Where-Object { $_ -like "$wordToComplete*" }
144+
} else {
145+
# Complete subcommand options
146+
$completions = Get-SlcliOptions $command $subCommand | Where-Object { $_ -like "$wordToComplete*" }
126147
}
127-
return
148+
} else {
149+
# Complete options for the current command/subcommand context
150+
$command = $arguments[0]
151+
$subCommand = if ($arguments.Count -gt 1 -and -not $arguments[1].StartsWith('-')) { $arguments[1] } else { $null }
152+
$completions = Get-SlcliOptions $command $subCommand | Where-Object { $_ -like "$wordToComplete*" }
128153
}
129154
130-
# If we're completing a command or subcommand
131-
if (-not $currentCommand) {
132-
# Top-level commands
133-
$commands = Get-SlcliCommands
134-
$commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
135-
[System.Management.Automation.CompletionResult]::new($_, $_, 'Command', $_)
136-
}
137-
} elseif (-not $currentSubCommand) {
138-
# Subcommands
139-
$commands = Get-SlcliCommands -subCommand $currentCommand
140-
$commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
141-
[System.Management.Automation.CompletionResult]::new($_, $_, 'Command', $_)
142-
}
143-
}
144-
}
155+
$completions | ForEach-Object {{
156+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
157+
}}
158+
}}
145159
"""
146160

161+
return powershell_script
162+
147163

148164
def detect_shell() -> Optional[str]:
149165
"""Auto-detect the current shell."""
@@ -162,7 +178,8 @@ def detect_shell() -> Optional[str]:
162178
def generate_completion_script(shell: str) -> Optional[str]:
163179
"""Generate completion script for the specified shell."""
164180
if shell.lower() == "powershell":
165-
return generate_powershell_completion()
181+
# Use dynamic completion for PowerShell
182+
return generate_powershell_completion_dynamic()
166183

167184
# For bash, zsh, fish - use Click's built-in completion
168185
env_vars = {
@@ -287,6 +304,7 @@ def install_powershell_completion(completion_script: str) -> bool:
287304
def install_completion_for_shell(shell: str) -> bool:
288305
"""Install completion script for the specified shell."""
289306
completion_script = generate_completion_script(shell)
307+
290308
if not completion_script:
291309
click.echo("✗ Failed to generate completion script", err=True)
292310
return False
@@ -346,6 +364,7 @@ def completion(shell, install):
346364
else:
347365
# Just output the completion script
348366
completion_script = generate_completion_script(shell)
367+
349368
if completion_script:
350369
click.echo(completion_script)
351370
else:

0 commit comments

Comments
 (0)