@garyr/pt-cli 1.0.1 → 1.1.0

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.
package/README.md CHANGED
@@ -42,6 +42,7 @@ graph LR
42
42
  - [Quick Start](#quick-start)
43
43
  - [Installation](#installation)
44
44
  - [Basic Commands](#basic-commands)
45
+ - [Shell Completions](#shell-completions)
45
46
  - [Agent Integration](#agent-integration)
46
47
  - [Documentation](#documentation)
47
48
  - [Development](#development)
@@ -124,6 +125,25 @@ pt init ./new-project --file my-template.json --yes
124
125
 
125
126
  ```
126
127
 
128
+ ### Shell Completions
129
+
130
+ Generate and install tab completions:
131
+
132
+ ```bash
133
+ # Bash
134
+ pt completion bash > /etc/bash_completion.d/pt
135
+ # Or user-local: pt completion bash > ~/.local/share/bash-completion/completions/pt
136
+
137
+ # Zsh
138
+ pt completion zsh > ~/.zsh/completions/_pt
139
+ # Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
140
+
141
+ # Fish
142
+ pt completion fish > ~/.config/fish/completions/pt.fish
143
+ ```
144
+
145
+ Template names auto-complete dynamically for `pt init`, `pt update`, `pt config`, and `pt remove`.
146
+
127
147
  ## Agent Integration
128
148
 
129
149
  `pt-cli` is fully compatible with AI agents. By utilizing non-interactive flags (`--yes`, `--vars`, `--name`, `--desc`), agents can autonomously scaffold and learn projects without hanging on interactive terminal prompts.
@@ -191,6 +211,7 @@ pt variables [--set] [--delete] [--json]
191
211
  pt default-post-config [--set --json]
192
212
  pt ignore [patterns] [--set]
193
213
  pt security-response <response>
214
+ pt completion <shell>
194
215
  ```
195
216
 
196
217
  **Config Schema (`~/.pt/config.yaml` v3.0):**
@@ -0,0 +1,408 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import YAML from 'yaml';
5
+ import { getConfigPath } from '../config.js';
6
+ export function getTemplatesForCompletion() {
7
+ try {
8
+ const configPath = getConfigPath();
9
+ if (!fs.existsSync(configPath)) {
10
+ return [];
11
+ }
12
+ const content = fs.readFileSync(configPath, 'utf-8');
13
+ if (!content.trim()) {
14
+ return [];
15
+ }
16
+ const parsed = YAML.parse(content);
17
+ if (!parsed || !parsed.templates || typeof parsed.templates !== 'object') {
18
+ return [];
19
+ }
20
+ return Object.keys(parsed.templates);
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ export function generateBashCompletion() {
27
+ return `# Bash completion for pt
28
+ _pt_completions() {
29
+ local cur prev words cword
30
+ if declare -F _init_completion >/dev/null 2>&1; then
31
+ _init_completion || return
32
+ else
33
+ cur="\${COMP_WORDS[COMP_CWORD]}"
34
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
35
+ words=("\${COMP_WORDS[@]}")
36
+ cword=$COMP_CWORD
37
+ fi
38
+
39
+ local commands="learn update init config ignore variables default-post-config add remove rm security-response completion"
40
+
41
+ # Complete top-level command or global flags
42
+ if [[ $cword -eq 1 ]]; then
43
+ if [[ "$cur" == -* ]]; then
44
+ COMPREPLY=( $(compgen -W "-v --version -h --help" -- "$cur") )
45
+ else
46
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
47
+ fi
48
+ return 0
49
+ fi
50
+
51
+ local cmd="\${words[1]}"
52
+
53
+ case "$cmd" in
54
+ learn)
55
+ if [[ "$cur" == -* ]]; then
56
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --name --desc --json --allow-untrusted -h --help" -- "$cur") )
57
+ fi
58
+ ;;
59
+ update)
60
+ if [[ "$cur" == -* ]]; then
61
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --desc --no-diff -h --help" -- "$cur") )
62
+ elif [[ $cword -eq 2 ]]; then
63
+ local templates
64
+ templates=$(pt completion --templates 2>/dev/null)
65
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
66
+ fi
67
+ ;;
68
+ init)
69
+ if [[ "$cur" == -* ]]; then
70
+ COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars -h --help" -- "$cur") )
71
+ elif [[ $cword -eq 2 ]]; then
72
+ local templates
73
+ templates=$(pt completion --templates 2>/dev/null)
74
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
75
+ fi
76
+ ;;
77
+ config)
78
+ if [[ "$cur" == -* ]]; then
79
+ COMPREPLY=( $(compgen -W "--json -h --help" -- "$cur") )
80
+ elif [[ $cword -eq 2 ]]; then
81
+ local templates
82
+ templates=$(pt completion --templates 2>/dev/null)
83
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
84
+ fi
85
+ ;;
86
+ ignore)
87
+ if [[ "$cur" == -* ]]; then
88
+ COMPREPLY=( $(compgen -W "--set -h --help" -- "$cur") )
89
+ fi
90
+ ;;
91
+ variables)
92
+ if [[ "$cur" == -* ]]; then
93
+ COMPREPLY=( $(compgen -W "--set --json --delete -h --help" -- "$cur") )
94
+ fi
95
+ ;;
96
+ default-post-config)
97
+ if [[ "$cur" == -* ]]; then
98
+ COMPREPLY=( $(compgen -W "--set --json -h --help" -- "$cur") )
99
+ fi
100
+ ;;
101
+ add)
102
+ if [[ "$cur" == -* ]]; then
103
+ COMPREPLY=( $(compgen -W "-f --file -h --help" -- "$cur") )
104
+ fi
105
+ ;;
106
+ remove|rm)
107
+ if [[ "$cur" == -* ]]; then
108
+ COMPREPLY=( $(compgen -W "-y --yes -h --help" -- "$cur") )
109
+ elif [[ $cword -eq 2 ]]; then
110
+ local templates
111
+ templates=$(pt completion --templates 2>/dev/null)
112
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
113
+ fi
114
+ ;;
115
+ completion)
116
+ if [[ "$cur" == -* ]]; then
117
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
118
+ elif [[ $cword -eq 2 ]]; then
119
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
120
+ fi
121
+ ;;
122
+ security-response)
123
+ if [[ "$cur" == -* ]]; then
124
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
125
+ fi
126
+ ;;
127
+ esac
128
+ }
129
+
130
+ complete -F _pt_completions pt
131
+ `;
132
+ }
133
+ export function generateZshCompletion() {
134
+ return `#compdef pt
135
+
136
+ _pt_templates() {
137
+ local -a templates
138
+ templates=(\${(f)"$(pt completion --templates 2>/dev/null)"})
139
+ if [[ \${#templates[@]} -gt 0 ]]; then
140
+ _describe -t templates 'template' templates
141
+ fi
142
+ }
143
+
144
+ _pt() {
145
+ local context state state_policy
146
+ typeset -A opt_args
147
+
148
+ _arguments -C \\
149
+ '(-v --version)'{-v,--version}'[output the version number]' \\
150
+ '(-h --help)'{-h,--help}'[display help for command]' \\
151
+ '1: :->command' \\
152
+ '*:: :->args'
153
+
154
+ case $state in
155
+ command)
156
+ local -a commands
157
+ commands=(
158
+ 'learn:Learn a project structure from an existing directory'
159
+ 'update:Update an existing template with new structure/files'
160
+ 'init:Initialize a new project from a learned template'
161
+ 'config:Show current config location and list templates, or export a specific template'
162
+ 'ignore:View or set global ignore patterns (comma-separated)'
163
+ 'variables:View or set global variables (comma-separated key=value)'
164
+ 'default-post-config:View or set default post-config tasks'
165
+ 'add:Import/add a template from a JSON string or file'
166
+ 'remove:Remove a learned template from the config'
167
+ 'rm:Remove a learned template from the config'
168
+ 'security-response:Handle security response from GUI'
169
+ 'completion:Generate shell completion script'
170
+ )
171
+ _describe -t commands 'pt command' commands
172
+ ;;
173
+ args)
174
+ case $words[1] in
175
+ learn)
176
+ _arguments \\
177
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
178
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
179
+ '--name=[Template name]:name:' \\
180
+ '--desc=[Template description]:description:' \\
181
+ '--json[Output template structure as JSON for sharing instead of saving]' \\
182
+ '--allow-untrusted[Bypass the trusted-source check for remote URLs]' \\
183
+ '(-h --help)'{-h,--help}'[display help for command]' \\
184
+ '1:path:_files -/'
185
+ ;;
186
+ update)
187
+ _arguments \\
188
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
189
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
190
+ '--desc=[Template description]:description:' \\
191
+ '--no-diff[Disable additive mode, show full list]' \\
192
+ '(-h --help)'{-h,--help}'[display help for command]' \\
193
+ '1:template:_pt_templates' \\
194
+ '2:sourcePath:_files -/'
195
+ ;;
196
+ init)
197
+ _arguments \\
198
+ '(-f --file)'{-f,--file}'[Initialize directly from a JSON template file]:file:_files' \\
199
+ '--skip-post-config[Skip running post-config tasks]' \\
200
+ '--dry-run[Show what would be created without making changes]' \\
201
+ '(-y --yes)'{-y,--yes}'[Automatically answer yes to prompts]' \\
202
+ '--vars=[Comma-separated key=value variables]:variables:' \\
203
+ '(-h --help)'{-h,--help}'[display help for command]' \\
204
+ '1:template:_pt_templates' \\
205
+ '2:destPath:_files -/'
206
+ ;;
207
+ config)
208
+ _arguments \\
209
+ '--json[Output config or specific template as JSON]' \\
210
+ '(-h --help)'{-h,--help}'[display help for command]' \\
211
+ '1:template:_pt_templates'
212
+ ;;
213
+ ignore)
214
+ _arguments \\
215
+ '--set[Set the ignore patterns to the provided value]' \\
216
+ '(-h --help)'{-h,--help}'[display help for command]' \\
217
+ '1:patterns:'
218
+ ;;
219
+ variables)
220
+ _arguments \\
221
+ '--set[Set the variables to the provided pairs]' \\
222
+ '--json=[Set variables via JSON string or file]:data:' \\
223
+ '--delete=[Delete a specific global variable]:key:' \\
224
+ '(-h --help)'{-h,--help}'[display help for command]' \\
225
+ '1:pairs:'
226
+ ;;
227
+ default-post-config)
228
+ _arguments \\
229
+ '--set[Set the default post-config tasks via JSON]' \\
230
+ '--json=[JSON string or file containing tasks array]:data:' \\
231
+ '(-h --help)'{-h,--help}'[display help for command]'
232
+ ;;
233
+ add)
234
+ _arguments \\
235
+ '(-f --file)'{-f,--file}'[Path to JSON file containing template data]:file:_files' \\
236
+ '(-h --help)'{-h,--help}'[display help for command]' \\
237
+ '1:name:' \\
238
+ '2:json:'
239
+ ;;
240
+ remove|rm)
241
+ _arguments \\
242
+ '(-y --yes)'{-y,--yes}'[Automatically confirm removal]' \\
243
+ '(-h --help)'{-h,--help}'[display help for command]' \\
244
+ '1:template:_pt_templates'
245
+ ;;
246
+ security-response)
247
+ _arguments \\
248
+ '(-h --help)'{-h,--help}'[display help for command]' \\
249
+ '1:response:'
250
+ ;;
251
+ completion)
252
+ _arguments \\
253
+ '(-h --help)'{-h,--help}'[display help for command]' \\
254
+ '1:shell:(bash zsh fish)'
255
+ ;;
256
+ esac
257
+ ;;
258
+ esac
259
+ }
260
+
261
+ if [[ "$(basename -- "$0")" != "_pt" ]]; then
262
+ compdef _pt pt 2>/dev/null || true
263
+ fi
264
+ `;
265
+ }
266
+ export function generateFishCompletion() {
267
+ return `# Fish completion for pt
268
+
269
+ function __fish_pt_needs_command
270
+ set -l cmd (commandline -opc)
271
+ if [ (count $cmd) -eq 1 ]
272
+ return 0
273
+ end
274
+ return 1
275
+ end
276
+
277
+ function __fish_pt_using_command
278
+ set -l cmd (commandline -opc)
279
+ if [ (count $cmd) -gt 1 ]
280
+ if [ "$argv[1]" = "$cmd[2]" ]
281
+ return 0
282
+ end
283
+ end
284
+ return 1
285
+ end
286
+
287
+ function __fish_pt_templates
288
+ pt completion --templates 2>/dev/null
289
+ end
290
+
291
+ # Global options
292
+ complete -c pt -n '__fish_pt_needs_command' -s v -l version -d 'output the version number'
293
+ complete -c pt -n '__fish_pt_needs_command' -s h -l help -d 'display help for command'
294
+
295
+ # Commands
296
+ complete -c pt -n '__fish_pt_needs_command' -a learn -d 'Learn a project structure from an existing directory'
297
+ complete -c pt -n '__fish_pt_needs_command' -a update -d 'Update an existing template with new structure/files'
298
+ complete -c pt -n '__fish_pt_needs_command' -a init -d 'Initialize a new project from a learned template'
299
+ complete -c pt -n '__fish_pt_needs_command' -a config -d 'Show current config location and list templates, or export a specific template'
300
+ complete -c pt -n '__fish_pt_needs_command' -a ignore -d 'View or set global ignore patterns (comma-separated)'
301
+ complete -c pt -n '__fish_pt_needs_command' -a variables -d 'View or set global variables (comma-separated key=value)'
302
+ complete -c pt -n '__fish_pt_needs_command' -a default-post-config -d 'View or set default post-config tasks'
303
+ complete -c pt -n '__fish_pt_needs_command' -a add -d 'Import/add a template from a JSON string or file'
304
+ complete -c pt -n '__fish_pt_needs_command' -a remove -d 'Remove a learned template from the config'
305
+ complete -c pt -n '__fish_pt_needs_command' -a rm -d 'Remove a learned template from the config'
306
+ complete -c pt -n '__fish_pt_needs_command' -a security-response -d 'Handle security response from GUI'
307
+ complete -c pt -n '__fish_pt_needs_command' -a completion -d 'Generate shell completion script'
308
+
309
+ # learn
310
+ complete -c pt -n '__fish_pt_using_command learn' -l ignore -d 'Folder patterns to ignore (comma-separated)'
311
+ complete -c pt -n '__fish_pt_using_command learn' -s y -l yes -d 'Automatically confirm prompts'
312
+ complete -c pt -n '__fish_pt_using_command learn' -l name -d 'Template name (skip prompt)'
313
+ complete -c pt -n '__fish_pt_using_command learn' -l desc -d 'Template description (skip prompt)'
314
+ complete -c pt -n '__fish_pt_using_command learn' -l json -d 'Output template structure as JSON for sharing instead of saving'
315
+ complete -c pt -n '__fish_pt_using_command learn' -l allow-untrusted -d 'Bypass the trusted-source check for remote URLs'
316
+
317
+ # update
318
+ complete -c pt -n '__fish_pt_using_command update' -a '(__fish_pt_templates)' -d 'Template name'
319
+ complete -c pt -n '__fish_pt_using_command update' -l ignore -d 'Folder patterns to ignore (comma-separated)'
320
+ complete -c pt -n '__fish_pt_using_command update' -s y -l yes -d 'Automatically confirm prompts'
321
+ complete -c pt -n '__fish_pt_using_command update' -l desc -d 'Template description (skip prompt)'
322
+ complete -c pt -n '__fish_pt_using_command update' -l no-diff -d 'Disable additive mode, show full list'
323
+
324
+ # init
325
+ complete -c pt -n '__fish_pt_using_command init' -a '(__fish_pt_templates)' -d 'Template name'
326
+ complete -c pt -n '__fish_pt_using_command init' -s f -l file -d 'Initialize directly from a JSON template file without adding it to local config'
327
+ complete -c pt -n '__fish_pt_using_command init' -l skip-post-config -d 'Skip running post-config tasks'
328
+ complete -c pt -n '__fish_pt_using_command init' -l dry-run -d 'Show what would be created without making changes'
329
+ complete -c pt -n '__fish_pt_using_command init' -s y -l yes -d 'Automatically answer yes to prompts'
330
+ complete -c pt -n '__fish_pt_using_command init' -l vars -d 'Comma-separated key=value variables'
331
+
332
+ # config
333
+ complete -c pt -n '__fish_pt_using_command config' -a '(__fish_pt_templates)' -d 'Template name'
334
+ complete -c pt -n '__fish_pt_using_command config' -l json -d 'Output config or specific template as JSON'
335
+
336
+ # ignore
337
+ complete -c pt -n '__fish_pt_using_command ignore' -l set -d 'Set the ignore patterns to the provided value'
338
+
339
+ # variables
340
+ complete -c pt -n '__fish_pt_using_command variables' -l set -d 'Set the variables to the provided pairs'
341
+ complete -c pt -n '__fish_pt_using_command variables' -l json -d 'Set variables via JSON string or file'
342
+ complete -c pt -n '__fish_pt_using_command variables' -l delete -d 'Delete a specific global variable'
343
+
344
+ # default-post-config
345
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l set -d 'Set the default post-config tasks via JSON'
346
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l json -d 'JSON string or file containing tasks array'
347
+
348
+ # add
349
+ complete -c pt -n '__fish_pt_using_command add' -s f -l file -d 'Path to JSON file containing template data'
350
+
351
+ # remove / rm
352
+ complete -c pt -n '__fish_pt_using_command remove' -a '(__fish_pt_templates)' -d 'Template name'
353
+ complete -c pt -n '__fish_pt_using_command remove' -s y -l yes -d 'Automatically confirm removal'
354
+ complete -c pt -n '__fish_pt_using_command rm' -a '(__fish_pt_templates)' -d 'Template name'
355
+ complete -c pt -n '__fish_pt_using_command rm' -s y -l yes -d 'Automatically confirm removal'
356
+
357
+ # completion
358
+ complete -c pt -n '__fish_pt_using_command completion' -a 'bash zsh fish' -d 'Shell'
359
+ `;
360
+ }
361
+ export function generateShellScript(shell) {
362
+ switch (shell.toLowerCase()) {
363
+ case 'bash':
364
+ return generateBashCompletion();
365
+ case 'zsh':
366
+ return generateZshCompletion();
367
+ case 'fish':
368
+ return generateFishCompletion();
369
+ default: {
370
+ const supported = ['bash', 'zsh', 'fish'];
371
+ throw new Error(`Unsupported shell: ${shell}. Supported: ${supported.join(', ')}`);
372
+ }
373
+ }
374
+ }
375
+ export function generateCompletion(shell) {
376
+ return generateShellScript(shell);
377
+ }
378
+ export async function completionCommand(shellArg, options) {
379
+ if (options?.templates || shellArg === '--templates' || shellArg === '_templates') {
380
+ const templates = getTemplatesForCompletion();
381
+ if (templates.length > 0) {
382
+ console.log(templates.join('\n'));
383
+ }
384
+ return;
385
+ }
386
+ if (!shellArg) {
387
+ console.error('Error: Please specify a shell (bash, zsh, fish).');
388
+ process.exit(1);
389
+ }
390
+ try {
391
+ const script = generateCompletion(shellArg);
392
+ console.log(script);
393
+ }
394
+ catch (err) {
395
+ console.error(err.message || String(err));
396
+ process.exit(1);
397
+ }
398
+ }
399
+ // Allow direct execution via tsx src/commands/completionCommand.ts <shell>
400
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
401
+ const shell = process.argv[2];
402
+ if (shell === '--templates' || shell === '_templates') {
403
+ completionCommand(shell, { templates: true });
404
+ }
405
+ else {
406
+ completionCommand(shell);
407
+ }
408
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { addCommand } from './commands/addCommand.js';
12
12
  import { removeCommand } from './commands/removeCommand.js';
13
13
  import { defaultPostConfigCommand } from './commands/defaultPostConfigCommand.js';
14
14
  import { securityResponseCommand } from './commands/securityResponseCommand.js';
15
+ import { completionCommand } from './commands/completionCommand.js';
15
16
  import pkg from '../package.json' with { type: 'json' };
16
17
  const program = new Command();
17
18
  program
@@ -114,4 +115,11 @@ program
114
115
  .action(async (response) => {
115
116
  await securityResponseCommand(response);
116
117
  });
118
+ program
119
+ .command('completion [shell]')
120
+ .description('Generate shell completion script')
121
+ .option('--templates', 'Internal helper to list template names for completion')
122
+ .action(async (shellArg, options) => {
123
+ await completionCommand(shellArg, options);
124
+ });
117
125
  program.parse(process.argv);
package/doc/usage.md CHANGED
@@ -305,3 +305,41 @@ To see your entire configuration (including all templates) in JSON format:
305
305
  ```bash
306
306
  pt config --json
307
307
  ```
308
+
309
+ ## Shell Completions
310
+
311
+ `pt` can generate tab-completion scripts for Bash, Zsh, and Fish shells.
312
+
313
+ ### Installation
314
+
315
+ #### Bash
316
+
317
+ ```bash
318
+ pt completion bash > /etc/bash_completion.d/pt
319
+ # Or user-local:
320
+ pt completion bash > ~/.local/share/bash-completion/completions/pt
321
+ ```
322
+
323
+ #### Zsh
324
+
325
+ ```bash
326
+ pt completion zsh > ~/.zsh/completions/_pt
327
+ # Add to ~/.zshrc:
328
+ # fpath=(~/.zsh/completions $fpath)
329
+ # autoload -U compinit && compinit
330
+ ```
331
+
332
+ #### Fish
333
+
334
+ ```bash
335
+ pt completion fish > ~/.config/fish/completions/pt.fish
336
+ ```
337
+
338
+ ### Dynamic Completion
339
+
340
+ Template names auto-complete dynamically from your local `~/.pt/config.yaml` for:
341
+ - `pt init <TAB>`
342
+ - `pt update <TAB>`
343
+ - `pt config <TAB>`
344
+ - `pt remove <TAB>` (and `pt rm <TAB>`)
345
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garyr/pt-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Project Template CLI - Learn structures and initialize projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,6 +13,9 @@
13
13
  "dev": "tsx src/index.ts",
14
14
  "test": "node --import tsx --test tests/**/*.test.ts",
15
15
  "test:sequential": "node --import tsx --test tests/config.test.ts && node --import tsx --test tests/init.test.ts && node --import tsx --test tests/learn.test.ts && node --import tsx --test tests/substitute.test.ts && node --import tsx --test tests/config-utils.test.ts",
16
+ "completion:bash": "tsx src/commands/completionCommand.ts bash",
17
+ "completion:zsh": "tsx src/commands/completionCommand.ts zsh",
18
+ "completion:fish": "tsx src/commands/completionCommand.ts fish",
16
19
  "prepublishOnly": "npm run build",
17
20
  "build:linux": "bun build ./src/index.ts --compile --minify --target=bun-linux-x64 --outfile ../BUILD/pt-cli/pt-linux",
18
21
  "build:macos": "bun build ./src/index.ts --compile --minify --target=bun-darwin-x64 --outfile ../BUILD/pt-cli/pt-macos",
@@ -0,0 +1,415 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import YAML from 'yaml';
5
+ import { getConfigPath } from '../config.js';
6
+
7
+ export function getTemplatesForCompletion(): string[] {
8
+ try {
9
+ const configPath = getConfigPath();
10
+ if (!fs.existsSync(configPath)) {
11
+ return [];
12
+ }
13
+ const content = fs.readFileSync(configPath, 'utf-8');
14
+ if (!content.trim()) {
15
+ return [];
16
+ }
17
+ const parsed = YAML.parse(content);
18
+ if (!parsed || !parsed.templates || typeof parsed.templates !== 'object') {
19
+ return [];
20
+ }
21
+ return Object.keys(parsed.templates);
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ export function generateBashCompletion(): string {
28
+ return `# Bash completion for pt
29
+ _pt_completions() {
30
+ local cur prev words cword
31
+ if declare -F _init_completion >/dev/null 2>&1; then
32
+ _init_completion || return
33
+ else
34
+ cur="\${COMP_WORDS[COMP_CWORD]}"
35
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
36
+ words=("\${COMP_WORDS[@]}")
37
+ cword=$COMP_CWORD
38
+ fi
39
+
40
+ local commands="learn update init config ignore variables default-post-config add remove rm security-response completion"
41
+
42
+ # Complete top-level command or global flags
43
+ if [[ $cword -eq 1 ]]; then
44
+ if [[ "$cur" == -* ]]; then
45
+ COMPREPLY=( $(compgen -W "-v --version -h --help" -- "$cur") )
46
+ else
47
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
48
+ fi
49
+ return 0
50
+ fi
51
+
52
+ local cmd="\${words[1]}"
53
+
54
+ case "$cmd" in
55
+ learn)
56
+ if [[ "$cur" == -* ]]; then
57
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --name --desc --json --allow-untrusted -h --help" -- "$cur") )
58
+ fi
59
+ ;;
60
+ update)
61
+ if [[ "$cur" == -* ]]; then
62
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --desc --no-diff -h --help" -- "$cur") )
63
+ elif [[ $cword -eq 2 ]]; then
64
+ local templates
65
+ templates=$(pt completion --templates 2>/dev/null)
66
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
67
+ fi
68
+ ;;
69
+ init)
70
+ if [[ "$cur" == -* ]]; then
71
+ COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars -h --help" -- "$cur") )
72
+ elif [[ $cword -eq 2 ]]; then
73
+ local templates
74
+ templates=$(pt completion --templates 2>/dev/null)
75
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
76
+ fi
77
+ ;;
78
+ config)
79
+ if [[ "$cur" == -* ]]; then
80
+ COMPREPLY=( $(compgen -W "--json -h --help" -- "$cur") )
81
+ elif [[ $cword -eq 2 ]]; then
82
+ local templates
83
+ templates=$(pt completion --templates 2>/dev/null)
84
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
85
+ fi
86
+ ;;
87
+ ignore)
88
+ if [[ "$cur" == -* ]]; then
89
+ COMPREPLY=( $(compgen -W "--set -h --help" -- "$cur") )
90
+ fi
91
+ ;;
92
+ variables)
93
+ if [[ "$cur" == -* ]]; then
94
+ COMPREPLY=( $(compgen -W "--set --json --delete -h --help" -- "$cur") )
95
+ fi
96
+ ;;
97
+ default-post-config)
98
+ if [[ "$cur" == -* ]]; then
99
+ COMPREPLY=( $(compgen -W "--set --json -h --help" -- "$cur") )
100
+ fi
101
+ ;;
102
+ add)
103
+ if [[ "$cur" == -* ]]; then
104
+ COMPREPLY=( $(compgen -W "-f --file -h --help" -- "$cur") )
105
+ fi
106
+ ;;
107
+ remove|rm)
108
+ if [[ "$cur" == -* ]]; then
109
+ COMPREPLY=( $(compgen -W "-y --yes -h --help" -- "$cur") )
110
+ elif [[ $cword -eq 2 ]]; then
111
+ local templates
112
+ templates=$(pt completion --templates 2>/dev/null)
113
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
114
+ fi
115
+ ;;
116
+ completion)
117
+ if [[ "$cur" == -* ]]; then
118
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
119
+ elif [[ $cword -eq 2 ]]; then
120
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
121
+ fi
122
+ ;;
123
+ security-response)
124
+ if [[ "$cur" == -* ]]; then
125
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
126
+ fi
127
+ ;;
128
+ esac
129
+ }
130
+
131
+ complete -F _pt_completions pt
132
+ `;
133
+ }
134
+
135
+ export function generateZshCompletion(): string {
136
+ return `#compdef pt
137
+
138
+ _pt_templates() {
139
+ local -a templates
140
+ templates=(\${(f)"$(pt completion --templates 2>/dev/null)"})
141
+ if [[ \${#templates[@]} -gt 0 ]]; then
142
+ _describe -t templates 'template' templates
143
+ fi
144
+ }
145
+
146
+ _pt() {
147
+ local context state state_policy
148
+ typeset -A opt_args
149
+
150
+ _arguments -C \\
151
+ '(-v --version)'{-v,--version}'[output the version number]' \\
152
+ '(-h --help)'{-h,--help}'[display help for command]' \\
153
+ '1: :->command' \\
154
+ '*:: :->args'
155
+
156
+ case $state in
157
+ command)
158
+ local -a commands
159
+ commands=(
160
+ 'learn:Learn a project structure from an existing directory'
161
+ 'update:Update an existing template with new structure/files'
162
+ 'init:Initialize a new project from a learned template'
163
+ 'config:Show current config location and list templates, or export a specific template'
164
+ 'ignore:View or set global ignore patterns (comma-separated)'
165
+ 'variables:View or set global variables (comma-separated key=value)'
166
+ 'default-post-config:View or set default post-config tasks'
167
+ 'add:Import/add a template from a JSON string or file'
168
+ 'remove:Remove a learned template from the config'
169
+ 'rm:Remove a learned template from the config'
170
+ 'security-response:Handle security response from GUI'
171
+ 'completion:Generate shell completion script'
172
+ )
173
+ _describe -t commands 'pt command' commands
174
+ ;;
175
+ args)
176
+ case $words[1] in
177
+ learn)
178
+ _arguments \\
179
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
180
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
181
+ '--name=[Template name]:name:' \\
182
+ '--desc=[Template description]:description:' \\
183
+ '--json[Output template structure as JSON for sharing instead of saving]' \\
184
+ '--allow-untrusted[Bypass the trusted-source check for remote URLs]' \\
185
+ '(-h --help)'{-h,--help}'[display help for command]' \\
186
+ '1:path:_files -/'
187
+ ;;
188
+ update)
189
+ _arguments \\
190
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
191
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
192
+ '--desc=[Template description]:description:' \\
193
+ '--no-diff[Disable additive mode, show full list]' \\
194
+ '(-h --help)'{-h,--help}'[display help for command]' \\
195
+ '1:template:_pt_templates' \\
196
+ '2:sourcePath:_files -/'
197
+ ;;
198
+ init)
199
+ _arguments \\
200
+ '(-f --file)'{-f,--file}'[Initialize directly from a JSON template file]:file:_files' \\
201
+ '--skip-post-config[Skip running post-config tasks]' \\
202
+ '--dry-run[Show what would be created without making changes]' \\
203
+ '(-y --yes)'{-y,--yes}'[Automatically answer yes to prompts]' \\
204
+ '--vars=[Comma-separated key=value variables]:variables:' \\
205
+ '(-h --help)'{-h,--help}'[display help for command]' \\
206
+ '1:template:_pt_templates' \\
207
+ '2:destPath:_files -/'
208
+ ;;
209
+ config)
210
+ _arguments \\
211
+ '--json[Output config or specific template as JSON]' \\
212
+ '(-h --help)'{-h,--help}'[display help for command]' \\
213
+ '1:template:_pt_templates'
214
+ ;;
215
+ ignore)
216
+ _arguments \\
217
+ '--set[Set the ignore patterns to the provided value]' \\
218
+ '(-h --help)'{-h,--help}'[display help for command]' \\
219
+ '1:patterns:'
220
+ ;;
221
+ variables)
222
+ _arguments \\
223
+ '--set[Set the variables to the provided pairs]' \\
224
+ '--json=[Set variables via JSON string or file]:data:' \\
225
+ '--delete=[Delete a specific global variable]:key:' \\
226
+ '(-h --help)'{-h,--help}'[display help for command]' \\
227
+ '1:pairs:'
228
+ ;;
229
+ default-post-config)
230
+ _arguments \\
231
+ '--set[Set the default post-config tasks via JSON]' \\
232
+ '--json=[JSON string or file containing tasks array]:data:' \\
233
+ '(-h --help)'{-h,--help}'[display help for command]'
234
+ ;;
235
+ add)
236
+ _arguments \\
237
+ '(-f --file)'{-f,--file}'[Path to JSON file containing template data]:file:_files' \\
238
+ '(-h --help)'{-h,--help}'[display help for command]' \\
239
+ '1:name:' \\
240
+ '2:json:'
241
+ ;;
242
+ remove|rm)
243
+ _arguments \\
244
+ '(-y --yes)'{-y,--yes}'[Automatically confirm removal]' \\
245
+ '(-h --help)'{-h,--help}'[display help for command]' \\
246
+ '1:template:_pt_templates'
247
+ ;;
248
+ security-response)
249
+ _arguments \\
250
+ '(-h --help)'{-h,--help}'[display help for command]' \\
251
+ '1:response:'
252
+ ;;
253
+ completion)
254
+ _arguments \\
255
+ '(-h --help)'{-h,--help}'[display help for command]' \\
256
+ '1:shell:(bash zsh fish)'
257
+ ;;
258
+ esac
259
+ ;;
260
+ esac
261
+ }
262
+
263
+ if [[ "$(basename -- "$0")" != "_pt" ]]; then
264
+ compdef _pt pt 2>/dev/null || true
265
+ fi
266
+ `;
267
+ }
268
+
269
+ export function generateFishCompletion(): string {
270
+ return `# Fish completion for pt
271
+
272
+ function __fish_pt_needs_command
273
+ set -l cmd (commandline -opc)
274
+ if [ (count $cmd) -eq 1 ]
275
+ return 0
276
+ end
277
+ return 1
278
+ end
279
+
280
+ function __fish_pt_using_command
281
+ set -l cmd (commandline -opc)
282
+ if [ (count $cmd) -gt 1 ]
283
+ if [ "$argv[1]" = "$cmd[2]" ]
284
+ return 0
285
+ end
286
+ end
287
+ return 1
288
+ end
289
+
290
+ function __fish_pt_templates
291
+ pt completion --templates 2>/dev/null
292
+ end
293
+
294
+ # Global options
295
+ complete -c pt -n '__fish_pt_needs_command' -s v -l version -d 'output the version number'
296
+ complete -c pt -n '__fish_pt_needs_command' -s h -l help -d 'display help for command'
297
+
298
+ # Commands
299
+ complete -c pt -n '__fish_pt_needs_command' -a learn -d 'Learn a project structure from an existing directory'
300
+ complete -c pt -n '__fish_pt_needs_command' -a update -d 'Update an existing template with new structure/files'
301
+ complete -c pt -n '__fish_pt_needs_command' -a init -d 'Initialize a new project from a learned template'
302
+ complete -c pt -n '__fish_pt_needs_command' -a config -d 'Show current config location and list templates, or export a specific template'
303
+ complete -c pt -n '__fish_pt_needs_command' -a ignore -d 'View or set global ignore patterns (comma-separated)'
304
+ complete -c pt -n '__fish_pt_needs_command' -a variables -d 'View or set global variables (comma-separated key=value)'
305
+ complete -c pt -n '__fish_pt_needs_command' -a default-post-config -d 'View or set default post-config tasks'
306
+ complete -c pt -n '__fish_pt_needs_command' -a add -d 'Import/add a template from a JSON string or file'
307
+ complete -c pt -n '__fish_pt_needs_command' -a remove -d 'Remove a learned template from the config'
308
+ complete -c pt -n '__fish_pt_needs_command' -a rm -d 'Remove a learned template from the config'
309
+ complete -c pt -n '__fish_pt_needs_command' -a security-response -d 'Handle security response from GUI'
310
+ complete -c pt -n '__fish_pt_needs_command' -a completion -d 'Generate shell completion script'
311
+
312
+ # learn
313
+ complete -c pt -n '__fish_pt_using_command learn' -l ignore -d 'Folder patterns to ignore (comma-separated)'
314
+ complete -c pt -n '__fish_pt_using_command learn' -s y -l yes -d 'Automatically confirm prompts'
315
+ complete -c pt -n '__fish_pt_using_command learn' -l name -d 'Template name (skip prompt)'
316
+ complete -c pt -n '__fish_pt_using_command learn' -l desc -d 'Template description (skip prompt)'
317
+ complete -c pt -n '__fish_pt_using_command learn' -l json -d 'Output template structure as JSON for sharing instead of saving'
318
+ complete -c pt -n '__fish_pt_using_command learn' -l allow-untrusted -d 'Bypass the trusted-source check for remote URLs'
319
+
320
+ # update
321
+ complete -c pt -n '__fish_pt_using_command update' -a '(__fish_pt_templates)' -d 'Template name'
322
+ complete -c pt -n '__fish_pt_using_command update' -l ignore -d 'Folder patterns to ignore (comma-separated)'
323
+ complete -c pt -n '__fish_pt_using_command update' -s y -l yes -d 'Automatically confirm prompts'
324
+ complete -c pt -n '__fish_pt_using_command update' -l desc -d 'Template description (skip prompt)'
325
+ complete -c pt -n '__fish_pt_using_command update' -l no-diff -d 'Disable additive mode, show full list'
326
+
327
+ # init
328
+ complete -c pt -n '__fish_pt_using_command init' -a '(__fish_pt_templates)' -d 'Template name'
329
+ complete -c pt -n '__fish_pt_using_command init' -s f -l file -d 'Initialize directly from a JSON template file without adding it to local config'
330
+ complete -c pt -n '__fish_pt_using_command init' -l skip-post-config -d 'Skip running post-config tasks'
331
+ complete -c pt -n '__fish_pt_using_command init' -l dry-run -d 'Show what would be created without making changes'
332
+ complete -c pt -n '__fish_pt_using_command init' -s y -l yes -d 'Automatically answer yes to prompts'
333
+ complete -c pt -n '__fish_pt_using_command init' -l vars -d 'Comma-separated key=value variables'
334
+
335
+ # config
336
+ complete -c pt -n '__fish_pt_using_command config' -a '(__fish_pt_templates)' -d 'Template name'
337
+ complete -c pt -n '__fish_pt_using_command config' -l json -d 'Output config or specific template as JSON'
338
+
339
+ # ignore
340
+ complete -c pt -n '__fish_pt_using_command ignore' -l set -d 'Set the ignore patterns to the provided value'
341
+
342
+ # variables
343
+ complete -c pt -n '__fish_pt_using_command variables' -l set -d 'Set the variables to the provided pairs'
344
+ complete -c pt -n '__fish_pt_using_command variables' -l json -d 'Set variables via JSON string or file'
345
+ complete -c pt -n '__fish_pt_using_command variables' -l delete -d 'Delete a specific global variable'
346
+
347
+ # default-post-config
348
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l set -d 'Set the default post-config tasks via JSON'
349
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l json -d 'JSON string or file containing tasks array'
350
+
351
+ # add
352
+ complete -c pt -n '__fish_pt_using_command add' -s f -l file -d 'Path to JSON file containing template data'
353
+
354
+ # remove / rm
355
+ complete -c pt -n '__fish_pt_using_command remove' -a '(__fish_pt_templates)' -d 'Template name'
356
+ complete -c pt -n '__fish_pt_using_command remove' -s y -l yes -d 'Automatically confirm removal'
357
+ complete -c pt -n '__fish_pt_using_command rm' -a '(__fish_pt_templates)' -d 'Template name'
358
+ complete -c pt -n '__fish_pt_using_command rm' -s y -l yes -d 'Automatically confirm removal'
359
+
360
+ # completion
361
+ complete -c pt -n '__fish_pt_using_command completion' -a 'bash zsh fish' -d 'Shell'
362
+ `;
363
+ }
364
+
365
+ export function generateShellScript(shell: string): string {
366
+ switch (shell.toLowerCase()) {
367
+ case 'bash':
368
+ return generateBashCompletion();
369
+ case 'zsh':
370
+ return generateZshCompletion();
371
+ case 'fish':
372
+ return generateFishCompletion();
373
+ default: {
374
+ const supported = ['bash', 'zsh', 'fish'];
375
+ throw new Error(`Unsupported shell: ${shell}. Supported: ${supported.join(', ')}`);
376
+ }
377
+ }
378
+ }
379
+
380
+ export function generateCompletion(shell: string): string {
381
+ return generateShellScript(shell);
382
+ }
383
+
384
+ export async function completionCommand(shellArg?: string, options?: { templates?: boolean }): Promise<void> {
385
+ if (options?.templates || shellArg === '--templates' || shellArg === '_templates') {
386
+ const templates = getTemplatesForCompletion();
387
+ if (templates.length > 0) {
388
+ console.log(templates.join('\n'));
389
+ }
390
+ return;
391
+ }
392
+
393
+ if (!shellArg) {
394
+ console.error('Error: Please specify a shell (bash, zsh, fish).');
395
+ process.exit(1);
396
+ }
397
+
398
+ try {
399
+ const script = generateCompletion(shellArg);
400
+ console.log(script);
401
+ } catch (err: any) {
402
+ console.error(err.message || String(err));
403
+ process.exit(1);
404
+ }
405
+ }
406
+
407
+ // Allow direct execution via tsx src/commands/completionCommand.ts <shell>
408
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
409
+ const shell = process.argv[2];
410
+ if (shell === '--templates' || shell === '_templates') {
411
+ completionCommand(shell, { templates: true });
412
+ } else {
413
+ completionCommand(shell);
414
+ }
415
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { addCommand } from './commands/addCommand.js';
16
16
  import { removeCommand } from './commands/removeCommand.js';
17
17
  import { defaultPostConfigCommand } from './commands/defaultPostConfigCommand.js';
18
18
  import { securityResponseCommand } from './commands/securityResponseCommand.js';
19
+ import { completionCommand } from './commands/completionCommand.js';
19
20
 
20
21
  import pkg from '../package.json' with { type: 'json' };
21
22
 
@@ -129,4 +130,12 @@ program
129
130
  await securityResponseCommand(response);
130
131
  });
131
132
 
133
+ program
134
+ .command('completion [shell]')
135
+ .description('Generate shell completion script')
136
+ .option('--templates', 'Internal helper to list template names for completion')
137
+ .action(async (shellArg: string | undefined, options) => {
138
+ await completionCommand(shellArg, options);
139
+ });
140
+
132
141
  program.parse(process.argv);
@@ -0,0 +1,210 @@
1
+ import { test, describe, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { execSync } from 'child_process';
6
+ import YAML from 'yaml';
7
+
8
+ // Force a temporary home directory for testing before importing anything from the CLI
9
+ const testHome = path.join(process.cwd(), '.test-home-completion');
10
+ process.env.HOME = testHome;
11
+
12
+ import {
13
+ generateCompletion,
14
+ generateBashCompletion,
15
+ generateZshCompletion,
16
+ generateFishCompletion,
17
+ getTemplatesForCompletion,
18
+ completionCommand,
19
+ } from '../src/commands/completionCommand.js';
20
+ import { getConfigPath, ensureConfigDir } from '../src/config.js';
21
+
22
+ describe('Shell Completion Generation', () => {
23
+ beforeEach(() => {
24
+ ensureConfigDir();
25
+ });
26
+
27
+ afterEach(() => {
28
+ if (fs.existsSync(testHome)) {
29
+ fs.rmSync(testHome, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test('generateCompletion throws on unsupported shell', () => {
34
+ assert.throws(
35
+ () => generateCompletion('powershell'),
36
+ /Unsupported shell: powershell\. Supported: bash, zsh, fish/
37
+ );
38
+ assert.throws(
39
+ () => generateCompletion('elvish'),
40
+ /Unsupported shell: elvish\. Supported: bash, zsh, fish/
41
+ );
42
+ });
43
+
44
+ test('generateBashCompletion generates valid bash script with commands and flags', () => {
45
+ const script = generateBashCompletion();
46
+ assert.ok(script.includes('_pt_completions()'), 'Must define _pt_completions');
47
+ assert.ok(script.includes('complete -F _pt_completions pt'), 'Must register complete -F');
48
+ assert.ok(script.includes('pt completion --templates'), 'Must include template completion call');
49
+
50
+ const expectedCommands = [
51
+ 'learn',
52
+ 'update',
53
+ 'init',
54
+ 'config',
55
+ 'ignore',
56
+ 'variables',
57
+ 'default-post-config',
58
+ 'add',
59
+ 'remove',
60
+ 'rm',
61
+ 'security-response',
62
+ 'completion',
63
+ ];
64
+
65
+ for (const cmd of expectedCommands) {
66
+ assert.ok(script.includes(cmd), `Bash completion must include command ${cmd}`);
67
+ }
68
+
69
+ // Verify bash script parses cleanly and registers completion
70
+ try {
71
+ const tempScriptPath = path.join(testHome, 'pt-completion.bash');
72
+ fs.writeFileSync(tempScriptPath, script, 'utf-8');
73
+ const verifyOutput = execSync(`bash -c "source '${tempScriptPath}' && complete -p pt"`, { encoding: 'utf-8' });
74
+ assert.ok(verifyOutput.includes('_pt_completions pt'), 'complete -p pt should return registered function');
75
+ } catch (e: any) {
76
+ if (e.status !== undefined) throw e;
77
+ }
78
+ });
79
+
80
+ test('generateZshCompletion generates valid zsh script with commands and flags', () => {
81
+ const script = generateZshCompletion();
82
+ assert.ok(script.includes('#compdef pt'), 'Must include #compdef pt header');
83
+ assert.ok(script.includes('_pt()'), 'Must define _pt function');
84
+ assert.ok(script.includes('_pt_templates()'), 'Must define _pt_templates function');
85
+ assert.ok(script.includes('pt completion --templates'), 'Must call pt completion --templates');
86
+
87
+ const expectedCommands = [
88
+ 'learn',
89
+ 'update',
90
+ 'init',
91
+ 'config',
92
+ 'ignore',
93
+ 'variables',
94
+ 'default-post-config',
95
+ 'add',
96
+ 'remove',
97
+ 'rm',
98
+ 'security-response',
99
+ 'completion',
100
+ ];
101
+
102
+ for (const cmd of expectedCommands) {
103
+ assert.ok(script.includes(cmd), `Zsh completion must include command ${cmd}`);
104
+ }
105
+ });
106
+
107
+ test('generateFishCompletion generates valid fish script with commands and flags', () => {
108
+ const script = generateFishCompletion();
109
+ assert.ok(script.includes('complete -c pt'), 'Must include complete -c pt');
110
+ assert.ok(script.includes('__fish_pt_templates'), 'Must define __fish_pt_templates function');
111
+ assert.ok(script.includes('pt completion --templates'), 'Must call pt completion --templates');
112
+
113
+ const expectedCommands = [
114
+ 'learn',
115
+ 'update',
116
+ 'init',
117
+ 'config',
118
+ 'ignore',
119
+ 'variables',
120
+ 'default-post-config',
121
+ 'add',
122
+ 'remove',
123
+ 'rm',
124
+ 'security-response',
125
+ 'completion',
126
+ ];
127
+
128
+ for (const cmd of expectedCommands) {
129
+ assert.ok(script.includes(cmd), `Fish completion must include command ${cmd}`);
130
+ }
131
+ });
132
+
133
+ test('getTemplatesForCompletion returns template names when config exists', () => {
134
+ ensureConfigDir();
135
+ const configPath = getConfigPath();
136
+ const testConfig = {
137
+ version: '3.0',
138
+ templates: {
139
+ 'web-app': { description: 'Web Application', folders: [] },
140
+ 'python-cli': { description: 'Python CLI', folders: [] },
141
+ 'godot-game': { description: 'Godot Game', folders: [] },
142
+ },
143
+ };
144
+ fs.writeFileSync(configPath, YAML.stringify(testConfig), 'utf-8');
145
+
146
+ const templates = getTemplatesForCompletion();
147
+ assert.deepStrictEqual(templates, ['web-app', 'python-cli', 'godot-game']);
148
+ });
149
+
150
+ test('getTemplatesForCompletion returns empty array when config does not exist', () => {
151
+ const configPath = getConfigPath();
152
+ if (fs.existsSync(configPath)) {
153
+ fs.unlinkSync(configPath);
154
+ }
155
+ const templates = getTemplatesForCompletion();
156
+ assert.deepStrictEqual(templates, []);
157
+ });
158
+
159
+ test('getTemplatesForCompletion returns empty array when config is empty or invalid', () => {
160
+ ensureConfigDir();
161
+ const configPath = getConfigPath();
162
+ fs.writeFileSync(configPath, '', 'utf-8');
163
+ assert.deepStrictEqual(getTemplatesForCompletion(), []);
164
+
165
+ fs.writeFileSync(configPath, ':::invalid yaml:::', 'utf-8');
166
+ assert.deepStrictEqual(getTemplatesForCompletion(), []);
167
+
168
+ fs.writeFileSync(configPath, YAML.stringify({ version: '3.0' }), 'utf-8');
169
+ assert.deepStrictEqual(getTemplatesForCompletion(), []);
170
+ });
171
+
172
+ test('completionCommand with --templates option outputs newline-separated template names', async () => {
173
+ ensureConfigDir();
174
+ const configPath = getConfigPath();
175
+ const testConfig = {
176
+ version: '3.0',
177
+ templates: {
178
+ 'alpha-template': { description: 'Alpha', folders: [] },
179
+ 'beta-template': { description: 'Beta', folders: [] },
180
+ },
181
+ };
182
+ fs.writeFileSync(configPath, YAML.stringify(testConfig), 'utf-8');
183
+
184
+ const logged: string[] = [];
185
+ const origLog = console.log;
186
+ console.log = (msg: any) => logged.push(String(msg));
187
+
188
+ try {
189
+ await completionCommand(undefined, { templates: true });
190
+ assert.strictEqual(logged.length, 1);
191
+ assert.strictEqual(logged[0], 'alpha-template\nbeta-template');
192
+ } finally {
193
+ console.log = origLog;
194
+ }
195
+ });
196
+
197
+ test('completionCommand with valid shell outputs script', async () => {
198
+ const logged: string[] = [];
199
+ const origLog = console.log;
200
+ console.log = (msg: any) => logged.push(String(msg));
201
+
202
+ try {
203
+ await completionCommand('bash');
204
+ assert.strictEqual(logged.length, 1);
205
+ assert.ok(logged[0].includes('_pt_completions()'));
206
+ } finally {
207
+ console.log = origLog;
208
+ }
209
+ });
210
+ });