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.
- agentworks/__init__.py +1 -0
- agentworks/agents/__init__.py +0 -0
- agentworks/agents/manager.py +1095 -0
- agentworks/agents/templates.py +145 -0
- agentworks/catalog.py +264 -0
- agentworks/catalog.toml +131 -0
- agentworks/cli.py +1462 -0
- agentworks/completions/__init__.py +33 -0
- agentworks/completions/bash.py +179 -0
- agentworks/completions/install.py +122 -0
- agentworks/completions/powershell.py +270 -0
- agentworks/completions/spec.py +216 -0
- agentworks/completions/zsh.py +256 -0
- agentworks/config.py +894 -0
- agentworks/db.py +1083 -0
- agentworks/doctor.py +430 -0
- agentworks/git_credentials/__init__.py +0 -0
- agentworks/git_credentials/azdo.py +29 -0
- agentworks/git_credentials/base.py +71 -0
- agentworks/git_credentials/github.py +22 -0
- agentworks/nerf-config.yaml +16 -0
- agentworks/output.py +296 -0
- agentworks/remote_exec.py +286 -0
- agentworks/sample-config.toml +289 -0
- agentworks/sessions/__init__.py +0 -0
- agentworks/sessions/console.py +164 -0
- agentworks/sessions/manager.py +1297 -0
- agentworks/sessions/templates.py +101 -0
- agentworks/sessions/tmux.py +503 -0
- agentworks/sources.py +303 -0
- agentworks/ssh.py +759 -0
- agentworks/ssh_config.py +255 -0
- agentworks/vm_hosts/__init__.py +0 -0
- agentworks/vm_hosts/manager.py +86 -0
- agentworks/vms/__init__.py +0 -0
- agentworks/vms/backup.py +409 -0
- agentworks/vms/base.py +56 -0
- agentworks/vms/bootstrap_script.py +185 -0
- agentworks/vms/cloud_init.py +55 -0
- agentworks/vms/initializer.py +1523 -0
- agentworks/vms/manager.py +1122 -0
- agentworks/vms/provisioners/__init__.py +0 -0
- agentworks/vms/provisioners/azure.py +602 -0
- agentworks/vms/provisioners/lima.py +295 -0
- agentworks/vms/provisioners/proxmox.py +279 -0
- agentworks/vms/provisioners/proxmox_api.py +261 -0
- agentworks/vms/provisioners/wsl2.py +340 -0
- agentworks/vms/templates.py +152 -0
- agentworks/workspaces/__init__.py +0 -0
- agentworks/workspaces/backends/__init__.py +0 -0
- agentworks/workspaces/backends/local.py +119 -0
- agentworks/workspaces/backends/vm.py +175 -0
- agentworks/workspaces/manager.py +1080 -0
- agentworks/workspaces/templates.py +76 -0
- agentworks/workspaces/tmuxinator.py +80 -0
- agentworks_cli-0.2.1.dist-info/METADATA +635 -0
- agentworks_cli-0.2.1.dist-info/RECORD +59 -0
- agentworks_cli-0.2.1.dist-info/WHEEL +4 -0
- 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}}}")
|