ai-cli-toolkit 0.2.0__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.
@@ -0,0 +1,680 @@
1
+ """Generate concise shell completions for ai-cli and wrapped tools.
2
+
3
+ Design goals:
4
+ - Keep completion scripts human-readable and fast (no huge autogenerated blobs)
5
+ - Distinguish wrapped aliases (~/.ai-cli/bin/<tool>) from native binaries
6
+ - Offer wrapper-specific flags only when command is wrapped
7
+ - Capture baseline completion scripts from tools that expose built-in generators
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ from ai_cli.config import ensure_config, get_tool_config
21
+ from ai_cli.tools import load_registry
22
+
23
+ WRAPPER_FLAGS = [
24
+ "--ai-cli-system-instructions-file",
25
+ "--ai-cli-system-instructions-text",
26
+ "--ai-cli-canary-rule",
27
+ "--ai-cli-passthrough",
28
+ "--ai-cli-debug-requests",
29
+ "--ai-cli-developer-instructions-mode",
30
+ "--ai-cli-no-startup-context",
31
+ ]
32
+
33
+
34
+ @dataclass
35
+ class ToolCompletionData:
36
+ name: str
37
+ native_flags: list[str]
38
+ native_subcommands: list[str]
39
+ file_flags: list[str]
40
+ dir_flags: list[str]
41
+
42
+
43
+ def _run(cmd: list[str], timeout: int = 5) -> tuple[int, str]:
44
+ try:
45
+ proc = subprocess.run(
46
+ cmd, check=False, capture_output=True, text=True,
47
+ timeout=timeout, stdin=subprocess.DEVNULL,
48
+ )
49
+ except subprocess.TimeoutExpired:
50
+ return 1, ""
51
+ except OSError:
52
+ return 1, ""
53
+ output = (proc.stdout or "") + (proc.stderr or "")
54
+ return proc.returncode, output
55
+
56
+
57
+ def _looks_like_script(text: str, shell: str) -> bool:
58
+ if not text.strip():
59
+ return False
60
+ if shell == "bash":
61
+ return "complete " in text or "compgen" in text
62
+ return "compdef" in text or "_arguments" in text
63
+
64
+
65
+ def _baseline_completion_command(tool: str, binary: str, shell: str) -> Optional[list[str]]:
66
+ if tool == "codex":
67
+ return [binary, "completion", shell]
68
+ if tool in {"copilot", "gemini", "claude"}:
69
+ return [binary, "completion", shell]
70
+ return None
71
+
72
+
73
+ def _extract_flags(help_text: str) -> list[tuple[Optional[str], str, bool]]:
74
+ flags: list[tuple[Optional[str], str, bool]] = []
75
+ seen: set[str] = set()
76
+ regex = re.compile(r"^\s*(?:(-[A-Za-z0-9]),\s*)?(--[A-Za-z0-9][A-Za-z0-9-]*)(.*)$")
77
+
78
+ for raw in help_text.splitlines():
79
+ line = raw.rstrip("\n")
80
+ m = regex.match(line)
81
+ if not m:
82
+ continue
83
+ short = m.group(1)
84
+ long_opt = m.group(2)
85
+ tail = m.group(3) or ""
86
+ if long_opt in seen:
87
+ continue
88
+ seen.add(long_opt)
89
+ expects_value = any(tok in tail for tok in ("<", ":", "="))
90
+ flags.append((short, long_opt, expects_value))
91
+
92
+ return flags
93
+
94
+
95
+ def _extract_commands(help_text: str) -> list[str]:
96
+ commands: list[str] = []
97
+ seen: set[str] = set()
98
+ in_block = False
99
+ cmd_line = re.compile(r"^ ([A-Za-z][A-Za-z0-9-]*)\s{2,}.*$")
100
+
101
+ for raw in help_text.splitlines():
102
+ line = raw.rstrip("\n")
103
+ low = line.strip().lower()
104
+
105
+ if not in_block and (
106
+ low.startswith("commands:") or low.startswith("available commands:")
107
+ ):
108
+ in_block = True
109
+ continue
110
+
111
+ if not in_block:
112
+ continue
113
+
114
+ if not line.strip():
115
+ continue
116
+
117
+ if low.startswith(("options:", "flags:", "arguments:", "positionals:", "examples:")):
118
+ break
119
+
120
+ m = cmd_line.match(raw)
121
+ if not m:
122
+ continue
123
+
124
+ token = m.group(1).strip("[]")
125
+ if not token or token.startswith("-") or token in {"[query..]", "[command]"}:
126
+ continue
127
+ if token in seen:
128
+ continue
129
+ seen.add(token)
130
+ commands.append(token)
131
+
132
+ return commands
133
+
134
+
135
+ def _classify_flags(flags: list[tuple[Optional[str], str, bool]]) -> tuple[list[str], list[str]]:
136
+ file_flags: list[str] = []
137
+ dir_flags: list[str] = []
138
+ for _, long_opt, expects in flags:
139
+ if not expects:
140
+ continue
141
+ low = long_opt.lower()
142
+ if any(k in low for k in ("dir", "directory", "worktree")):
143
+ dir_flags.append(long_opt)
144
+ elif any(k in low for k in ("file", "path", "config", "schema", "settings", "ca")):
145
+ file_flags.append(long_opt)
146
+ return sorted(set(file_flags)), sorted(set(dir_flags))
147
+
148
+
149
+ _responsive_tools: set[str] = set()
150
+ """Tools whose --help responded within timeout (skip slow ones in baseline)."""
151
+
152
+
153
+ def _tool_data() -> list[ToolCompletionData]:
154
+ registry = load_registry()
155
+ cfg = ensure_config()
156
+ result: list[ToolCompletionData] = []
157
+
158
+ for name, spec in registry.items():
159
+ tool_cfg = get_tool_config(cfg, name)
160
+ binary = spec.resolve_binary(tool_cfg.get("binary", ""))
161
+ code, help_text = _run([binary, "--help"])
162
+ if code != 0 and not help_text.strip():
163
+ help_text = ""
164
+ else:
165
+ _responsive_tools.add(name)
166
+
167
+ parsed_flags = _extract_flags(help_text)
168
+ commands = _extract_commands(help_text)
169
+ file_flags, dir_flags = _classify_flags(parsed_flags)
170
+
171
+ native_flags: list[str] = []
172
+ for short, long_opt, _ in parsed_flags:
173
+ if short:
174
+ native_flags.append(short)
175
+ native_flags.append(long_opt)
176
+
177
+ result.append(
178
+ ToolCompletionData(
179
+ name=name,
180
+ native_flags=sorted(set(native_flags)),
181
+ native_subcommands=commands,
182
+ file_flags=file_flags,
183
+ dir_flags=dir_flags,
184
+ )
185
+ )
186
+
187
+ return result
188
+
189
+
190
+ def _capture_baselines(root: Path, tools: list[ToolCompletionData], shell: str) -> None:
191
+ registry = load_registry()
192
+ cfg = ensure_config()
193
+ dest = root / "completions" / "generated" / "baseline"
194
+ dest.mkdir(parents=True, exist_ok=True)
195
+
196
+ for tool in tools:
197
+ if tool.name not in _responsive_tools:
198
+ continue
199
+ spec = registry[tool.name]
200
+ tool_cfg = get_tool_config(cfg, tool.name)
201
+ binary = spec.resolve_binary(tool_cfg.get("binary", ""))
202
+ cmd = _baseline_completion_command(tool.name, binary, shell)
203
+ if not cmd:
204
+ continue
205
+ rc, out = _run(cmd)
206
+ if rc == 0 and _looks_like_script(out, shell):
207
+ ext = "bash" if shell == "bash" else "zsh"
208
+ (dest / f"{tool.name}.{ext}").write_text(out, encoding="utf-8")
209
+
210
+
211
+ def _join(values: list[str]) -> str:
212
+ return " ".join(values)
213
+
214
+
215
+ def _render_bash(tools: list[ToolCompletionData]) -> str:
216
+ case_blocks = []
217
+ for t in tools:
218
+ case_blocks.append(
219
+ f""" {t.name})
220
+ native_flags=\"{_join(t.native_flags)}\"
221
+ native_subcommands=\"{_join(t.native_subcommands)}\"
222
+ file_flags=\"{_join(t.file_flags)}\"
223
+ dir_flags=\"{_join(t.dir_flags)}\"
224
+ ;;
225
+ """
226
+ )
227
+
228
+ ai_subcommands = "claude codex copilot gemini menu status system prompt-edit session traffic update completions help"
229
+ tool_names = "claude codex copilot gemini"
230
+ wrapper_flags = _join(WRAPPER_FLAGS)
231
+ traffic_callers = "claude copilot codex gemini"
232
+ traffic_sort_opts = "date address"
233
+
234
+ return f'''#!/usr/bin/env bash
235
+ # Bash completion for ai-cli and wrapped/native tool aliases.
236
+ # Generated by ai_cli.completion_gen
237
+
238
+ _ai_cli_is_wrapped_cmd() {{
239
+ local cmd="$1"
240
+ local resolved
241
+ resolved="$(command -v "$cmd" 2>/dev/null || true)"
242
+ [[ "$resolved" == "$HOME/.ai-cli/bin/$cmd" ]]
243
+ }}
244
+
245
+ _ai_cli_fill_tool_data() {{
246
+ local tool="$1"
247
+ native_flags=""
248
+ native_subcommands=""
249
+ file_flags=""
250
+ dir_flags=""
251
+ case "$tool" in
252
+ {''.join(case_blocks)} *) ;;
253
+ esac
254
+ }}
255
+
256
+ _ai_cli_tool_completion_impl() {{
257
+ local tool="$1"
258
+ local cur prev words cword
259
+ _init_completion 2>/dev/null || {{
260
+ COMPREPLY=()
261
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
262
+ prev="${{COMP_WORDS[COMP_CWORD-1]}}"
263
+ words=("${{COMP_WORDS[@]}}")
264
+ cword=$COMP_CWORD
265
+ }}
266
+
267
+ _ai_cli_fill_tool_data "$tool"
268
+
269
+ local flags="$native_flags"
270
+ if _ai_cli_is_wrapped_cmd "$tool"; then
271
+ flags="$flags {wrapper_flags}"
272
+ fi
273
+
274
+ case " $file_flags " in
275
+ *" $prev "*) COMPREPLY=($(compgen -f -- "$cur")); return ;;
276
+ esac
277
+ case " $dir_flags " in
278
+ *" $prev "*) COMPREPLY=($(compgen -d -- "$cur")); return ;;
279
+ esac
280
+
281
+ if [[ "$cur" == -* ]]; then
282
+ COMPREPLY=($(compgen -W "$flags" -- "$cur"))
283
+ return
284
+ fi
285
+
286
+ local i subcommand=""
287
+ for (( i=1; i < cword; i++ )); do
288
+ if [[ "${{words[i]}}" != -* ]]; then
289
+ subcommand="${{words[i]}}"
290
+ break
291
+ fi
292
+ done
293
+
294
+ if [[ -z "$subcommand" ]]; then
295
+ COMPREPLY=($(compgen -W "$native_subcommands" -- "$cur"))
296
+ COMPREPLY+=($(compgen -d -- "$cur"))
297
+ fi
298
+ }}
299
+
300
+ _ai_cli_completion() {{
301
+ local cur prev words cword
302
+ _init_completion 2>/dev/null || {{
303
+ COMPREPLY=()
304
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
305
+ prev="${{COMP_WORDS[COMP_CWORD-1]}}"
306
+ words=("${{COMP_WORDS[@]}}")
307
+ cword=$COMP_CWORD
308
+ }}
309
+
310
+ # Top-level subcommand completions with descriptions
311
+ if [[ $cword -eq 1 ]]; then
312
+ COMPREPLY=($(compgen -W "{ai_subcommands}" -- "$cur"))
313
+ return
314
+ fi
315
+
316
+ local cmd="${{words[1]}}"
317
+ case "$cmd" in
318
+ claude|codex|copilot|gemini)
319
+ _ai_cli_tool_completion_impl "$cmd"
320
+ return ;;
321
+ system)
322
+ if [[ $cword -eq 2 ]]; then
323
+ COMPREPLY=($(compgen -W "prompt {tool_names}" -- "$cur"))
324
+ else
325
+ COMPREPLY=($(compgen -W "{tool_names}" -- "$cur"))
326
+ fi
327
+ return ;;
328
+ session)
329
+ case "$prev" in
330
+ --agent) COMPREPLY=($(compgen -W "all {tool_names}" -- "$cur")); return ;;
331
+ --tail) return ;;
332
+ esac
333
+ if [[ "$cur" == -* ]]; then
334
+ COMPREPLY=($(compgen -W "--agent --all --list --grep --tail --tools --raw" -- "$cur"))
335
+ else
336
+ COMPREPLY=($(compgen -f -- "$cur"))
337
+ fi
338
+ return ;;
339
+ traffic)
340
+ case "$prev" in
341
+ --caller|-c) COMPREPLY=($(compgen -W "{traffic_callers}" -- "$cur")); return ;;
342
+ --sort) COMPREPLY=($(compgen -W "{traffic_sort_opts}" -- "$cur")); return ;;
343
+ --db) COMPREPLY=($(compgen -f -- "$cur")); return ;;
344
+ --host|--search|-s|--limit|-n|--detail|-d) return ;;
345
+ esac
346
+ if [[ "$cur" == -* ]]; then
347
+ COMPREPLY=($(compgen -W "--caller --host --search --api --sort --limit --db --plain --detail" -- "$cur"))
348
+ fi
349
+ return ;;
350
+ update)
351
+ if [[ "$cur" == -* ]]; then
352
+ COMPREPLY=($(compgen -W "--all --dry-run --list --method --list-methods" -- "$cur"))
353
+ else
354
+ COMPREPLY=($(compgen -W "{tool_names}" -- "$cur"))
355
+ fi
356
+ return ;;
357
+ completions)
358
+ if [[ $cword -eq 2 ]]; then
359
+ COMPREPLY=($(compgen -W "generate" -- "$cur"))
360
+ elif [[ "$cur" == -* ]]; then
361
+ COMPREPLY=($(compgen -W "--shell" -- "$cur"))
362
+ fi
363
+ return ;;
364
+ esac
365
+ }}
366
+
367
+ _ai_cli_claude_completion() {{ _ai_cli_tool_completion_impl claude; }}
368
+ _ai_cli_codex_completion() {{ _ai_cli_tool_completion_impl codex; }}
369
+ _ai_cli_copilot_completion() {{ _ai_cli_tool_completion_impl copilot; }}
370
+ _ai_cli_gemini_completion() {{ _ai_cli_tool_completion_impl gemini; }}
371
+
372
+ complete -F _ai_cli_completion ai-cli
373
+ complete -F _ai_cli_claude_completion claude
374
+ complete -F _ai_cli_codex_completion codex
375
+ complete -F _ai_cli_copilot_completion copilot
376
+ complete -F _ai_cli_gemini_completion gemini
377
+ '''
378
+
379
+
380
+ def _render_zsh(tools: list[ToolCompletionData]) -> str:
381
+ case_blocks = []
382
+ for t in tools:
383
+ case_blocks.append(
384
+ f""" {t.name})
385
+ native_flags=({_join(t.native_flags)})
386
+ native_subcommands=({_join(t.native_subcommands)})
387
+ file_flags=({_join(t.file_flags)})
388
+ dir_flags=({_join(t.dir_flags)})
389
+ ;;
390
+ """
391
+ )
392
+
393
+ ai_subcommands = "claude codex copilot gemini menu status system prompt-edit session traffic update completions help"
394
+ tool_names = "claude codex copilot gemini"
395
+ wrapper_flags = _join(WRAPPER_FLAGS)
396
+ traffic_callers = "claude copilot codex gemini"
397
+ traffic_sort_opts = "date address"
398
+
399
+ return f'''#compdef ai-cli claude codex copilot gemini
400
+ # Zsh completion for ai-cli and wrapped/native tool aliases.
401
+ # Generated by ai_cli.completion_gen
402
+
403
+ autoload -U is-at-least
404
+
405
+ _ai_cli_is_wrapped_cmd() {{
406
+ local cmd="$1"
407
+ local resolved
408
+ resolved="$(whence -p "$cmd" 2>/dev/null)"
409
+ [[ "$resolved" == "$HOME/.ai-cli/bin/$cmd" ]]
410
+ }}
411
+
412
+ _ai_cli_fill_tool_data() {{
413
+ local tool="$1"
414
+ native_flags=()
415
+ native_subcommands=()
416
+ file_flags=()
417
+ dir_flags=()
418
+ case "$tool" in
419
+ {''.join(case_blocks)} *) ;;
420
+ esac
421
+ }}
422
+
423
+ _ai_cli_tool_completion_impl() {{
424
+ local tool="$1"
425
+ _ai_cli_fill_tool_data "$tool"
426
+
427
+ local -a flags
428
+ flags=(${{native_flags[@]}})
429
+ if _ai_cli_is_wrapped_cmd "$tool"; then
430
+ flags+=({wrapper_flags})
431
+ fi
432
+
433
+ local prev="${{words[CURRENT-1]}}"
434
+ if (( ${{file_flags[(Ie)$prev]}} )); then
435
+ _files
436
+ return
437
+ fi
438
+ if (( ${{dir_flags[(Ie)$prev]}} )); then
439
+ _directories
440
+ return
441
+ fi
442
+
443
+ if [[ "${{words[CURRENT]}}" == -* ]]; then
444
+ _describe 'option' flags
445
+ return
446
+ fi
447
+
448
+ local i subcommand=""
449
+ for ((i=2; i<CURRENT; i++)); do
450
+ if [[ "${{words[i]}}" != -* ]]; then
451
+ subcommand="${{words[i]}}"
452
+ break
453
+ fi
454
+ done
455
+
456
+ if [[ -z "$subcommand" ]]; then
457
+ _describe 'command' native_subcommands
458
+ _directories
459
+ fi
460
+ }}
461
+
462
+ _ai_cli_main_completion() {{
463
+ local cur="${{words[CURRENT]}}"
464
+ local cmd="${{words[2]}}"
465
+
466
+ if (( CURRENT == 2 )); then
467
+ local -a sc
468
+ sc=(
469
+ "claude:Launch Claude Code agent"
470
+ "codex:Launch OpenAI Codex agent"
471
+ "copilot:Launch GitHub Copilot CLI"
472
+ "gemini:Launch Google Gemini CLI"
473
+ "menu:Interactive tool manager (TUI)"
474
+ "status:Show installed tools and versions"
475
+ "system:Edit or view system instructions"
476
+ "prompt-edit:Edit global or tool prompt file"
477
+ "session:Browse agent conversation history"
478
+ "traffic:Browse and search proxied API traffic"
479
+ "update:Install or update wrapped tools"
480
+ "completions:Generate shell completion scripts"
481
+ "help:Show usage information"
482
+ )
483
+ _describe 'command' sc
484
+ return
485
+ fi
486
+
487
+ case "$cmd" in
488
+ claude|codex|copilot|gemini)
489
+ _ai_cli_tool_completion_impl "$cmd"
490
+ return ;;
491
+ system)
492
+ if (( CURRENT == 3 )); then
493
+ local -a sa
494
+ sa=(
495
+ "prompt:Show captured system prompt for a model"
496
+ "claude:Edit Claude instructions"
497
+ "codex:Edit Codex instructions"
498
+ "copilot:Edit Copilot instructions"
499
+ "gemini:Edit Gemini instructions"
500
+ )
501
+ _describe 'subcommand' sa
502
+ else
503
+ local -a tn
504
+ tn=({tool_names})
505
+ _describe 'tool' tn
506
+ fi
507
+ return ;;
508
+ prompt-edit)
509
+ if (( CURRENT == 3 )); then
510
+ local -a sa
511
+ sa=(
512
+ "global:Edit global prompt file"
513
+ "tool:Edit tool-specific prompt file"
514
+ )
515
+ _describe 'scope' sa
516
+ elif (( CURRENT == 4 )); then
517
+ local -a tn
518
+ tn=({tool_names})
519
+ _describe 'tool' tn
520
+ fi
521
+ return ;;
522
+ session)
523
+ case "${{words[CURRENT-1]}}" in
524
+ --agent)
525
+ local -a a
526
+ a=(
527
+ "all:All tools"
528
+ "claude:Claude sessions"
529
+ "codex:Codex sessions"
530
+ "copilot:Copilot sessions"
531
+ "gemini:Gemini sessions"
532
+ )
533
+ _describe 'agent' a
534
+ return ;;
535
+ --tail)
536
+ _message 'number'
537
+ return ;;
538
+ esac
539
+ if [[ "$cur" == -* ]]; then
540
+ local -a sf
541
+ sf=(
542
+ "--agent:Filter by tool name"
543
+ "--all:Show all sessions"
544
+ "--list:List sessions"
545
+ "--grep:Search session content"
546
+ "--tail:Show last N entries"
547
+ "--tools:Show tool metadata"
548
+ "--raw:Raw output format"
549
+ )
550
+ _describe 'flag' sf
551
+ else
552
+ _files
553
+ fi
554
+ return ;;
555
+ traffic)
556
+ case "${{words[CURRENT-1]}}" in
557
+ --caller|-c)
558
+ local -a tc
559
+ tc=(
560
+ "claude:Show Claude traffic"
561
+ "copilot:Show Copilot traffic"
562
+ "codex:Show Codex traffic"
563
+ "gemini:Show Gemini traffic"
564
+ )
565
+ _describe 'caller' tc
566
+ return ;;
567
+ --sort)
568
+ local -a so
569
+ so=(
570
+ "date:Sort by timestamp (newest first)"
571
+ "address:Sort by host and path"
572
+ )
573
+ _describe 'sort order' so
574
+ return ;;
575
+ --db)
576
+ _files
577
+ return ;;
578
+ --host|--search|-s|--limit|-n|--detail|-d)
579
+ return ;;
580
+ esac
581
+ if [[ "$cur" == -* ]]; then
582
+ local -a tf
583
+ tf=(
584
+ "--caller:Filter by caller tool"
585
+ "--host:Filter by host substring"
586
+ "--search:Search request/response bodies"
587
+ "--api:Show only confirmed API calls"
588
+ "--sort:Sort order (date or address)"
589
+ "--limit:Maximum rows to show"
590
+ "--db:Path to traffic database"
591
+ "--plain:Plain text output (no curses)"
592
+ "--detail:Show detail for a specific row ID"
593
+ )
594
+ _describe 'flag' tf
595
+ fi
596
+ return ;;
597
+ update)
598
+ if [[ "$cur" == -* ]]; then
599
+ local -a uf
600
+ uf=(
601
+ "--all:Update all tools"
602
+ "--dry-run:Show what would be done"
603
+ "--list:List available tools"
604
+ "--method:Installation method"
605
+ "--list-methods:List install methods"
606
+ )
607
+ _describe 'flag' uf
608
+ else
609
+ local -a tn
610
+ tn=({tool_names})
611
+ _describe 'tool' tn
612
+ fi
613
+ return ;;
614
+ completions)
615
+ if (( CURRENT == 3 )); then
616
+ local -a acts
617
+ acts=("generate:Generate completion scripts")
618
+ _describe 'action' acts
619
+ elif [[ "$cur" == -* ]]; then
620
+ local -a cf
621
+ cf=("--shell:Shell type (bash, zsh, all)")
622
+ _describe 'flag' cf
623
+ fi
624
+ return ;;
625
+ esac
626
+ }}
627
+
628
+ _ai_cli_command_completion() {{ _ai_cli_main_completion "$@"; }}
629
+ _ai_cli_claude_completion() {{ _ai_cli_tool_completion_impl claude; }}
630
+ _ai_cli_codex_completion() {{ _ai_cli_tool_completion_impl codex; }}
631
+ _ai_cli_copilot_completion() {{ _ai_cli_tool_completion_impl copilot; }}
632
+ _ai_cli_gemini_completion() {{ _ai_cli_tool_completion_impl gemini; }}
633
+
634
+ compdef _ai_cli_command_completion ai-cli
635
+ compdef _ai_cli_claude_completion claude
636
+ compdef _ai_cli_codex_completion codex
637
+ compdef _ai_cli_copilot_completion copilot
638
+ compdef _ai_cli_gemini_completion gemini
639
+ '''
640
+
641
+
642
+ def generate(shell: str = "all") -> int:
643
+ root = Path(__file__).resolve().parent.parent
644
+ comp_dir = root / "completions"
645
+ comp_dir.mkdir(parents=True, exist_ok=True)
646
+
647
+ tools = _tool_data()
648
+
649
+ if shell in ("all", "bash"):
650
+ _capture_baselines(root, tools, "bash")
651
+ (comp_dir / "ai-cli.bash").write_text(_render_bash(tools), encoding="utf-8")
652
+
653
+ if shell in ("all", "zsh"):
654
+ _capture_baselines(root, tools, "zsh")
655
+ (comp_dir / "_ai-cli").write_text(_render_zsh(tools), encoding="utf-8")
656
+
657
+ print("Generated completions:")
658
+ print(f"- {comp_dir / '_ai-cli'}")
659
+ print(f"- {comp_dir / 'ai-cli.bash'}")
660
+ print(f"- {comp_dir / 'generated' / 'baseline'}")
661
+ return 0
662
+
663
+
664
+ def main(argv: Optional[list[str]] = None) -> int:
665
+ parser = argparse.ArgumentParser(prog="ai-cli completions")
666
+ sub = parser.add_subparsers(dest="action")
667
+ gen = sub.add_parser("generate")
668
+ gen.add_argument("--shell", choices=["bash", "zsh", "all"], default="all")
669
+ args = parser.parse_args(argv)
670
+
671
+ action = args.action or "generate"
672
+ if action == "generate":
673
+ return generate(shell=getattr(args, "shell", "all"))
674
+
675
+ parser.print_help()
676
+ return 1
677
+
678
+
679
+ if __name__ == "__main__":
680
+ raise SystemExit(main())