@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.
package/README.md CHANGED
@@ -32,20 +32,27 @@ graph LR
32
32
  <!-- TOC -->
33
33
 
34
34
  - [pt - Project Template CLI](#pt---project-template-cli)
35
- - [Why pt-cli?](#why-pt-cli)
36
- - [Core Benefits & Uses](#core-benefits--uses)
37
- - [🚀 Low-Friction Templating](#-low-friction-templating)
38
- - [🧠 Reduces Cognitive Load](#-reduces-cognitive-load)
39
- - [📦 Sharing is Caring](#-sharing-is-caring)
40
- - [🤖 Agentic and API Friendly](#-agentic-and-api-friendly)
41
- - [Features at a Glance](#features-at-a-glance)
42
- - [Quick Start](#quick-start)
43
- - [Installation](#installation)
44
- - [Basic Commands](#basic-commands)
45
- - [Agent Integration](#agent-integration)
46
- - [Documentation](#documentation)
47
- - [Development](#development)
48
- - [Where are the Templates?](#where-are-the-templates)
35
+ - [Why pt-cli?](#why-pt-cli)
36
+ - [Core Benefits \& Uses](#core-benefits--uses)
37
+ - [🚀 Low-Friction Templating](#-low-friction-templating)
38
+ - [🧠 Reduces Cognitive Load](#-reduces-cognitive-load)
39
+ - [📦 Sharing is Caring](#-sharing-is-caring)
40
+ - [🤖 Agentic and API Friendly](#-agentic-and-api-friendly)
41
+ - [Features at a Glance](#features-at-a-glance)
42
+ - [Quick Start](#quick-start)
43
+ - [Installation](#installation)
44
+ - [Basic Commands](#basic-commands)
45
+ - [Shell Completions](#shell-completions)
46
+ - [Agent Integration](#agent-integration)
47
+ - [Documentation](#documentation)
48
+ - [Development](#development)
49
+ - [Where are the Templates?](#where-are-the-templates)
50
+ - [1.0 Release \& API Stability](#10-release--api-stability)
51
+ - [🔒 Stability Guarantee (1.x series)](#-stability-guarantee-1x-series)
52
+ - [📦 Versioning Policy](#-versioning-policy)
53
+ - [📋 What's Locked in 1.0](#-whats-locked-in-10)
54
+ - [📖 Migration from 0.x to 1.0](#-migration-from-0x-to-10)
55
+ - [Documentation](#documentation-1)
49
56
 
50
57
  <!-- /TOC -->
51
58
 
@@ -124,6 +131,25 @@ pt init ./new-project --file my-template.json --yes
124
131
 
125
132
  ```
126
133
 
134
+ ### Shell Completions
135
+
136
+ Generate and install tab completions:
137
+
138
+ ```bash
139
+ # Bash
140
+ pt completion bash > /etc/bash_completion.d/pt
141
+ # Or user-local: pt completion bash > ~/.local/share/bash-completion/completions/pt
142
+
143
+ # Zsh
144
+ pt completion zsh > ~/.zsh/completions/_pt
145
+ # Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)
146
+
147
+ # Fish
148
+ pt completion fish > ~/.config/fish/completions/pt.fish
149
+ ```
150
+
151
+ Template names auto-complete dynamically for `pt init`, `pt update`, `pt config`, and `pt remove`.
152
+
127
153
  ## Agent Integration
128
154
 
129
155
  `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 +217,7 @@ pt variables [--set] [--delete] [--json]
191
217
  pt default-post-config [--set --json]
192
218
  pt ignore [patterns] [--set]
193
219
  pt security-response <response>
220
+ pt completion <shell>
194
221
  ```
195
222
 
196
223
  **Config Schema (`~/.pt/config.yaml` v3.0):**
@@ -0,0 +1,396 @@
1
+ import fs from 'fs';
2
+ import YAML from 'yaml';
3
+ import { getConfigPath } from '../config.js';
4
+ export function getTemplatesForCompletion() {
5
+ try {
6
+ const configPath = getConfigPath();
7
+ if (!fs.existsSync(configPath)) {
8
+ return [];
9
+ }
10
+ const content = fs.readFileSync(configPath, 'utf-8');
11
+ if (!content.trim()) {
12
+ return [];
13
+ }
14
+ const parsed = YAML.parse(content);
15
+ if (!parsed || !parsed.templates || typeof parsed.templates !== 'object') {
16
+ return [];
17
+ }
18
+ return Object.keys(parsed.templates);
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ export function generateBashCompletion() {
25
+ return `# Bash completion for pt
26
+ _pt_completions() {
27
+ local cur prev words cword
28
+ if declare -F _init_completion >/dev/null 2>&1; then
29
+ _init_completion || return
30
+ else
31
+ cur="\${COMP_WORDS[COMP_CWORD]}"
32
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
33
+ words=("\${COMP_WORDS[@]}")
34
+ cword=$COMP_CWORD
35
+ fi
36
+
37
+ local commands="learn update init config ignore variables default-post-config add remove rm security-response completion"
38
+
39
+ # Complete top-level command or global flags
40
+ if [[ $cword -eq 1 ]]; then
41
+ if [[ "$cur" == -* ]]; then
42
+ COMPREPLY=( $(compgen -W "-v --version -h --help" -- "$cur") )
43
+ else
44
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
45
+ fi
46
+ return 0
47
+ fi
48
+
49
+ local cmd="\${words[1]}"
50
+
51
+ case "$cmd" in
52
+ learn)
53
+ if [[ "$cur" == -* ]]; then
54
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --name --desc --json --allow-untrusted -h --help" -- "$cur") )
55
+ fi
56
+ ;;
57
+ update)
58
+ if [[ "$cur" == -* ]]; then
59
+ COMPREPLY=( $(compgen -W "--ignore -y --yes --desc --no-diff -h --help" -- "$cur") )
60
+ elif [[ $cword -eq 2 ]]; then
61
+ local templates
62
+ templates=$(pt completion --templates 2>/dev/null)
63
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
64
+ fi
65
+ ;;
66
+ init)
67
+ if [[ "$cur" == -* ]]; then
68
+ COMPREPLY=( $(compgen -W "-f --file --skip-post-config --dry-run -y --yes --vars -h --help" -- "$cur") )
69
+ elif [[ $cword -eq 2 ]]; then
70
+ local templates
71
+ templates=$(pt completion --templates 2>/dev/null)
72
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
73
+ fi
74
+ ;;
75
+ config)
76
+ if [[ "$cur" == -* ]]; then
77
+ COMPREPLY=( $(compgen -W "--json -h --help" -- "$cur") )
78
+ elif [[ $cword -eq 2 ]]; then
79
+ local templates
80
+ templates=$(pt completion --templates 2>/dev/null)
81
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
82
+ fi
83
+ ;;
84
+ ignore)
85
+ if [[ "$cur" == -* ]]; then
86
+ COMPREPLY=( $(compgen -W "--set -h --help" -- "$cur") )
87
+ fi
88
+ ;;
89
+ variables)
90
+ if [[ "$cur" == -* ]]; then
91
+ COMPREPLY=( $(compgen -W "--set --json --delete -h --help" -- "$cur") )
92
+ fi
93
+ ;;
94
+ default-post-config)
95
+ if [[ "$cur" == -* ]]; then
96
+ COMPREPLY=( $(compgen -W "--set --json -h --help" -- "$cur") )
97
+ fi
98
+ ;;
99
+ add)
100
+ if [[ "$cur" == -* ]]; then
101
+ COMPREPLY=( $(compgen -W "-f --file -h --help" -- "$cur") )
102
+ fi
103
+ ;;
104
+ remove|rm)
105
+ if [[ "$cur" == -* ]]; then
106
+ COMPREPLY=( $(compgen -W "-y --yes -h --help" -- "$cur") )
107
+ elif [[ $cword -eq 2 ]]; then
108
+ local templates
109
+ templates=$(pt completion --templates 2>/dev/null)
110
+ COMPREPLY=( $(compgen -W "$templates" -- "$cur") )
111
+ fi
112
+ ;;
113
+ completion)
114
+ if [[ "$cur" == -* ]]; then
115
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
116
+ elif [[ $cword -eq 2 ]]; then
117
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
118
+ fi
119
+ ;;
120
+ security-response)
121
+ if [[ "$cur" == -* ]]; then
122
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
123
+ fi
124
+ ;;
125
+ esac
126
+ }
127
+
128
+ complete -F _pt_completions pt
129
+ `;
130
+ }
131
+ export function generateZshCompletion() {
132
+ return `#compdef pt
133
+
134
+ _pt_templates() {
135
+ local -a templates
136
+ templates=(\${(f)"$(pt completion --templates 2>/dev/null)"})
137
+ if [[ \${#templates[@]} -gt 0 ]]; then
138
+ _describe -t templates 'template' templates
139
+ fi
140
+ }
141
+
142
+ _pt() {
143
+ local context state state_policy
144
+ typeset -A opt_args
145
+
146
+ _arguments -C \\
147
+ '(-v --version)'{-v,--version}'[output the version number]' \\
148
+ '(-h --help)'{-h,--help}'[display help for command]' \\
149
+ '1: :->command' \\
150
+ '*:: :->args'
151
+
152
+ case $state in
153
+ command)
154
+ local -a commands
155
+ commands=(
156
+ 'learn:Learn a project structure from an existing directory'
157
+ 'update:Update an existing template with new structure/files'
158
+ 'init:Initialize a new project from a learned template'
159
+ 'config:Show current config location and list templates, or export a specific template'
160
+ 'ignore:View or set global ignore patterns (comma-separated)'
161
+ 'variables:View or set global variables (comma-separated key=value)'
162
+ 'default-post-config:View or set default post-config tasks'
163
+ 'add:Import/add a template from a JSON string or file'
164
+ 'remove:Remove a learned template from the config'
165
+ 'rm:Remove a learned template from the config'
166
+ 'security-response:Handle security response from GUI'
167
+ 'completion:Generate shell completion script'
168
+ )
169
+ _describe -t commands 'pt command' commands
170
+ ;;
171
+ args)
172
+ case $words[1] in
173
+ learn)
174
+ _arguments \\
175
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
176
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
177
+ '--name=[Template name]:name:' \\
178
+ '--desc=[Template description]:description:' \\
179
+ '--json[Output template structure as JSON for sharing instead of saving]' \\
180
+ '--allow-untrusted[Bypass the trusted-source check for remote URLs]' \\
181
+ '(-h --help)'{-h,--help}'[display help for command]' \\
182
+ '1:path:_files -/'
183
+ ;;
184
+ update)
185
+ _arguments \\
186
+ '--ignore=[Folder patterns to ignore]:patterns:' \\
187
+ '(-y --yes)'{-y,--yes}'[Automatically confirm prompts]' \\
188
+ '--desc=[Template description]:description:' \\
189
+ '--no-diff[Disable additive mode, show full list]' \\
190
+ '(-h --help)'{-h,--help}'[display help for command]' \\
191
+ '1:template:_pt_templates' \\
192
+ '2:sourcePath:_files -/'
193
+ ;;
194
+ init)
195
+ _arguments \\
196
+ '(-f --file)'{-f,--file}'[Initialize directly from a JSON template file]:file:_files' \\
197
+ '--skip-post-config[Skip running post-config tasks]' \\
198
+ '--dry-run[Show what would be created without making changes]' \\
199
+ '(-y --yes)'{-y,--yes}'[Automatically answer yes to prompts]' \\
200
+ '--vars=[Comma-separated key=value variables]:variables:' \\
201
+ '(-h --help)'{-h,--help}'[display help for command]' \\
202
+ '1:template:_pt_templates' \\
203
+ '2:destPath:_files -/'
204
+ ;;
205
+ config)
206
+ _arguments \\
207
+ '--json[Output config or specific template as JSON]' \\
208
+ '(-h --help)'{-h,--help}'[display help for command]' \\
209
+ '1:template:_pt_templates'
210
+ ;;
211
+ ignore)
212
+ _arguments \\
213
+ '--set[Set the ignore patterns to the provided value]' \\
214
+ '(-h --help)'{-h,--help}'[display help for command]' \\
215
+ '1:patterns:'
216
+ ;;
217
+ variables)
218
+ _arguments \\
219
+ '--set[Set the variables to the provided pairs]' \\
220
+ '--json=[Set variables via JSON string or file]:data:' \\
221
+ '--delete=[Delete a specific global variable]:key:' \\
222
+ '(-h --help)'{-h,--help}'[display help for command]' \\
223
+ '1:pairs:'
224
+ ;;
225
+ default-post-config)
226
+ _arguments \\
227
+ '--set[Set the default post-config tasks via JSON]' \\
228
+ '--json=[JSON string or file containing tasks array]:data:' \\
229
+ '(-h --help)'{-h,--help}'[display help for command]'
230
+ ;;
231
+ add)
232
+ _arguments \\
233
+ '(-f --file)'{-f,--file}'[Path to JSON file containing template data]:file:_files' \\
234
+ '(-h --help)'{-h,--help}'[display help for command]' \\
235
+ '1:name:' \\
236
+ '2:json:'
237
+ ;;
238
+ remove|rm)
239
+ _arguments \\
240
+ '(-y --yes)'{-y,--yes}'[Automatically confirm removal]' \\
241
+ '(-h --help)'{-h,--help}'[display help for command]' \\
242
+ '1:template:_pt_templates'
243
+ ;;
244
+ security-response)
245
+ _arguments \\
246
+ '(-h --help)'{-h,--help}'[display help for command]' \\
247
+ '1:response:'
248
+ ;;
249
+ completion)
250
+ _arguments \\
251
+ '(-h --help)'{-h,--help}'[display help for command]' \\
252
+ '1:shell:(bash zsh fish)'
253
+ ;;
254
+ esac
255
+ ;;
256
+ esac
257
+ }
258
+
259
+ if [[ "$(basename -- "$0")" != "_pt" ]]; then
260
+ compdef _pt pt 2>/dev/null || true
261
+ fi
262
+ `;
263
+ }
264
+ export function generateFishCompletion() {
265
+ return `# Fish completion for pt
266
+
267
+ function __fish_pt_needs_command
268
+ set -l cmd (commandline -opc)
269
+ if [ (count $cmd) -eq 1 ]
270
+ return 0
271
+ end
272
+ return 1
273
+ end
274
+
275
+ function __fish_pt_using_command
276
+ set -l cmd (commandline -opc)
277
+ if [ (count $cmd) -gt 1 ]
278
+ if [ "$argv[1]" = "$cmd[2]" ]
279
+ return 0
280
+ end
281
+ end
282
+ return 1
283
+ end
284
+
285
+ function __fish_pt_templates
286
+ pt completion --templates 2>/dev/null
287
+ end
288
+
289
+ # Global options
290
+ complete -c pt -n '__fish_pt_needs_command' -s v -l version -d 'output the version number'
291
+ complete -c pt -n '__fish_pt_needs_command' -s h -l help -d 'display help for command'
292
+
293
+ # Commands
294
+ complete -c pt -n '__fish_pt_needs_command' -a learn -d 'Learn a project structure from an existing directory'
295
+ complete -c pt -n '__fish_pt_needs_command' -a update -d 'Update an existing template with new structure/files'
296
+ complete -c pt -n '__fish_pt_needs_command' -a init -d 'Initialize a new project from a learned template'
297
+ complete -c pt -n '__fish_pt_needs_command' -a config -d 'Show current config location and list templates, or export a specific template'
298
+ complete -c pt -n '__fish_pt_needs_command' -a ignore -d 'View or set global ignore patterns (comma-separated)'
299
+ complete -c pt -n '__fish_pt_needs_command' -a variables -d 'View or set global variables (comma-separated key=value)'
300
+ complete -c pt -n '__fish_pt_needs_command' -a default-post-config -d 'View or set default post-config tasks'
301
+ complete -c pt -n '__fish_pt_needs_command' -a add -d 'Import/add a template from a JSON string or file'
302
+ complete -c pt -n '__fish_pt_needs_command' -a remove -d 'Remove a learned template from the config'
303
+ complete -c pt -n '__fish_pt_needs_command' -a rm -d 'Remove a learned template from the config'
304
+ complete -c pt -n '__fish_pt_needs_command' -a security-response -d 'Handle security response from GUI'
305
+ complete -c pt -n '__fish_pt_needs_command' -a completion -d 'Generate shell completion script'
306
+
307
+ # learn
308
+ complete -c pt -n '__fish_pt_using_command learn' -l ignore -d 'Folder patterns to ignore (comma-separated)'
309
+ complete -c pt -n '__fish_pt_using_command learn' -s y -l yes -d 'Automatically confirm prompts'
310
+ complete -c pt -n '__fish_pt_using_command learn' -l name -d 'Template name (skip prompt)'
311
+ complete -c pt -n '__fish_pt_using_command learn' -l desc -d 'Template description (skip prompt)'
312
+ complete -c pt -n '__fish_pt_using_command learn' -l json -d 'Output template structure as JSON for sharing instead of saving'
313
+ complete -c pt -n '__fish_pt_using_command learn' -l allow-untrusted -d 'Bypass the trusted-source check for remote URLs'
314
+
315
+ # update
316
+ complete -c pt -n '__fish_pt_using_command update' -a '(__fish_pt_templates)' -d 'Template name'
317
+ complete -c pt -n '__fish_pt_using_command update' -l ignore -d 'Folder patterns to ignore (comma-separated)'
318
+ complete -c pt -n '__fish_pt_using_command update' -s y -l yes -d 'Automatically confirm prompts'
319
+ complete -c pt -n '__fish_pt_using_command update' -l desc -d 'Template description (skip prompt)'
320
+ complete -c pt -n '__fish_pt_using_command update' -l no-diff -d 'Disable additive mode, show full list'
321
+
322
+ # init
323
+ complete -c pt -n '__fish_pt_using_command init' -a '(__fish_pt_templates)' -d 'Template name'
324
+ 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'
325
+ complete -c pt -n '__fish_pt_using_command init' -l skip-post-config -d 'Skip running post-config tasks'
326
+ complete -c pt -n '__fish_pt_using_command init' -l dry-run -d 'Show what would be created without making changes'
327
+ complete -c pt -n '__fish_pt_using_command init' -s y -l yes -d 'Automatically answer yes to prompts'
328
+ complete -c pt -n '__fish_pt_using_command init' -l vars -d 'Comma-separated key=value variables'
329
+
330
+ # config
331
+ complete -c pt -n '__fish_pt_using_command config' -a '(__fish_pt_templates)' -d 'Template name'
332
+ complete -c pt -n '__fish_pt_using_command config' -l json -d 'Output config or specific template as JSON'
333
+
334
+ # ignore
335
+ complete -c pt -n '__fish_pt_using_command ignore' -l set -d 'Set the ignore patterns to the provided value'
336
+
337
+ # variables
338
+ complete -c pt -n '__fish_pt_using_command variables' -l set -d 'Set the variables to the provided pairs'
339
+ complete -c pt -n '__fish_pt_using_command variables' -l json -d 'Set variables via JSON string or file'
340
+ complete -c pt -n '__fish_pt_using_command variables' -l delete -d 'Delete a specific global variable'
341
+
342
+ # default-post-config
343
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l set -d 'Set the default post-config tasks via JSON'
344
+ complete -c pt -n '__fish_pt_using_command default-post-config' -l json -d 'JSON string or file containing tasks array'
345
+
346
+ # add
347
+ complete -c pt -n '__fish_pt_using_command add' -s f -l file -d 'Path to JSON file containing template data'
348
+
349
+ # remove / rm
350
+ complete -c pt -n '__fish_pt_using_command remove' -a '(__fish_pt_templates)' -d 'Template name'
351
+ complete -c pt -n '__fish_pt_using_command remove' -s y -l yes -d 'Automatically confirm removal'
352
+ complete -c pt -n '__fish_pt_using_command rm' -a '(__fish_pt_templates)' -d 'Template name'
353
+ complete -c pt -n '__fish_pt_using_command rm' -s y -l yes -d 'Automatically confirm removal'
354
+
355
+ # completion
356
+ complete -c pt -n '__fish_pt_using_command completion' -a 'bash zsh fish' -d 'Shell'
357
+ `;
358
+ }
359
+ export function generateShellScript(shell) {
360
+ switch (shell.toLowerCase()) {
361
+ case 'bash':
362
+ return generateBashCompletion();
363
+ case 'zsh':
364
+ return generateZshCompletion();
365
+ case 'fish':
366
+ return generateFishCompletion();
367
+ default: {
368
+ const supported = ['bash', 'zsh', 'fish'];
369
+ throw new Error(`Unsupported shell: ${shell}. Supported: ${supported.join(', ')}`);
370
+ }
371
+ }
372
+ }
373
+ export function generateCompletion(shell) {
374
+ return generateShellScript(shell);
375
+ }
376
+ export async function completionCommand(shellArg, options) {
377
+ if (options?.templates || shellArg === '--templates' || shellArg === '_templates') {
378
+ const templates = getTemplatesForCompletion();
379
+ if (templates.length > 0) {
380
+ console.log(templates.join('\n'));
381
+ }
382
+ return;
383
+ }
384
+ if (!shellArg) {
385
+ console.error('Error: Please specify a shell (bash, zsh, fish).');
386
+ process.exit(1);
387
+ }
388
+ try {
389
+ const script = generateCompletion(shellArg);
390
+ console.log(script);
391
+ }
392
+ catch (err) {
393
+ console.error(err.message || String(err));
394
+ process.exit(1);
395
+ }
396
+ }
@@ -238,13 +238,19 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
238
238
  printNoNewFolders();
239
239
  folders = existingTemplate.folders;
240
240
  }
241
+ // selectedStructure should include ALL folders (existing + added) in additive mode
242
+ // so that the final filter doesn't drop user-selected new folders
243
+ selectedStructure = [
244
+ ...new Set([
245
+ ...existingTemplate.folders?.map((f) => f.name) || [],
246
+ ...folders.filter(f => !existingTemplate.folders?.some(ef => ef.name === f.name)).map(f => f.name),
247
+ ]),
248
+ ];
241
249
  // New files
242
250
  const newFiles = rootFiles.filter((f) => !existingTemplate.copy_files?.some((cf) => cf.src === f));
243
251
  printNewFiles(newFiles.length, newFiles);
244
252
  const addedFiles = await promptNewFiles(newFiles, options);
245
253
  selectedFiles = [...(existingTemplate.copy_files?.filter((cf) => !rootDirs.includes(cf.src)).map((cf) => cf.src) || []), ...addedFiles];
246
- // Structure
247
- selectedStructure = existingTemplate.folders?.map((f) => f.name) || [];
248
254
  // Seed selectedFolders from existing copy_files directory entries
249
255
  selectedFolders = (existingTemplate.copy_files || [])
250
256
  .filter((f) => rootDirs.includes(f.src))
@@ -266,7 +272,14 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
266
272
  }
267
273
  // --- COPY FILES ---
268
274
  const existingCopyFiles = isUpdate ? config.templates[updateTemplate].copy_files || [] : [];
269
- const copy_files = buildCopyFiles(selectedFiles, selectedFolders, existingCopyFiles);
275
+ // If JSON template config has copy_files, use those directly (for new templates)
276
+ let copy_files;
277
+ if (!isUpdate && fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
278
+ copy_files = [...fileTemplateConfig.copy_files];
279
+ }
280
+ else {
281
+ copy_files = buildCopyFiles(selectedFiles, selectedFolders, existingCopyFiles);
282
+ }
270
283
  const templateConfig = {
271
284
  description: description,
272
285
  templateRoot: resolvedPath,
@@ -308,8 +321,18 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
308
321
  const detectedExecutables = rootFiles
309
322
  .filter(file => isExecutable(path.join(resolvedPath, file), file))
310
323
  .filter(file => !shouldExcludeFile(file));
324
+ // In additive mode (isUpdate), only add executables that were explicitly selected by the user
325
+ let selectedExecutables = [];
326
+ if (isUpdate) {
327
+ // In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
328
+ selectedExecutables = detectedExecutables.filter(exec => selectedFiles.includes(exec));
329
+ }
330
+ else {
331
+ // In new template mode, include all detected executables (original behavior)
332
+ selectedExecutables = detectedExecutables;
333
+ }
311
334
  const existingPostCopy = isUpdate ? config.templates[updateTemplate].post_copy || [] : [];
312
- const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, detectedExecutables);
335
+ const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
313
336
  if (post_copy.length > 0) {
314
337
  templateConfig.post_copy = post_copy;
315
338
  const postCopySrcs = post_copy.map(f => f.src);
@@ -318,9 +341,7 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
318
341
  // --- OUTPUT ---
319
342
  if (options.json) {
320
343
  const output = { name: targetName, ...templateConfig };
321
- process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
322
- process.exit(0);
323
- });
344
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
324
345
  return;
325
346
  }
326
347
  config.templates[targetName] = templateConfig;
@@ -153,7 +153,9 @@ export function getRootEntries(dirPath, ignorePatterns) {
153
153
  const entries = fs.readdirSync(dirPath, { withFileTypes: true })
154
154
  .filter(e => !shouldExclude(dirPath, path.join(dirPath, e.name), ignorePatterns))
155
155
  .filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
156
- const files = entries.filter(e => e.isFile()).map(e => e.name);
156
+ const files = entries.filter(e => e.isFile())
157
+ .map(e => e.name)
158
+ .filter(fileName => !shouldExcludeFile(fileName));
157
159
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
158
160
  return { files, dirs };
159
161
  }
@@ -204,12 +206,21 @@ export function mergePostConfigTasks(existingTasks, jsonTasks, detectedTasks) {
204
206
  if (jsonTasks && Array.isArray(jsonTasks)) {
205
207
  return [...jsonTasks];
206
208
  }
207
- return detectedTasks.length > 0 ? detectedTasks : existingTasks;
209
+ // Merge detected tasks with existing, avoiding duplicates
210
+ const merged = [...existingTasks];
211
+ for (const dt of detectedTasks) {
212
+ const exists = merged.some(et => et.command === dt.command && et.script === dt.script);
213
+ if (!exists) {
214
+ merged.push(dt);
215
+ }
216
+ }
217
+ return merged;
208
218
  }
209
219
  /**
210
220
  * Merge post_copy files from existing, JSON file, and detected executables
221
+ * Only adds executables that were explicitly selected by the user
211
222
  */
212
- export function mergePostCopyFiles(existingPostCopy, jsonPostCopy, detectedExecutables) {
223
+ export function mergePostCopyFiles(existingPostCopy, jsonPostCopy, selectedExecutables) {
213
224
  let post_copy = [...existingPostCopy];
214
225
  if (jsonPostCopy && Array.isArray(jsonPostCopy)) {
215
226
  for (const pc of jsonPostCopy) {
@@ -218,10 +229,9 @@ export function mergePostCopyFiles(existingPostCopy, jsonPostCopy, detectedExecu
218
229
  }
219
230
  }
220
231
  }
221
- const newExecutables = detectedExecutables.filter(file => !post_copy.some(existing => existing.src === file));
222
- if (newExecutables.length > 0) {
223
- // In interactive mode, we'd prompt to add these - for now just auto-add
224
- for (const file of newExecutables) {
232
+ // Only add executables that were explicitly selected by the user
233
+ for (const file of selectedExecutables) {
234
+ if (!post_copy.some(existing => existing.src === file)) {
225
235
  post_copy.push({ src: file, dest: file });
226
236
  }
227
237
  }