@garyr/pt-cli 1.0.1 → 1.1.1

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.
@@ -4,7 +4,7 @@ import inquirer from 'inquirer';
4
4
  import { loadConfig, saveConfig, getTemplateNames, shouldExclude, shouldIgnore, shouldExcludeFile, getDefaultPostConfig } from '../config.js';
5
5
  import chalk from 'chalk';
6
6
  import { downloadAndExtract } from '../remote.js';
7
- import { extractStructure, findVariablesInFiles, isExecutable, parseInfoFile, loadJsonTemplateConfig, parsePostConfigScript, mergePostConfigTasks, mergePostCopyFiles, promptAdditionalVariables, promptNewVariables, promptGlobalVariables, printNoNewVariables, promptNewFolders, printNoNewFolders, printNewFiles, promptNewFiles, promptRootFiles, promptStructureFolders, promptCopyFolders, promptPostConfigTasks } from './template-utils.js';
7
+ import { extractStructure, findVariablesInFiles, isExecutable, parseInfoFile, loadJsonTemplateConfig, parsePostConfigScript, mergePostConfigTasks, mergePostCopyFiles, promptNewVariables, promptGlobalVariables, printNoNewVariables, promptNewFolders, printNoNewFolders, printNewFiles, promptNewFiles, promptRootFiles, promptStructureFolders, promptCopyFolders, promptPostConfigTasks } from './template-utils.js';
8
8
  export async function update(sourcePath, templateName, options = {}) {
9
9
  const isFullMode = options.noDiff;
10
10
  let resolvedPath;
@@ -125,11 +125,13 @@ export async function update(sourcePath, templateName, options = {}) {
125
125
  if (!isFullMode) {
126
126
  // Additive mode: only present new variables for selection
127
127
  const existingVarNames = new Set(config.templates[templateName].variables?.map(v => v.name) || []);
128
+ // newVars are NOT added to variables yet - only existing variables + JSON variables are in variables
128
129
  const newVars = variables.filter(v => !existingVarNames.has(v.name));
129
130
  if (newVars.length > 0) {
130
131
  console.log(chalk.cyan(`\nšŸ“Š New Variables:`));
131
132
  console.log(chalk.green(` + ${newVars.length} new variable(s): ${newVars.map(v => v.name).join(', ')}`));
132
133
  const selectedNewVars = await promptNewVariables(newVars, options);
134
+ // Only add the selected new variables (not all newVars)
133
135
  variables.push(...selectedNewVars);
134
136
  }
135
137
  else {
@@ -150,24 +152,34 @@ export async function update(sourcePath, templateName, options = {}) {
150
152
  }
151
153
  }
152
154
  else {
153
- // Full mode: original behavior with optional default/global variables prompt
154
- const globalVarsToPrompt = [];
155
- if (config.variables && Array.isArray(config.variables)) {
156
- for (const v of config.variables) {
157
- if (!variables.some(existing => existing.name === v.name)) {
158
- globalVarsToPrompt.push({ ...v });
155
+ // Full mode: replace all variables with detected ones (no additive)
156
+ variables = [];
157
+ // Add detected variables
158
+ for (const varName of detectedVars) {
159
+ variables.push({
160
+ name: varName,
161
+ prompt: `Enter ${varName}:`,
162
+ required: true
163
+ });
164
+ }
165
+ // Also include JSON variables
166
+ if (fileTemplateConfig.variables && Array.isArray(fileTemplateConfig.variables)) {
167
+ for (const v of fileTemplateConfig.variables) {
168
+ const existingIndex = variables.findIndex(existing => existing.name === v.name);
169
+ if (existingIndex !== -1) {
170
+ variables[existingIndex] = { ...variables[existingIndex], ...v };
171
+ }
172
+ else {
173
+ variables.push({ ...v });
159
174
  }
160
175
  }
161
176
  }
162
- if (globalVarsToPrompt.length > 0) {
163
- const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
164
- variables.push(...selectedGlobals);
165
- }
166
- const additionalVars = await promptAdditionalVariables(variables, options);
167
- variables.push(...additionalVars);
168
177
  }
169
178
  // 1. Structure (skeleton) - Additive mode
170
179
  let folders = [];
180
+ let selectedStructure = [];
181
+ let selectedFiles = [];
182
+ let selectedFolders = [];
171
183
  if (!isFullMode) {
172
184
  // Additive mode: only add new folders
173
185
  const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
@@ -185,6 +197,14 @@ export async function update(sourcePath, templateName, options = {}) {
185
197
  printNoNewFolders();
186
198
  folders = existingFolders;
187
199
  }
200
+ // selectedStructure should include ALL folders (existing + added) in additive mode
201
+ // so that the final filter doesn't drop user-selected new folders
202
+ selectedStructure = [
203
+ ...new Set([
204
+ ...existingFolders.map(f => f.name),
205
+ ...folders.filter(f => !existingFolders.some(ef => ef.name === f.name)).map(f => f.name),
206
+ ]),
207
+ ];
188
208
  }
189
209
  else {
190
210
  // Full mode: original behavior
@@ -196,11 +216,10 @@ export async function update(sourcePath, templateName, options = {}) {
196
216
  const rootEntries = fs.readdirSync(resolvedPath, { withFileTypes: true })
197
217
  .filter(e => !shouldExclude(resolvedPath, path.join(resolvedPath, e.name), ignorePatterns))
198
218
  .filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
199
- const rootFiles = rootEntries.filter(e => e.isFile()).map(e => e.name);
219
+ const rootFiles = rootEntries.filter(e => e.isFile())
220
+ .map(e => e.name)
221
+ .filter(fileName => !shouldExcludeFile(fileName));
200
222
  const rootDirs = rootEntries.filter(e => e.isDirectory()).map(e => e.name);
201
- let selectedFiles = [];
202
- let selectedFolders = [];
203
- let selectedStructure = [];
204
223
  if (!isFullMode) {
205
224
  // Additive mode for files and folders
206
225
  const existingCopyFiles = config.templates[templateName].copy_files || [];
@@ -209,8 +228,6 @@ export async function update(sourcePath, templateName, options = {}) {
209
228
  printNewFiles(newFiles.length, newFiles);
210
229
  const addedFiles = await promptNewFiles(newFiles, options);
211
230
  selectedFiles = [...existingCopyFiles.filter(cf => !rootDirs.includes(cf.src)).map(cf => cf.src), ...addedFiles];
212
- // Structure
213
- selectedStructure = config.templates[templateName].folders?.map(f => f.name) || [];
214
231
  // Seed selectedFolders from existing copy_files directory entries
215
232
  selectedFolders = existingCopyFiles
216
233
  .filter(f => rootDirs.includes(f.src))
@@ -228,9 +245,9 @@ export async function update(sourcePath, templateName, options = {}) {
228
245
  }
229
246
  }
230
247
  else {
231
- // Full mode: original behavior
248
+ // Full mode: original behavior - pick up all files like a fresh learn
232
249
  if (options.yes || options.json) {
233
- selectedFiles = rootFiles.filter(f => ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase()));
250
+ selectedFiles = rootFiles;
234
251
  selectedStructure = rootDirs;
235
252
  selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
236
253
  }
@@ -308,8 +325,24 @@ export async function update(sourcePath, templateName, options = {}) {
308
325
  const detectedExecutables = rootFiles
309
326
  .filter(file => isExecutable(path.join(resolvedPath, file), file))
310
327
  .filter(file => !shouldExcludeFile(file));
328
+ // In additive mode, only add executables that were explicitly selected by the user
329
+ let selectedExecutables = [];
330
+ if (!isFullMode) {
331
+ // In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
332
+ // BUT exclude files that already exist in copy_files with custom settings (chmod, substitute_variables: false)
333
+ const existingCopyFiles = config.templates[templateName].copy_files || [];
334
+ const existingCopySrcs = new Set(existingCopyFiles.map(cf => cf.src));
335
+ const existingCustomSettings = new Set(existingCopyFiles
336
+ .filter(cf => cf.chmod || cf.substitute_variables === false)
337
+ .map(cf => cf.src));
338
+ selectedExecutables = detectedExecutables.filter(exec => selectedFiles.includes(exec) && !existingCustomSettings.has(exec));
339
+ }
340
+ else {
341
+ // In full mode, include all detected executables (original behavior)
342
+ selectedExecutables = detectedExecutables;
343
+ }
311
344
  const existingPostCopy = config.templates[templateName].post_copy || [];
312
- const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, detectedExecutables);
345
+ const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
313
346
  if (post_copy.length > 0) {
314
347
  templateConfig.post_copy = post_copy;
315
348
  const postCopySrcs = post_copy.map(f => f.src);
@@ -320,9 +353,7 @@ export async function update(sourcePath, templateName, options = {}) {
320
353
  name: templateName,
321
354
  ...templateConfig
322
355
  };
323
- process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
324
- process.exit(0);
325
- });
356
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
326
357
  return;
327
358
  }
328
359
  config.templates[templateName] = templateConfig;
package/dist/config.js CHANGED
@@ -223,6 +223,7 @@ export const DEFAULT_EXCLUDES = [
223
223
  'dist',
224
224
  'build',
225
225
  'bin',
226
+ '.vscode',
226
227
  '.DS_Store',
227
228
  'Thumbs.db',
228
229
  ];
@@ -327,6 +328,8 @@ export function shouldExcludeFile(fileName) {
327
328
  'yarn.lock',
328
329
  'pnpm-lock.yaml',
329
330
  'composer.lock',
331
+ 'post_config.sh',
332
+ 'post_config.bat',
330
333
  ];
331
334
  for (const pattern of excludePatterns) {
332
335
  if (pattern.startsWith('*')) {
package/dist/index.js CHANGED
@@ -12,12 +12,13 @@ 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
18
19
  .name('pt')
19
20
  .description('Project Template CLI - Learn project structures and initialize new ones')
20
- .version(pkg.version, '-v', 'output the version number');
21
+ .version(pkg.version, '-v, --version', 'output the version number');
21
22
  program
22
23
  .command('learn [path]')
23
24
  .description('Learn a project structure from an existing directory')
@@ -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.1",
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/index.ts completion bash",
17
+ "completion:zsh": "tsx src/index.ts completion zsh",
18
+ "completion:fish": "tsx src/index.ts completion 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,404 @@
1
+ import fs from 'fs';
2
+ import YAML from 'yaml';
3
+ import { getConfigPath } from '../config.js';
4
+
5
+ export function getTemplatesForCompletion(): string[] {
6
+ try {
7
+ const configPath = getConfigPath();
8
+ if (!fs.existsSync(configPath)) {
9
+ return [];
10
+ }
11
+ const content = fs.readFileSync(configPath, 'utf-8');
12
+ if (!content.trim()) {
13
+ return [];
14
+ }
15
+ const parsed = YAML.parse(content);
16
+ if (!parsed || !parsed.templates || typeof parsed.templates !== 'object') {
17
+ return [];
18
+ }
19
+ return Object.keys(parsed.templates);
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ export function generateBashCompletion(): string {
26
+ return `# Bash completion for pt
27
+ _pt_completions() {
28
+ local cur prev words cword
29
+ if declare -F _init_completion >/dev/null 2>&1; then
30
+ _init_completion || return
31
+ else
32
+ cur="\${COMP_WORDS[COMP_CWORD]}"
33
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
34
+ words=("\${COMP_WORDS[@]}")
35
+ cword=$COMP_CWORD
36
+ fi
37
+
38
+ local commands="learn update init config ignore variables default-post-config add remove rm security-response completion"
39
+
40
+ # Complete top-level command or global flags
41
+ if [[ $cword -eq 1 ]]; then
42
+ if [[ "$cur" == -* ]]; then
43
+ COMPREPLY=( $(compgen -W "-v --version -h --help" -- "$cur") )
44
+ else
45
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
46
+ fi
47
+ return 0
48
+ fi
49
+
50
+ local cmd="\${words[1]}"
51
+
52
+ case "$cmd" in
53
+ learn)
54
+ if [[ "$cur" == -* ]]; then
55
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --name --desc --json --allow-untrusted -h --help" -- "$cur") )
56
+ fi
57
+ ;;
58
+ update)
59
+ if [[ "$cur" == -* ]]; then
60
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --desc --no-diff -h --help" -- "$cur") )
61
+ elif [[ $cword -eq 2 ]]; then
62
+ local templates
63
+ templates=$(pt completion --templates 2>/dev/null)
64
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
65
+ fi
66
+ ;;
67
+ init)
68
+ if [[ "$cur" == -* ]]; then
69
+ COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars -h --help" -- "$cur") )
70
+ elif [[ $cword -eq 2 ]]; then
71
+ local templates
72
+ templates=$(pt completion --templates 2>/dev/null)
73
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
74
+ fi
75
+ ;;
76
+ config)
77
+ if [[ "$cur" == -* ]]; then
78
+ COMPREPLY=( $(compgen -W "--json -h --help" -- "$cur") )
79
+ elif [[ $cword -eq 2 ]]; then
80
+ local templates
81
+ templates=$(pt completion --templates 2>/dev/null)
82
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
83
+ fi
84
+ ;;
85
+ ignore)
86
+ if [[ "$cur" == -* ]]; then
87
+ COMPREPLY=( $(compgen -W "--set -h --help" -- "$cur") )
88
+ fi
89
+ ;;
90
+ variables)
91
+ if [[ "$cur" == -* ]]; then
92
+ COMPREPLY=( $(compgen -W "--set --json --delete -h --help" -- "$cur") )
93
+ fi
94
+ ;;
95
+ default-post-config)
96
+ if [[ "$cur" == -* ]]; then
97
+ COMPREPLY=( $(compgen -W "--set --json -h --help" -- "$cur") )
98
+ fi
99
+ ;;
100
+ add)
101
+ if [[ "$cur" == -* ]]; then
102
+ COMPREPLY=( $(compgen -W "-f --file -h --help" -- "$cur") )
103
+ fi
104
+ ;;
105
+ remove|rm)
106
+ if [[ "$cur" == -* ]]; then
107
+ COMPREPLY=( $(compgen -W "-y --yes -h --help" -- "$cur") )
108
+ elif [[ $cword -eq 2 ]]; then
109
+ local templates
110
+ templates=$(pt completion --templates 2>/dev/null)
111
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
112
+ fi
113
+ ;;
114
+ completion)
115
+ if [[ "$cur" == -* ]]; then
116
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
117
+ elif [[ $cword -eq 2 ]]; then
118
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
119
+ fi
120
+ ;;
121
+ security-response)
122
+ if [[ "$cur" == -* ]]; then
123
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
124
+ fi
125
+ ;;
126
+ esac
127
+ }
128
+
129
+ complete -F _pt_completions pt
130
+ `;
131
+ }
132
+
133
+ export function generateZshCompletion(): string {
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
+
267
+ export function generateFishCompletion(): string {
268
+ return `# Fish completion for pt
269
+
270
+ function __fish_pt_needs_command
271
+ set -l cmd (commandline -opc)
272
+ if [ (count $cmd) -eq 1 ]
273
+ return 0
274
+ end
275
+ return 1
276
+ end
277
+
278
+ function __fish_pt_using_command
279
+ set -l cmd (commandline -opc)
280
+ if [ (count $cmd) -gt 1 ]
281
+ if [ "$argv[1]" = "$cmd[2]" ]
282
+ return 0
283
+ end
284
+ end
285
+ return 1
286
+ end
287
+
288
+ function __fish_pt_templates
289
+ pt completion --templates 2>/dev/null
290
+ end
291
+
292
+ # Global options
293
+ complete -c pt -n '__fish_pt_needs_command' -s v -l version -d 'output the version number'
294
+ complete -c pt -n '__fish_pt_needs_command' -s h -l help -d 'display help for command'
295
+
296
+ # Commands
297
+ complete -c pt -n '__fish_pt_needs_command' -a learn -d 'Learn a project structure from an existing directory'
298
+ complete -c pt -n '__fish_pt_needs_command' -a update -d 'Update an existing template with new structure/files'
299
+ complete -c pt -n '__fish_pt_needs_command' -a init -d 'Initialize a new project from a learned template'
300
+ complete -c pt -n '__fish_pt_needs_command' -a config -d 'Show current config location and list templates, or export a specific template'
301
+ complete -c pt -n '__fish_pt_needs_command' -a ignore -d 'View or set global ignore patterns (comma-separated)'
302
+ complete -c pt -n '__fish_pt_needs_command' -a variables -d 'View or set global variables (comma-separated key=value)'
303
+ complete -c pt -n '__fish_pt_needs_command' -a default-post-config -d 'View or set default post-config tasks'
304
+ complete -c pt -n '__fish_pt_needs_command' -a add -d 'Import/add a template from a JSON string or file'
305
+ complete -c pt -n '__fish_pt_needs_command' -a remove -d 'Remove a learned template from the config'
306
+ complete -c pt -n '__fish_pt_needs_command' -a rm -d 'Remove a learned template from the config'
307
+ complete -c pt -n '__fish_pt_needs_command' -a security-response -d 'Handle security response from GUI'
308
+ complete -c pt -n '__fish_pt_needs_command' -a completion -d 'Generate shell completion script'
309
+
310
+ # learn
311
+ complete -c pt -n '__fish_pt_using_command learn' -l ignore -d 'Folder patterns to ignore (comma-separated)'
312
+ complete -c pt -n '__fish_pt_using_command learn' -s y -l yes -d 'Automatically confirm prompts'
313
+ complete -c pt -n '__fish_pt_using_command learn' -l name -d 'Template name (skip prompt)'
314
+ complete -c pt -n '__fish_pt_using_command learn' -l desc -d 'Template description (skip prompt)'
315
+ complete -c pt -n '__fish_pt_using_command learn' -l json -d 'Output template structure as JSON for sharing instead of saving'
316
+ complete -c pt -n '__fish_pt_using_command learn' -l allow-untrusted -d 'Bypass the trusted-source check for remote URLs'
317
+
318
+ # update
319
+ complete -c pt -n '__fish_pt_using_command update' -a '(__fish_pt_templates)' -d 'Template name'
320
+ complete -c pt -n '__fish_pt_using_command update' -l ignore -d 'Folder patterns to ignore (comma-separated)'
321
+ complete -c pt -n '__fish_pt_using_command update' -s y -l yes -d 'Automatically confirm prompts'
322
+ complete -c pt -n '__fish_pt_using_command update' -l desc -d 'Template description (skip prompt)'
323
+ complete -c pt -n '__fish_pt_using_command update' -l no-diff -d 'Disable additive mode, show full list'
324
+
325
+ # init
326
+ complete -c pt -n '__fish_pt_using_command init' -a '(__fish_pt_templates)' -d 'Template name'
327
+ 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'
328
+ complete -c pt -n '__fish_pt_using_command init' -l skip-post-config -d 'Skip running post-config tasks'
329
+ complete -c pt -n '__fish_pt_using_command init' -l dry-run -d 'Show what would be created without making changes'
330
+ complete -c pt -n '__fish_pt_using_command init' -s y -l yes -d 'Automatically answer yes to prompts'
331
+ complete -c pt -n '__fish_pt_using_command init' -l vars -d 'Comma-separated key=value variables'
332
+
333
+ # config
334
+ complete -c pt -n '__fish_pt_using_command config' -a '(__fish_pt_templates)' -d 'Template name'
335
+ complete -c pt -n '__fish_pt_using_command config' -l json -d 'Output config or specific template as JSON'
336
+
337
+ # ignore
338
+ complete -c pt -n '__fish_pt_using_command ignore' -l set -d 'Set the ignore patterns to the provided value'
339
+
340
+ # variables
341
+ complete -c pt -n '__fish_pt_using_command variables' -l set -d 'Set the variables to the provided pairs'
342
+ complete -c pt -n '__fish_pt_using_command variables' -l json -d 'Set variables via JSON string or file'
343
+ complete -c pt -n '__fish_pt_using_command variables' -l delete -d 'Delete a specific global variable'
344
+
345
+ # default-post-config
346
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l set -d 'Set the default post-config tasks via JSON'
347
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l json -d 'JSON string or file containing tasks array'
348
+
349
+ # add
350
+ complete -c pt -n '__fish_pt_using_command add' -s f -l file -d 'Path to JSON file containing template data'
351
+
352
+ # remove / rm
353
+ complete -c pt -n '__fish_pt_using_command remove' -a '(__fish_pt_templates)' -d 'Template name'
354
+ complete -c pt -n '__fish_pt_using_command remove' -s y -l yes -d 'Automatically confirm removal'
355
+ complete -c pt -n '__fish_pt_using_command rm' -a '(__fish_pt_templates)' -d 'Template name'
356
+ complete -c pt -n '__fish_pt_using_command rm' -s y -l yes -d 'Automatically confirm removal'
357
+
358
+ # completion
359
+ complete -c pt -n '__fish_pt_using_command completion' -a 'bash zsh fish' -d 'Shell'
360
+ `;
361
+ }
362
+
363
+ export function generateShellScript(shell: string): string {
364
+ switch (shell.toLowerCase()) {
365
+ case 'bash':
366
+ return generateBashCompletion();
367
+ case 'zsh':
368
+ return generateZshCompletion();
369
+ case 'fish':
370
+ return generateFishCompletion();
371
+ default: {
372
+ const supported = ['bash', 'zsh', 'fish'];
373
+ throw new Error(`Unsupported shell: ${shell}. Supported: ${supported.join(', ')}`);
374
+ }
375
+ }
376
+ }
377
+
378
+ export function generateCompletion(shell: string): string {
379
+ return generateShellScript(shell);
380
+ }
381
+
382
+ export async function completionCommand(shellArg?: string, options?: { templates?: boolean }): Promise<void> {
383
+ if (options?.templates || shellArg === '--templates' || shellArg === '_templates') {
384
+ const templates = getTemplatesForCompletion();
385
+ if (templates.length > 0) {
386
+ console.log(templates.join('\n'));
387
+ }
388
+ return;
389
+ }
390
+
391
+ if (!shellArg) {
392
+ console.error('Error: Please specify a shell (bash, zsh, fish).');
393
+ process.exit(1);
394
+ }
395
+
396
+ try {
397
+ const script = generateCompletion(shellArg);
398
+ console.log(script);
399
+ } catch (err: any) {
400
+ console.error(err.message || String(err));
401
+ process.exit(1);
402
+ }
403
+ }
404
+