agentworks-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. agentworks/__init__.py +1 -0
  2. agentworks/agents/__init__.py +0 -0
  3. agentworks/agents/manager.py +1095 -0
  4. agentworks/agents/templates.py +145 -0
  5. agentworks/catalog.py +264 -0
  6. agentworks/catalog.toml +131 -0
  7. agentworks/cli.py +1462 -0
  8. agentworks/completions/__init__.py +33 -0
  9. agentworks/completions/bash.py +179 -0
  10. agentworks/completions/install.py +122 -0
  11. agentworks/completions/powershell.py +270 -0
  12. agentworks/completions/spec.py +216 -0
  13. agentworks/completions/zsh.py +256 -0
  14. agentworks/config.py +894 -0
  15. agentworks/db.py +1083 -0
  16. agentworks/doctor.py +430 -0
  17. agentworks/git_credentials/__init__.py +0 -0
  18. agentworks/git_credentials/azdo.py +29 -0
  19. agentworks/git_credentials/base.py +71 -0
  20. agentworks/git_credentials/github.py +22 -0
  21. agentworks/nerf-config.yaml +16 -0
  22. agentworks/output.py +296 -0
  23. agentworks/remote_exec.py +286 -0
  24. agentworks/sample-config.toml +289 -0
  25. agentworks/sessions/__init__.py +0 -0
  26. agentworks/sessions/console.py +164 -0
  27. agentworks/sessions/manager.py +1297 -0
  28. agentworks/sessions/templates.py +101 -0
  29. agentworks/sessions/tmux.py +503 -0
  30. agentworks/sources.py +303 -0
  31. agentworks/ssh.py +759 -0
  32. agentworks/ssh_config.py +255 -0
  33. agentworks/vm_hosts/__init__.py +0 -0
  34. agentworks/vm_hosts/manager.py +86 -0
  35. agentworks/vms/__init__.py +0 -0
  36. agentworks/vms/backup.py +409 -0
  37. agentworks/vms/base.py +56 -0
  38. agentworks/vms/bootstrap_script.py +185 -0
  39. agentworks/vms/cloud_init.py +55 -0
  40. agentworks/vms/initializer.py +1523 -0
  41. agentworks/vms/manager.py +1122 -0
  42. agentworks/vms/provisioners/__init__.py +0 -0
  43. agentworks/vms/provisioners/azure.py +602 -0
  44. agentworks/vms/provisioners/lima.py +295 -0
  45. agentworks/vms/provisioners/proxmox.py +279 -0
  46. agentworks/vms/provisioners/proxmox_api.py +261 -0
  47. agentworks/vms/provisioners/wsl2.py +340 -0
  48. agentworks/vms/templates.py +152 -0
  49. agentworks/workspaces/__init__.py +0 -0
  50. agentworks/workspaces/backends/__init__.py +0 -0
  51. agentworks/workspaces/backends/local.py +119 -0
  52. agentworks/workspaces/backends/vm.py +175 -0
  53. agentworks/workspaces/manager.py +1080 -0
  54. agentworks/workspaces/templates.py +76 -0
  55. agentworks/workspaces/tmuxinator.py +80 -0
  56. agentworks_cli-0.2.1.dist-info/METADATA +635 -0
  57. agentworks_cli-0.2.1.dist-info/RECORD +59 -0
  58. agentworks_cli-0.2.1.dist-info/WHEEL +4 -0
  59. agentworks_cli-0.2.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,33 @@
1
+ """Shell completion script generation for agentworks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agentworks.completions.spec import build_spec, completion_version
6
+
7
+
8
+ def generate(shell: str) -> str:
9
+ """Generate a completion script for the given shell."""
10
+ from agentworks.cli import app
11
+ from agentworks.completions.bash import generate_bash
12
+ from agentworks.completions.powershell import generate_powershell
13
+ from agentworks.completions.zsh import generate_zsh
14
+
15
+ spec = build_spec(app)
16
+ version = completion_version(spec)
17
+
18
+ generators = {
19
+ "bash": generate_bash,
20
+ "zsh": generate_zsh,
21
+ "powershell": generate_powershell,
22
+ }
23
+
24
+ generator = generators.get(shell)
25
+ if generator is None:
26
+ supported = ", ".join(sorted(generators))
27
+ msg = f"Unsupported shell: {shell}. Supported: {supported}"
28
+ raise ValueError(msg)
29
+
30
+ return generator(spec, version)
31
+
32
+
33
+ SUPPORTED_SHELLS = ("bash", "zsh", "powershell")
@@ -0,0 +1,179 @@
1
+ """Generate bash completion script from a CommandSpec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from agentworks.completions.spec import CommandSpec
9
+
10
+ # Shell snippets that provide dynamic completions.
11
+ DYNAMIC_SNIPPETS: dict[str, str] = {
12
+ "vms": ("$(agentworks vm list 2>/dev/null | tail -n +3 | awk '{print $1}')"),
13
+ "vm_hosts": ("$(agentworks vm-host list 2>/dev/null | tail -n +3 | awk '{print $1}')"),
14
+ "workspaces": ("$(agentworks workspace list 2>/dev/null | tail -n +3 | awk '{print $1}')"),
15
+ "ws_templates": (
16
+ "$(sed -n 's/^\\[workspace_templates\\.\\([^]]*\\)\\]/\\1/p'"
17
+ ' "$HOME/.config/agentworks/config.toml" 2>/dev/null)'
18
+ ),
19
+ "git_credentials": (
20
+ "$(sed -n 's/^\\[git_credentials\\.\\([^]]*\\)\\]/\\1/p' \"$HOME/.config/agentworks/config.toml\" 2>/dev/null)"
21
+ ),
22
+ "catalog_entries": ("$(agentworks installer list 2>/dev/null | tail -n +3 | awk '{print $2}')"),
23
+ "sessions": ("$(agentworks session list --no-status 2>/dev/null | tail -n +3 | awk '{print $1}')"),
24
+ "agents": ("$(agentworks agent list 2>/dev/null | tail -n +3 | awk '{print $1}')"),
25
+ "session_templates": (
26
+ "default $(sed -n 's/^\\[session_templates\\.\\([^]]*\\)\\]/\\1/p'"
27
+ ' "$HOME/.config/agentworks/config.toml" 2>/dev/null)'
28
+ ),
29
+ "vm_templates": (
30
+ "$(sed -n 's/^\\[vm_templates\\.\\([^]]*\\)\\]/\\1/p' \"$HOME/.config/agentworks/config.toml\" 2>/dev/null)"
31
+ ),
32
+ "agent_templates": (
33
+ "$(sed -n 's/^\\[agent_templates\\.\\([^]]*\\)\\]/\\1/p' \"$HOME/.config/agentworks/config.toml\" 2>/dev/null)"
34
+ ),
35
+ }
36
+
37
+
38
+ def generate_bash(spec: CommandSpec, version: str) -> str:
39
+ """Generate a complete bash completion script."""
40
+ lines: list[str] = []
41
+
42
+ lines.append("# Auto-generated by agentworks. Do not edit.")
43
+ lines.append(f"# agentworks-completion-version: {version}")
44
+ lines.append("#")
45
+ lines.append("# Install:")
46
+ lines.append("# mkdir -p ~/.local/share/bash-completion/completions")
47
+ lines.append("# agentworks completion bash > ~/.local/share/bash-completion/completions/agentworks")
48
+ lines.append("#")
49
+ lines.append("# Or source directly in ~/.bashrc:")
50
+ lines.append('# eval "$(agentworks completion bash)"')
51
+ lines.append("")
52
+
53
+ lines.append("_agentworks() {")
54
+ lines.append(" local cur prev words cword")
55
+ lines.append(" if type _init_completion &>/dev/null; then")
56
+ lines.append(" _init_completion || return")
57
+ lines.append(" else")
58
+ lines.append(' cur="${COMP_WORDS[COMP_CWORD]}"')
59
+ lines.append(' prev="${COMP_WORDS[COMP_CWORD-1]}"')
60
+ lines.append(' words=("${COMP_WORDS[@]}")')
61
+ lines.append(" cword=$COMP_CWORD")
62
+ lines.append(" fi")
63
+ lines.append("")
64
+
65
+ _emit_dispatch(lines, spec)
66
+
67
+ lines.append("}")
68
+ lines.append("")
69
+ lines.append("complete -F _agentworks agentworks")
70
+ lines.append("")
71
+
72
+ return "\n".join(lines)
73
+
74
+
75
+ def _emit_dispatch(lines: list[str], spec: CommandSpec) -> None:
76
+ """Emit the main dispatch logic."""
77
+ # Build sorted subcommand names
78
+ sub_names = sorted(spec.subcommands.keys())
79
+ sub_names_str = " ".join(sub_names)
80
+
81
+ lines.append(" # Determine command context")
82
+ lines.append(' local cmd1="${words[1]:-}"')
83
+ lines.append(' local cmd2="${words[2]:-}"')
84
+ lines.append("")
85
+
86
+ # Level 1: completing the top-level command name
87
+ lines.append(" # Top-level commands")
88
+ lines.append(" if [[ $cword -eq 1 ]]; then")
89
+ lines.append(f' COMPREPLY=($(compgen -W "{sub_names_str}" -- "$cur"))')
90
+ lines.append(" return")
91
+ lines.append(" fi")
92
+ lines.append("")
93
+
94
+ # Level 2: dispatch to subcommand groups
95
+ lines.append(' case "$cmd1" in')
96
+
97
+ for name in sub_names:
98
+ sub = spec.subcommands[name]
99
+ lines.append(f" {name})")
100
+ if sub.subcommands:
101
+ _emit_group_completions(lines, sub)
102
+ else:
103
+ _emit_leaf_completions(lines, sub, token_offset=2)
104
+ lines.append(" ;;")
105
+
106
+ lines.append(" esac")
107
+
108
+
109
+ def _emit_group_completions(lines: list[str], spec: CommandSpec) -> None:
110
+ """Emit completions for a command group (has subcommands)."""
111
+ sub_names = sorted(spec.subcommands.keys())
112
+ sub_names_str = " ".join(sub_names)
113
+
114
+ # If we're completing the subcommand name
115
+ lines.append(" if [[ $cword -eq 2 ]]; then")
116
+ lines.append(f' COMPREPLY=($(compgen -W "{sub_names_str}" -- "$cur"))')
117
+ lines.append(" return")
118
+ lines.append(" fi")
119
+
120
+ # Dispatch to leaf commands
121
+ lines.append(' case "$cmd2" in')
122
+ for name in sub_names:
123
+ sub = spec.subcommands[name]
124
+ lines.append(f" {name})")
125
+ _emit_leaf_completions(lines, sub, token_offset=3)
126
+ lines.append(" ;;")
127
+ lines.append(" esac")
128
+
129
+
130
+ def _emit_leaf_completions(lines: list[str], spec: CommandSpec, token_offset: int) -> None:
131
+ """Emit completions for a leaf command."""
132
+ indent = " " if token_offset == 2 else " "
133
+
134
+ options_with_values = [p for p in spec.params if not p.is_argument and not p.is_flag]
135
+ positional_args = [p for p in spec.params if p.is_argument]
136
+ all_options = [p for p in spec.params if not p.is_argument]
137
+
138
+ # Check if previous token is an option expecting a value
139
+ if options_with_values:
140
+ lines.append(f'{indent}case "$prev" in')
141
+ for param in options_with_values:
142
+ opt = param.opts[0] if param.opts else f"--{param.name}"
143
+ lines.append(f"{indent} {opt})")
144
+ if param.choices:
145
+ choices_str = " ".join(param.choices)
146
+ lines.append(f'{indent} COMPREPLY=($(compgen -W "{choices_str}" -- "$cur"))')
147
+ elif param.dynamic_completer and param.dynamic_completer in DYNAMIC_SNIPPETS:
148
+ snippet = DYNAMIC_SNIPPETS[param.dynamic_completer]
149
+ lines.append(f'{indent} COMPREPLY=($(compgen -W "{snippet}" -- "$cur"))')
150
+ lines.append(f"{indent} return")
151
+ lines.append(f"{indent} ;;")
152
+ lines.append(f"{indent}esac")
153
+
154
+ # Positional argument completions
155
+ if positional_args:
156
+ param = positional_args[0]
157
+ words: str | None = None
158
+ if param.choices:
159
+ words = " ".join(param.choices)
160
+ elif param.dynamic_completer and param.dynamic_completer in DYNAMIC_SNIPPETS:
161
+ words = DYNAMIC_SNIPPETS[param.dynamic_completer]
162
+ if words:
163
+ lines.append(f'{indent}if [[ $cword -eq {token_offset} && "$cur" != -* ]]; then')
164
+ lines.append(f'{indent} COMPREPLY=($(compgen -W "{words}" -- "$cur"))')
165
+ lines.append(f"{indent} return")
166
+ lines.append(f"{indent}fi")
167
+
168
+ # Fall through to option completions
169
+ if all_options:
170
+ opts = []
171
+ for param in all_options:
172
+ opt = param.opts[0] if param.opts else f"--{param.name}"
173
+ opts.append(opt)
174
+ opts.append("--help")
175
+ opts_str = " ".join(opts)
176
+ lines.append(f'{indent}if [[ "$cur" == -* ]]; then')
177
+ lines.append(f'{indent} COMPREPLY=($(compgen -W "{opts_str}" -- "$cur"))')
178
+ lines.append(f"{indent} return")
179
+ lines.append(f"{indent}fi")
@@ -0,0 +1,122 @@
1
+ """Install shell completions to the default location."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+
13
+ def install_completions(shell: str, script: str) -> None:
14
+ """Write the completion script to the appropriate location."""
15
+ if shell == "bash":
16
+ _install_bash(script)
17
+ elif shell == "zsh":
18
+ _install_zsh(script)
19
+ elif shell == "powershell":
20
+ _install_powershell(script)
21
+ else:
22
+ typer.echo(f"Error: --install not supported for '{shell}'", err=True)
23
+ raise typer.Exit(1)
24
+
25
+
26
+ def _install_bash(script: str) -> None:
27
+ """Install bash completions to the standard user directory."""
28
+ target_dir = Path.home() / ".local" / "share" / "bash-completion" / "completions"
29
+ target_dir.mkdir(parents=True, exist_ok=True)
30
+ target = target_dir / "agentworks"
31
+ target.write_text(script)
32
+ typer.echo(f"Installed to {target}")
33
+
34
+ # Check if bash-completion is likely available
35
+ bashrc = Path.home() / ".bashrc"
36
+ if bashrc.exists():
37
+ content = bashrc.read_text()
38
+ if "bash-completion" in content or "bash_completion" in content:
39
+ return
40
+ typer.echo("Note: ensure bash-completion is installed and loaded in your .bashrc")
41
+
42
+
43
+ def _install_zsh(script: str) -> None:
44
+ """Install zsh completions to Oh My Zsh custom dir or ~/.zfunc."""
45
+ home = Path.home()
46
+
47
+ # Prefer Oh My Zsh if present
48
+ zsh_custom = os.environ.get("ZSH_CUSTOM")
49
+ if zsh_custom:
50
+ target_dir = Path(zsh_custom) / "completions"
51
+ elif (home / ".oh-my-zsh" / "custom").is_dir():
52
+ target_dir = home / ".oh-my-zsh" / "custom" / "completions"
53
+ else:
54
+ target_dir = home / ".zfunc"
55
+
56
+ target_dir.mkdir(parents=True, exist_ok=True)
57
+ target = target_dir / "_agentworks"
58
+ target.write_text(script)
59
+ typer.echo(f"Installed to {target}")
60
+
61
+ # Check if ~/.zfunc needs fpath setup (not needed for Oh My Zsh)
62
+ if target_dir.name == ".zfunc":
63
+ typer.echo("Note: ensure your .zshrc has: fpath=(~/.zfunc $fpath)")
64
+
65
+
66
+ def _install_powershell(script: str) -> None:
67
+ """Install PowerShell completions and update $PROFILE to source them."""
68
+ profile_path = _query_powershell_profile()
69
+ if profile_path is None:
70
+ typer.echo("Error: could not determine PowerShell $PROFILE path", err=True)
71
+ typer.echo("Is powershell or pwsh installed and on PATH?", err=True)
72
+ raise typer.Exit(1)
73
+
74
+ # Install completions next to the profile
75
+ target_dir = profile_path.parent / "Completions"
76
+ target_dir.mkdir(parents=True, exist_ok=True)
77
+ target = target_dir / "agentworks.ps1"
78
+ target.write_text(script)
79
+ typer.echo(f"Installed to {target}")
80
+
81
+ # Ensure $PROFILE sources the completion script
82
+ if profile_path.exists():
83
+ content = profile_path.read_text()
84
+ if "agentworks.ps1" in content:
85
+ typer.echo("$PROFILE already sources agentworks completions")
86
+ return
87
+ else:
88
+ content = ""
89
+
90
+ source_line = f'. "{target}"'
91
+ profile_path.parent.mkdir(parents=True, exist_ok=True)
92
+ with profile_path.open("a") as f:
93
+ if content and not content.endswith("\n"):
94
+ f.write("\n")
95
+ f.write(f"{source_line}\n")
96
+ typer.echo(f"Added to $PROFILE: {profile_path}")
97
+
98
+
99
+ def _query_powershell_profile() -> Path | None:
100
+ """Ask PowerShell for the actual $PROFILE path.
101
+
102
+ Tries pwsh (PowerShell Core) first, then powershell (Windows PowerShell).
103
+ Uses -NoProfile to avoid loading a broken profile during the query.
104
+ """
105
+ for cmd in ("pwsh", "powershell"):
106
+ if not shutil.which(cmd):
107
+ continue
108
+ try:
109
+ result = subprocess.run(
110
+ [cmd, "-NoProfile", "-Command", "$PROFILE"],
111
+ capture_output=True,
112
+ text=True,
113
+ encoding="utf-8",
114
+ errors="replace",
115
+ timeout=10,
116
+ )
117
+ path = result.stdout.strip()
118
+ if result.returncode == 0 and path:
119
+ return Path(path)
120
+ except (subprocess.TimeoutExpired, OSError):
121
+ continue
122
+ return None
@@ -0,0 +1,270 @@
1
+ """Generate PowerShell completion script from a CommandSpec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from agentworks.completions.spec import CommandSpec
9
+
10
+ # PowerShell snippets that provide dynamic completions.
11
+ DYNAMIC_SNIPPETS: dict[str, str] = {
12
+ "vms": (
13
+ "(agentworks vm list 2>$null | Select-Object -Skip 2 |"
14
+ " ForEach-Object { ($_ -split '\\s+')[0] } |"
15
+ ' Where-Object { $_ -like "$wordToComplete*" })'
16
+ ),
17
+ "vm_hosts": (
18
+ "(agentworks vm-host list 2>$null | Select-Object -Skip 2 |"
19
+ " ForEach-Object { ($_ -split '\\s+')[0] } |"
20
+ ' Where-Object { $_ -like "$wordToComplete*" })'
21
+ ),
22
+ "workspaces": (
23
+ "(agentworks workspace list 2>$null | Select-Object -Skip 2 |"
24
+ " ForEach-Object { ($_ -split '\\s+')[0] } |"
25
+ ' Where-Object { $_ -like "$wordToComplete*" })'
26
+ ),
27
+ "ws_templates": (
28
+ "& { $f = Join-Path $env:USERPROFILE '.config/agentworks/config.toml';"
29
+ " if (Test-Path $f) {"
30
+ " Get-Content $f | Select-String '^\\[workspace_templates\\.([^\\]]+)\\]'"
31
+ " | ForEach-Object { $_.Matches[0].Groups[1].Value }"
32
+ ' | Where-Object { $_ -like "$wordToComplete*" } } }'
33
+ ),
34
+ "git_credentials": (
35
+ "& { $f = Join-Path $env:USERPROFILE '.config/agentworks/config.toml';"
36
+ " if (Test-Path $f) {"
37
+ " Get-Content $f | Select-String '^\\[git_credentials\\.([^\\]]+)\\]'"
38
+ " | ForEach-Object { $_.Matches[0].Groups[1].Value }"
39
+ ' | Where-Object { $_ -like "$wordToComplete*" } } }'
40
+ ),
41
+ "catalog_entries": (
42
+ "& { agentworks installer list 2>$null"
43
+ " | Select-Object -Skip 2"
44
+ " | ForEach-Object { ($_ -split '\\s+')[1] }"
45
+ ' | Where-Object { $_ -like "$wordToComplete*" } }'
46
+ ),
47
+ "sessions": (
48
+ "(agentworks session list --no-status 2>$null | Select-Object -Skip 2 |"
49
+ " ForEach-Object { ($_ -split '\\s+')[0] } |"
50
+ ' Where-Object { $_ -like "$wordToComplete*" })'
51
+ ),
52
+ "agents": (
53
+ "(agentworks agent list 2>$null | Select-Object -Skip 2 |"
54
+ " ForEach-Object { ($_ -split '\\s+')[0] } |"
55
+ ' Where-Object { $_ -like "$wordToComplete*" })'
56
+ ),
57
+ "session_templates": (
58
+ "& { $builtins = @('default');"
59
+ " $f = Join-Path $env:USERPROFILE '.config/agentworks/config.toml';"
60
+ " $user = @(); if (Test-Path $f) {"
61
+ " $user = Get-Content $f | Select-String '^\\[session_templates\\.([^\\]]+)\\]'"
62
+ " | ForEach-Object { $_.Matches[0].Groups[1].Value } }"
63
+ ' ($builtins + $user) | Where-Object { $_ -like "$wordToComplete*" } }'
64
+ ),
65
+ "vm_templates": (
66
+ "& { $f = Join-Path $env:USERPROFILE '.config/agentworks/config.toml';"
67
+ " $t = @(); if (Test-Path $f) {"
68
+ " $t = Get-Content $f | Select-String '^\\[vm_templates\\.([^\\]]+)\\]'"
69
+ " | ForEach-Object { $_.Matches[0].Groups[1].Value } }"
70
+ ' $t | Where-Object { $_ -like "$wordToComplete*" } }'
71
+ ),
72
+ "agent_templates": (
73
+ "& { $f = Join-Path $env:USERPROFILE '.config/agentworks/config.toml';"
74
+ " $t = @(); if (Test-Path $f) {"
75
+ " $t = Get-Content $f | Select-String '^\\[agent_templates\\.([^\\]]+)\\]'"
76
+ " | ForEach-Object { $_.Matches[0].Groups[1].Value } }"
77
+ ' $t | Where-Object { $_ -like "$wordToComplete*" } }'
78
+ ),
79
+ }
80
+
81
+
82
+ def _open_result_array(lines: list[str], indent: str) -> None:
83
+ """Open a @(...) array for CompletionResult items."""
84
+ lines.append(f"{indent}@(")
85
+
86
+
87
+ def _close_result_array_with_filter(lines: list[str], indent: str) -> None:
88
+ """Close the @(...) array and pipe through Where-Object."""
89
+ lines.append(f'{indent}) | Where-Object {{ $_.CompletionText -like "$wordToComplete*" }}')
90
+
91
+
92
+ def generate_powershell(spec: CommandSpec, version: str) -> str:
93
+ """Generate a complete PowerShell completion script."""
94
+ lines: list[str] = []
95
+
96
+ lines.append("# Auto-generated by agentworks. Do not edit.")
97
+ lines.append(f"# agentworks-completion-version: {version}")
98
+ lines.append("#")
99
+ lines.append("# Install:")
100
+ lines.append('# $dir = "$HOME\\Documents\\PowerShell\\Completions"')
101
+ lines.append("# New-Item -ItemType Directory -Force -Path $dir")
102
+ lines.append('# agentworks completion powershell > "$dir\\agentworks.ps1"')
103
+ lines.append('# then add to $PROFILE: . "$dir\\agentworks.ps1"')
104
+ lines.append("")
105
+ lines.append("Register-ArgumentCompleter -Native -CommandName agentworks -ScriptBlock {")
106
+ lines.append(" param($wordToComplete, $commandAst, $cursorPosition)")
107
+ lines.append("")
108
+ lines.append(" $tokens = $commandAst.ToString().Trim() -split '\\s+'")
109
+ lines.append(" $tokenCount = if ($wordToComplete -eq '') { $tokens.Count + 1 } else { $tokens.Count }")
110
+ lines.append("")
111
+ lines.append(" # Determine command context")
112
+ lines.append(" $cmd1 = if ($tokenCount -gt 1) { $tokens[1] } else { $null }")
113
+ lines.append(" $cmd2 = if ($tokenCount -gt 2) { $tokens[2] } else { $null }")
114
+ lines.append("")
115
+
116
+ _emit_dispatch(lines, spec)
117
+
118
+ lines.append("}")
119
+ lines.append("")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def _emit_dispatch(lines: list[str], spec: CommandSpec) -> None:
125
+ """Emit the main dispatch logic."""
126
+ # Level 1: completing the top-level command/group name
127
+ lines.append(" # Top-level commands")
128
+ lines.append(" if ($tokenCount -le 2) {")
129
+ _open_result_array(lines, " ")
130
+
131
+ for name, sub in sorted(spec.subcommands.items()):
132
+ escaped = sub.help.replace("'", "''")
133
+ lines.append(
134
+ f" [System.Management.Automation.CompletionResult]::new('{name}', '{name}',"
135
+ f" 'Command', '{escaped}')"
136
+ )
137
+
138
+ _close_result_array_with_filter(lines, " ")
139
+ lines.append(" return")
140
+ lines.append(" }")
141
+ lines.append("")
142
+
143
+ # Level 2: dispatch to subcommand groups
144
+ lines.append(" switch ($cmd1) {")
145
+
146
+ for name, sub in sorted(spec.subcommands.items()):
147
+ lines.append(f" '{name}' {{")
148
+ if sub.subcommands:
149
+ _emit_group_completions(lines, sub, depth=2)
150
+ else:
151
+ _emit_leaf_completions(lines, sub)
152
+ lines.append(" }")
153
+
154
+ lines.append(" }")
155
+
156
+
157
+ def _emit_group_completions(lines: list[str], spec: CommandSpec, depth: int) -> None:
158
+ """Emit completions for a command group (has subcommands)."""
159
+ indent = " " * 3
160
+
161
+ # If we're completing the subcommand name
162
+ lines.append(f"{indent}if ($tokenCount -le 3) {{")
163
+ _open_result_array(lines, f"{indent} ")
164
+
165
+ for name, sub in sorted(spec.subcommands.items()):
166
+ escaped = sub.help.replace("'", "''")
167
+ lines.append(
168
+ f"{indent} [System.Management.Automation.CompletionResult]::new('{name}', '{name}',"
169
+ f" 'Command', '{escaped}')"
170
+ )
171
+
172
+ _close_result_array_with_filter(lines, f"{indent} ")
173
+ lines.append(f"{indent} return")
174
+ lines.append(f"{indent}}}")
175
+ lines.append("")
176
+
177
+ # Dispatch to leaf commands
178
+ lines.append(f"{indent}switch ($cmd2) {{")
179
+ for name, sub in sorted(spec.subcommands.items()):
180
+ lines.append(f"{indent} '{name}' {{")
181
+ _emit_param_completions(lines, sub, token_offset=3)
182
+ lines.append(f"{indent} }}")
183
+ lines.append(f"{indent}}}")
184
+
185
+
186
+ def _emit_leaf_completions(lines: list[str], spec: CommandSpec) -> None:
187
+ """Emit completions for a leaf command (no subcommands)."""
188
+ _emit_param_completions(lines, spec, token_offset=2)
189
+
190
+
191
+ def _emit_param_completions(lines: list[str], spec: CommandSpec, token_offset: int) -> None:
192
+ """Emit parameter completions for a leaf command."""
193
+ indent = " " * 4
194
+
195
+ # Check if the previous token is an option expecting a value
196
+ options_with_values = [p for p in spec.params if not p.is_argument and not p.is_flag]
197
+ positional_args = [p for p in spec.params if p.is_argument]
198
+
199
+ if options_with_values:
200
+ lines.append(f"{indent}$prevToken = $tokens[$tokenCount - 2]")
201
+ lines.append(f"{indent}switch ($prevToken) {{")
202
+
203
+ for param in options_with_values:
204
+ opt = param.opts[0] if param.opts else f"--{param.name}"
205
+ lines.append(f"{indent} '{opt}' {{")
206
+
207
+ if param.choices:
208
+ _open_result_array(lines, f"{indent} ")
209
+ for choice in param.choices:
210
+ lines.append(
211
+ f"{indent} [System.Management.Automation.CompletionResult]::new('{choice}',"
212
+ f" '{choice}', 'ParameterValue', '{choice}')"
213
+ )
214
+ _close_result_array_with_filter(lines, f"{indent} ")
215
+ elif param.dynamic_completer and param.dynamic_completer in DYNAMIC_SNIPPETS:
216
+ snippet = DYNAMIC_SNIPPETS[param.dynamic_completer]
217
+ lines.append(f"{indent} {snippet}")
218
+ else:
219
+ lines.append(f"{indent} # free text")
220
+
221
+ lines.append(f"{indent} return")
222
+ lines.append(f"{indent} }}")
223
+
224
+ lines.append(f"{indent}}}")
225
+ lines.append("")
226
+
227
+ # Positional argument completions
228
+ if positional_args:
229
+ param = positional_args[0]
230
+ if param.choices:
231
+ lines.append(f"{indent}# Positional: {param.name}")
232
+ lines.append(f"{indent}if ($wordToComplete -notlike '-*' -and $tokenCount -eq {token_offset + 1}) {{")
233
+ _open_result_array(lines, f"{indent} ")
234
+ for choice in param.choices:
235
+ lines.append(
236
+ f"{indent} [System.Management.Automation.CompletionResult]::new('{choice}',"
237
+ f" '{choice}', 'ParameterValue', '{choice}')"
238
+ )
239
+ _close_result_array_with_filter(lines, f"{indent} ")
240
+ lines.append(f"{indent} return")
241
+ lines.append(f"{indent}}}")
242
+ lines.append("")
243
+ elif param.dynamic_completer and param.dynamic_completer in DYNAMIC_SNIPPETS:
244
+ snippet = DYNAMIC_SNIPPETS[param.dynamic_completer]
245
+ lines.append(f"{indent}# Positional: {param.name}")
246
+ lines.append(f"{indent}if ($wordToComplete -notlike '-*' -and $tokenCount -eq {token_offset + 1}) {{")
247
+ lines.append(f"{indent} {snippet}")
248
+ lines.append(f"{indent} return")
249
+ lines.append(f"{indent}}}")
250
+ lines.append("")
251
+
252
+ # Fall through to option completions
253
+ all_options = [p for p in spec.params if not p.is_argument]
254
+ if all_options:
255
+ lines.append(f"{indent}# Options")
256
+ lines.append(f"{indent}if ($wordToComplete -like '-*') {{")
257
+ _open_result_array(lines, f"{indent} ")
258
+ for param in all_options:
259
+ opt = param.opts[0] if param.opts else f"--{param.name}"
260
+ escaped = param.help.replace("'", "''")
261
+ lines.append(
262
+ f"{indent} [System.Management.Automation.CompletionResult]::new('{opt}', '{opt}',"
263
+ f" 'ParameterValue', '{escaped}')"
264
+ )
265
+ lines.append(
266
+ f"{indent} [System.Management.Automation.CompletionResult]::new('--help', '--help',"
267
+ " 'ParameterValue', 'Show help')"
268
+ )
269
+ _close_result_array_with_filter(lines, f"{indent} ")
270
+ lines.append(f"{indent}}}")