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,216 @@
|
|
|
1
|
+
"""Introspect the Typer/Click command tree and build a completion spec."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
# -- Data model ------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ParamSpec:
|
|
17
|
+
"""Specification for a single CLI parameter."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
opts: list[str]
|
|
21
|
+
help: str
|
|
22
|
+
is_flag: bool
|
|
23
|
+
is_argument: bool
|
|
24
|
+
multiple: bool
|
|
25
|
+
required: bool
|
|
26
|
+
choices: list[str] | None = None
|
|
27
|
+
dynamic_completer: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CommandSpec:
|
|
32
|
+
"""Specification for a CLI command or group."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
help: str
|
|
36
|
+
params: list[ParamSpec] = field(default_factory=list)
|
|
37
|
+
subcommands: dict[str, CommandSpec] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# -- Dynamic completions (the only hand-maintained piece) ------------------
|
|
41
|
+
|
|
42
|
+
# Maps (dotted_command_path, param_name) to a completer identifier.
|
|
43
|
+
# Completer identifiers are abstract labels that each shell generator
|
|
44
|
+
# knows how to render into shell-specific completion functions.
|
|
45
|
+
#
|
|
46
|
+
# The completer identifiers and their corresponding CLI commands:
|
|
47
|
+
# "vms" -> agentworks vm list
|
|
48
|
+
# "vm_hosts" -> agentworks vm-host list
|
|
49
|
+
# "workspaces" -> agentworks workspace list
|
|
50
|
+
# "ws_templates" -> [workspace_templates.*] sections in config.toml
|
|
51
|
+
# "git_credentials" -> [git_credentials.*] sections in config.toml
|
|
52
|
+
# "catalog_entries" -> all entry names from built-in + custom catalog
|
|
53
|
+
# "sessions" -> agentworks session list --no-status
|
|
54
|
+
# "session_templates" -> [session_templates.*] sections in config.toml
|
|
55
|
+
# "agents" -> agentworks agent list
|
|
56
|
+
# "vm_templates" -> [vm_templates.*] sections in config.toml
|
|
57
|
+
# "agent_templates" -> [agent_templates.*] sections in config.toml
|
|
58
|
+
|
|
59
|
+
DYNAMIC_COMPLETIONS: dict[tuple[str, str], str] = {
|
|
60
|
+
("vm.start", "name"): "vms",
|
|
61
|
+
("vm.stop", "name"): "vms",
|
|
62
|
+
("vm.delete", "name"): "vms",
|
|
63
|
+
("vm.rekey", "name"): "vms",
|
|
64
|
+
("vm.backup", "name"): "vms",
|
|
65
|
+
("vm.describe", "name"): "vms",
|
|
66
|
+
("vm.reinit", "name"): "vms",
|
|
67
|
+
("vm.exec", "name"): "vms",
|
|
68
|
+
("vm.shell", "name"): "vms",
|
|
69
|
+
("vm.add-git-credential", "name"): "vms",
|
|
70
|
+
("vm.port-forward", "name"): "vms",
|
|
71
|
+
("vm.create", "template"): "vm_templates",
|
|
72
|
+
("vm.create", "vm_host"): "vm_hosts",
|
|
73
|
+
("vm-host.remove", "name"): "vm_hosts",
|
|
74
|
+
("workspace.shell", "name"): "workspaces",
|
|
75
|
+
("workspace.console", "name"): "workspaces",
|
|
76
|
+
("workspace.copy", "source"): "workspaces",
|
|
77
|
+
("workspace.copy", "vm"): "vms",
|
|
78
|
+
("workspace.describe", "name"): "workspaces",
|
|
79
|
+
("workspace.rehome", "name"): "workspaces",
|
|
80
|
+
("workspace.repair", "name"): "workspaces",
|
|
81
|
+
("workspace.delete", "name"): "workspaces",
|
|
82
|
+
("workspace.create", "vm"): "vms",
|
|
83
|
+
("workspace.create", "template"): "ws_templates",
|
|
84
|
+
("workspace.list", "vm"): "vms",
|
|
85
|
+
("vm.logs", "name"): "vms",
|
|
86
|
+
("vm.add-git-credential", "credential"): "git_credentials",
|
|
87
|
+
("agent.create", "vm"): "vms",
|
|
88
|
+
("agent.create", "template"): "agent_templates",
|
|
89
|
+
("agent.describe", "name"): "agents",
|
|
90
|
+
("agent.reinit", "name"): "agents",
|
|
91
|
+
("agent.workspace-grants.grant", "name"): "agents",
|
|
92
|
+
("agent.workspace-grants.deny", "name"): "agents",
|
|
93
|
+
("agent.workspace-grants.list", "name"): "agents",
|
|
94
|
+
("agent.exec", "name"): "agents",
|
|
95
|
+
("agent.shell", "name"): "agents",
|
|
96
|
+
("agent.shell", "workspace"): "workspaces",
|
|
97
|
+
("agent.delete", "name"): "agents",
|
|
98
|
+
("agent.list", "vm"): "vms",
|
|
99
|
+
("installer.describe", "name"): "catalog_entries",
|
|
100
|
+
# Session commands
|
|
101
|
+
("session.create", "agent"): "agents",
|
|
102
|
+
("session.create", "workspace"): "workspaces",
|
|
103
|
+
("session.create", "template"): "session_templates",
|
|
104
|
+
("session.create", "workspace_template"): "ws_templates",
|
|
105
|
+
("session.create", "vm"): "vms",
|
|
106
|
+
("session.describe", "name"): "sessions",
|
|
107
|
+
("session.list", "workspace"): "workspaces",
|
|
108
|
+
("session.stop", "name"): "sessions",
|
|
109
|
+
("session.stop", "vm"): "vms",
|
|
110
|
+
("session.stop", "workspace"): "workspaces",
|
|
111
|
+
("session.restart", "name"): "sessions",
|
|
112
|
+
("session.restart", "vm"): "vms",
|
|
113
|
+
("session.restart", "workspace"): "workspaces",
|
|
114
|
+
|
|
115
|
+
("session.attach", "name"): "sessions",
|
|
116
|
+
("session.delete", "name"): "sessions",
|
|
117
|
+
("session.logs", "name"): "sessions",
|
|
118
|
+
|
|
119
|
+
# VM console
|
|
120
|
+
("vm.console", "name"): "vms",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# -- Introspection ---------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_spec(app: typer.Typer) -> CommandSpec:
|
|
128
|
+
"""Walk the Typer app and build a CommandSpec tree."""
|
|
129
|
+
click_app = typer.main.get_command(app)
|
|
130
|
+
return _build_command_spec(click_app, path="")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_command_spec(cmd: click.Command, path: str) -> CommandSpec:
|
|
134
|
+
"""Recursively build a CommandSpec from a Click command."""
|
|
135
|
+
help_text = (cmd.help or "").split("\n")[0].strip()
|
|
136
|
+
name = cmd.name or ""
|
|
137
|
+
|
|
138
|
+
spec = CommandSpec(name=name, help=help_text)
|
|
139
|
+
|
|
140
|
+
# Build params
|
|
141
|
+
current_path = f"{path}.{name}" if path else name
|
|
142
|
+
for param in cmd.params:
|
|
143
|
+
if param.name == "help" or getattr(param, "hidden", False):
|
|
144
|
+
continue
|
|
145
|
+
spec.params.append(_build_param_spec(param, current_path))
|
|
146
|
+
|
|
147
|
+
# Build subcommands for groups
|
|
148
|
+
if isinstance(cmd, click.Group):
|
|
149
|
+
ctx = click.Context(cmd, info_name=name)
|
|
150
|
+
for sub_name in cmd.list_commands(ctx):
|
|
151
|
+
sub_cmd = cmd.get_command(ctx, sub_name)
|
|
152
|
+
if sub_cmd is not None:
|
|
153
|
+
spec.subcommands[sub_name] = _build_command_spec(sub_cmd, path=current_path)
|
|
154
|
+
|
|
155
|
+
return spec
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_param_spec(param: click.Parameter, command_path: str) -> ParamSpec:
|
|
159
|
+
"""Build a ParamSpec from a Click parameter."""
|
|
160
|
+
is_argument = isinstance(param, click.Argument)
|
|
161
|
+
|
|
162
|
+
choices = None
|
|
163
|
+
if isinstance(param.type, click.Choice):
|
|
164
|
+
choices = list(param.type.choices)
|
|
165
|
+
|
|
166
|
+
opts: list[str] = []
|
|
167
|
+
if isinstance(param, click.Option):
|
|
168
|
+
opts = list(param.opts)
|
|
169
|
+
|
|
170
|
+
# DYNAMIC_COMPLETIONS keys use paths without the root app name
|
|
171
|
+
# (e.g. "vm.shell" not "agentworks.vm.shell")
|
|
172
|
+
lookup_path = ".".join(command_path.split(".")[1:]) if "." in command_path else command_path
|
|
173
|
+
dynamic = DYNAMIC_COMPLETIONS.get((lookup_path, param.name or ""))
|
|
174
|
+
|
|
175
|
+
return ParamSpec(
|
|
176
|
+
name=param.name or "",
|
|
177
|
+
opts=opts,
|
|
178
|
+
help=getattr(param, "help", None) or "",
|
|
179
|
+
is_flag=getattr(param, "is_flag", False),
|
|
180
|
+
is_argument=is_argument,
|
|
181
|
+
multiple=param.multiple,
|
|
182
|
+
required=param.required,
|
|
183
|
+
choices=choices,
|
|
184
|
+
dynamic_completer=dynamic,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -- Version stamp ---------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def completion_version(spec: CommandSpec) -> str:
|
|
192
|
+
"""Compute a content hash of the spec for staleness detection."""
|
|
193
|
+
serialized = json.dumps(_spec_to_dict(spec), sort_keys=True)
|
|
194
|
+
return hashlib.sha256(serialized.encode()).hexdigest()[:12]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _spec_to_dict(spec: CommandSpec) -> dict: # type: ignore[type-arg]
|
|
198
|
+
"""Serialize a CommandSpec tree to a dict for hashing."""
|
|
199
|
+
return {
|
|
200
|
+
"name": spec.name,
|
|
201
|
+
"help": spec.help,
|
|
202
|
+
"params": [
|
|
203
|
+
{
|
|
204
|
+
"name": p.name,
|
|
205
|
+
"opts": p.opts,
|
|
206
|
+
"is_flag": p.is_flag,
|
|
207
|
+
"is_argument": p.is_argument,
|
|
208
|
+
"multiple": p.multiple,
|
|
209
|
+
"required": p.required,
|
|
210
|
+
"choices": p.choices,
|
|
211
|
+
"dynamic_completer": p.dynamic_completer,
|
|
212
|
+
}
|
|
213
|
+
for p in spec.params
|
|
214
|
+
],
|
|
215
|
+
"subcommands": {k: _spec_to_dict(v) for k, v in sorted(spec.subcommands.items())},
|
|
216
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Generate zsh 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, ParamSpec
|
|
9
|
+
|
|
10
|
+
# Shell functions that provide dynamic completions.
|
|
11
|
+
# Each key matches a completer identifier from DYNAMIC_COMPLETIONS.
|
|
12
|
+
DYNAMIC_FUNCTIONS: dict[str, str] = {
|
|
13
|
+
"vms": """\
|
|
14
|
+
_agentworks_vms() {
|
|
15
|
+
local -a vms
|
|
16
|
+
vms=(${(f)"$(agentworks vm list 2>/dev/null | tail -n +3 | awk '{print $1}')"})
|
|
17
|
+
_describe 'vm' vms
|
|
18
|
+
}""",
|
|
19
|
+
"vm_hosts": """\
|
|
20
|
+
_agentworks_vm_hosts() {
|
|
21
|
+
local -a hosts
|
|
22
|
+
hosts=(${(f)"$(agentworks vm-host list 2>/dev/null | tail -n +3 | awk '{print $1}')"})
|
|
23
|
+
_describe 'vm-host' hosts
|
|
24
|
+
}""",
|
|
25
|
+
"workspaces": """\
|
|
26
|
+
_agentworks_workspaces() {
|
|
27
|
+
local -a workspaces
|
|
28
|
+
workspaces=(${(f)"$(agentworks workspace list 2>/dev/null | tail -n +3 | awk '{print $1}')"})
|
|
29
|
+
_describe 'workspace' workspaces
|
|
30
|
+
}""",
|
|
31
|
+
"ws_templates": """\
|
|
32
|
+
_agentworks_templates() {
|
|
33
|
+
local -a templates config_file
|
|
34
|
+
config_file="${HOME}/.config/agentworks/config.toml"
|
|
35
|
+
[[ -f "$config_file" ]] || return
|
|
36
|
+
templates=(${(f)"$(sed -n 's/^\\[workspace_templates\\.\\([^]]*\\)\\]/\\1/p' "$config_file" 2>/dev/null)"})
|
|
37
|
+
_describe 'template' templates
|
|
38
|
+
}""",
|
|
39
|
+
"git_credentials": """\
|
|
40
|
+
_agentworks_git_credentials() {
|
|
41
|
+
local -a creds config_file
|
|
42
|
+
config_file="${HOME}/.config/agentworks/config.toml"
|
|
43
|
+
[[ -f "$config_file" ]] || return
|
|
44
|
+
creds=(${(f)"$(sed -n 's/^\\[git_credentials\\.\\([^]]*\\)\\]/\\1/p' "$config_file" 2>/dev/null)"})
|
|
45
|
+
_describe 'git-credential' creds
|
|
46
|
+
}""",
|
|
47
|
+
"catalog_entries": """\
|
|
48
|
+
_agentworks_catalog_entries() {
|
|
49
|
+
local -a entries
|
|
50
|
+
entries=(${(f)"$(agentworks installer list 2>/dev/null | tail -n +3 | awk '{print $2}')"})
|
|
51
|
+
_describe 'installer' entries
|
|
52
|
+
}""",
|
|
53
|
+
"sessions": """\
|
|
54
|
+
_agentworks_sessions() {
|
|
55
|
+
local -a sessions
|
|
56
|
+
sessions=(${(f)"$(agentworks session list --no-status 2>/dev/null | tail -n +3 | awk '{print $1}')"})
|
|
57
|
+
_describe 'session' sessions
|
|
58
|
+
}""",
|
|
59
|
+
"agents": """\
|
|
60
|
+
_agentworks_agents() {
|
|
61
|
+
local -a agents
|
|
62
|
+
agents=(${(f)"$(agentworks agent list 2>/dev/null | tail -n +3 | awk '{print $1}')"})
|
|
63
|
+
_describe 'agent' agents
|
|
64
|
+
}""",
|
|
65
|
+
"session_templates": """\
|
|
66
|
+
_agentworks_session_templates() {
|
|
67
|
+
local -a templates config_file
|
|
68
|
+
config_file="${HOME}/.config/agentworks/config.toml"
|
|
69
|
+
templates=(default)
|
|
70
|
+
if [[ -f "$config_file" ]]; then
|
|
71
|
+
templates+=(${(f)"$(sed -n 's/^\\[session_templates\\.\\([^]]*\\)\\]/\\1/p' "$config_file" 2>/dev/null)"})
|
|
72
|
+
fi
|
|
73
|
+
_describe 'session-template' templates
|
|
74
|
+
}""",
|
|
75
|
+
"vm_templates": """\
|
|
76
|
+
_agentworks_vm_templates() {
|
|
77
|
+
local -a templates config_file
|
|
78
|
+
config_file="${HOME}/.config/agentworks/config.toml"
|
|
79
|
+
templates=()
|
|
80
|
+
if [[ -f "$config_file" ]]; then
|
|
81
|
+
templates+=(${(f)"$(sed -n 's/^\\[vm_templates\\.\\([^]]*\\)\\]/\\1/p' "$config_file" 2>/dev/null)"})
|
|
82
|
+
fi
|
|
83
|
+
_describe 'vm-template' templates
|
|
84
|
+
}""",
|
|
85
|
+
"agent_templates": """\
|
|
86
|
+
_agentworks_agent_templates() {
|
|
87
|
+
local -a templates config_file
|
|
88
|
+
config_file="${HOME}/.config/agentworks/config.toml"
|
|
89
|
+
templates=()
|
|
90
|
+
if [[ -f "$config_file" ]]; then
|
|
91
|
+
templates+=(${(f)"$(sed -n 's/^\\[agent_templates\\.\\([^]]*\\)\\]/\\1/p' "$config_file" 2>/dev/null)"})
|
|
92
|
+
fi
|
|
93
|
+
_describe 'agent-template' templates
|
|
94
|
+
}""",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Maps completer identifiers to their zsh function names.
|
|
98
|
+
COMPLETER_FUNC_NAMES: dict[str, str] = {
|
|
99
|
+
"vms": "_agentworks_vms",
|
|
100
|
+
"vm_hosts": "_agentworks_vm_hosts",
|
|
101
|
+
"workspaces": "_agentworks_workspaces",
|
|
102
|
+
"ws_templates": "_agentworks_templates",
|
|
103
|
+
"git_credentials": "_agentworks_git_credentials",
|
|
104
|
+
"catalog_entries": "_agentworks_catalog_entries",
|
|
105
|
+
"sessions": "_agentworks_sessions",
|
|
106
|
+
"session_templates": "_agentworks_session_templates",
|
|
107
|
+
"vm_templates": "_agentworks_vm_templates",
|
|
108
|
+
"agent_templates": "_agentworks_agent_templates",
|
|
109
|
+
"agents": "_agentworks_agents",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_zsh(spec: CommandSpec, version: str) -> str:
|
|
114
|
+
"""Generate a complete zsh completion script."""
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
|
|
117
|
+
lines.append("#compdef agentworks")
|
|
118
|
+
lines.append("")
|
|
119
|
+
lines.append("# Auto-generated by agentworks. Do not edit.")
|
|
120
|
+
lines.append(f"# agentworks-completion-version: {version}")
|
|
121
|
+
lines.append("#")
|
|
122
|
+
lines.append("# Install (Oh My Zsh):")
|
|
123
|
+
lines.append("# mkdir -p ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/completions")
|
|
124
|
+
lines.append("# agentworks completion zsh > ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/completions/_agentworks")
|
|
125
|
+
lines.append("#")
|
|
126
|
+
lines.append("# Install (manual):")
|
|
127
|
+
lines.append("# mkdir -p ~/.zfunc && agentworks completion zsh > ~/.zfunc/_agentworks")
|
|
128
|
+
lines.append("# then add: fpath=(~/.zfunc $fpath) to your .zshrc before compinit")
|
|
129
|
+
lines.append("")
|
|
130
|
+
|
|
131
|
+
# Emit dynamic completer functions
|
|
132
|
+
used_completers = _collect_completers(spec)
|
|
133
|
+
for completer_id in sorted(used_completers):
|
|
134
|
+
if completer_id in DYNAMIC_FUNCTIONS:
|
|
135
|
+
lines.append(DYNAMIC_FUNCTIONS[completer_id])
|
|
136
|
+
lines.append("")
|
|
137
|
+
|
|
138
|
+
# Emit the main dispatch function
|
|
139
|
+
_emit_group(lines, spec, func_name="_agentworks")
|
|
140
|
+
lines.append("")
|
|
141
|
+
lines.append('_agentworks "$@"')
|
|
142
|
+
lines.append("")
|
|
143
|
+
|
|
144
|
+
return "\n".join(lines)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _collect_completers(spec: CommandSpec) -> set[str]:
|
|
148
|
+
"""Collect all dynamic completer identifiers used in the spec tree."""
|
|
149
|
+
completers: set[str] = set()
|
|
150
|
+
for param in spec.params:
|
|
151
|
+
if param.dynamic_completer:
|
|
152
|
+
completers.add(param.dynamic_completer)
|
|
153
|
+
for sub in spec.subcommands.values():
|
|
154
|
+
completers.update(_collect_completers(sub))
|
|
155
|
+
return completers
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _emit_group(lines: list[str], spec: CommandSpec, func_name: str) -> None:
|
|
159
|
+
"""Emit a zsh function for a command group."""
|
|
160
|
+
lines.append(f"{func_name}() {{")
|
|
161
|
+
lines.append(" local -a subcommands")
|
|
162
|
+
lines.append(" _arguments -C \\")
|
|
163
|
+
lines.append(" '--help[Show help]' \\")
|
|
164
|
+
lines.append(" '1:command:->command' \\")
|
|
165
|
+
lines.append(" '*::arg:->args'")
|
|
166
|
+
lines.append("")
|
|
167
|
+
lines.append(" case $state in")
|
|
168
|
+
lines.append(" command)")
|
|
169
|
+
lines.append(" subcommands=(")
|
|
170
|
+
|
|
171
|
+
for name, sub in sorted(spec.subcommands.items()):
|
|
172
|
+
escaped_help = sub.help.replace("'", "'\\''")
|
|
173
|
+
lines.append(f" '{name}:{escaped_help}'")
|
|
174
|
+
|
|
175
|
+
lines.append(" )")
|
|
176
|
+
lines.append(" _describe 'command' subcommands")
|
|
177
|
+
lines.append(" ;;")
|
|
178
|
+
lines.append(" args)")
|
|
179
|
+
lines.append(" case $line[1] in")
|
|
180
|
+
|
|
181
|
+
for name, _sub in sorted(spec.subcommands.items()):
|
|
182
|
+
sub_func = f"{func_name}_{name.replace('-', '_')}"
|
|
183
|
+
lines.append(f" {name})")
|
|
184
|
+
lines.append(f' {sub_func} "$@"')
|
|
185
|
+
lines.append(" ;;")
|
|
186
|
+
|
|
187
|
+
lines.append(" esac")
|
|
188
|
+
lines.append(" ;;")
|
|
189
|
+
lines.append(" esac")
|
|
190
|
+
lines.append("}")
|
|
191
|
+
|
|
192
|
+
# Emit sub-functions
|
|
193
|
+
for name, sub in sorted(spec.subcommands.items()):
|
|
194
|
+
sub_func = f"{func_name}_{name.replace('-', '_')}"
|
|
195
|
+
lines.append("")
|
|
196
|
+
if sub.subcommands:
|
|
197
|
+
_emit_group(lines, sub, func_name=sub_func)
|
|
198
|
+
else:
|
|
199
|
+
_emit_leaf(lines, sub, func_name=sub_func)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _emit_leaf(lines: list[str], spec: CommandSpec, func_name: str) -> None:
|
|
203
|
+
"""Emit a zsh function for a leaf command."""
|
|
204
|
+
args = _build_arguments(spec.params)
|
|
205
|
+
if not args:
|
|
206
|
+
lines.append(f"{func_name}() {{")
|
|
207
|
+
lines.append(" _arguments \\")
|
|
208
|
+
lines.append(" '--help[Show help]'")
|
|
209
|
+
lines.append("}")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
lines.append(f"{func_name}() {{")
|
|
213
|
+
lines.append(" _arguments \\")
|
|
214
|
+
for i, arg in enumerate(args):
|
|
215
|
+
separator = " \\" if i < len(args) else ""
|
|
216
|
+
lines.append(f" {arg}{separator}")
|
|
217
|
+
lines.append(" '--help[Show help]'")
|
|
218
|
+
lines.append("}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _build_arguments(params: list[ParamSpec]) -> list[str]:
|
|
222
|
+
"""Build zsh _arguments specs from params."""
|
|
223
|
+
args: list[str] = []
|
|
224
|
+
positional_index = 1
|
|
225
|
+
|
|
226
|
+
for param in params:
|
|
227
|
+
if param.is_argument:
|
|
228
|
+
label = param.name
|
|
229
|
+
if param.choices:
|
|
230
|
+
choices_str = " ".join(param.choices)
|
|
231
|
+
args.append(f"'{positional_index}:{label}:({choices_str})'")
|
|
232
|
+
elif param.dynamic_completer and param.dynamic_completer in COMPLETER_FUNC_NAMES:
|
|
233
|
+
completer = f":{COMPLETER_FUNC_NAMES[param.dynamic_completer]}"
|
|
234
|
+
args.append(f"'{positional_index}:{label}{completer}'")
|
|
235
|
+
else:
|
|
236
|
+
args.append(f"'{positional_index}:{label}:'")
|
|
237
|
+
positional_index += 1
|
|
238
|
+
elif param.is_flag:
|
|
239
|
+
escaped_help = param.help.replace("'", "'\\''")
|
|
240
|
+
opt = param.opts[0] if param.opts else f"--{param.name}"
|
|
241
|
+
args.append(f"'{opt}[{escaped_help}]'")
|
|
242
|
+
else:
|
|
243
|
+
escaped_help = param.help.replace("'", "'\\''")
|
|
244
|
+
opt = param.opts[0] if param.opts else f"--{param.name}"
|
|
245
|
+
multiple = "*" if param.multiple else ""
|
|
246
|
+
|
|
247
|
+
if param.choices:
|
|
248
|
+
choices_str = " ".join(param.choices)
|
|
249
|
+
args.append(f"'{multiple}{opt}[{escaped_help}]:{param.name}:({choices_str})'")
|
|
250
|
+
elif param.dynamic_completer and param.dynamic_completer in COMPLETER_FUNC_NAMES:
|
|
251
|
+
func = COMPLETER_FUNC_NAMES[param.dynamic_completer]
|
|
252
|
+
args.append(f"'{multiple}{opt}[{escaped_help}]:{param.name}:{func}'")
|
|
253
|
+
else:
|
|
254
|
+
args.append(f"'{multiple}{opt}[{escaped_help}]:{param.name}:'")
|
|
255
|
+
|
|
256
|
+
return args
|