@garyr/pt-cli 0.40.0 → 0.41.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.
@@ -1,64 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import inquirer from 'inquirer';
4
- import { loadConfig, saveConfig, shouldExclude, shouldIgnore, shouldExcludeFile, getDefaultPostConfig } from '../config.js';
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
- function getNewFolders(storedStructure, targetPath, ignorePatterns) {
8
- const added = [];
9
- const getStructureMap = (nodes) => {
10
- const map = new Map();
11
- for (const node of nodes) {
12
- map.set(node.name, node);
13
- }
14
- return map;
15
- };
16
- const storedMap = getStructureMap(storedStructure);
17
- const targetStructure = extractStructure(targetPath, targetPath, ignorePatterns);
18
- const targetMap = getStructureMap(targetStructure);
19
- // Find only new folders
20
- for (const [name, targetNode] of targetMap) {
21
- if (!storedMap.has(name)) {
22
- added.push(targetNode);
23
- }
24
- }
25
- return { added };
26
- }
27
- function getNewFiles(storedTemplate, targetPath) {
28
- const newFiles = [];
29
- // Get current files in target
30
- const currentFiles = fs.readdirSync(targetPath, { withFileTypes: true })
31
- .filter(e => e.isFile())
32
- .map(e => e.name);
33
- // Get files from stored template
34
- const storedFiles = storedTemplate.copy_files?.map(f => f.src) || [];
35
- // Find only new files
36
- for (const file of currentFiles) {
37
- if (!storedFiles.includes(file)) {
38
- newFiles.push(file);
39
- }
40
- }
41
- return { newFiles };
42
- }
43
- function getNewVariables(storedVariables, targetPath) {
44
- const newVariables = [];
45
- // Get variables from target directory
46
- const targetVars = findVariablesInFiles(targetPath, targetPath);
47
- const storedVarMap = new Set(storedVariables.map(v => v.name));
48
- // Find only new variables
49
- for (const varName of targetVars) {
50
- if (!storedVarMap.has(varName)) {
51
- newVariables.push({
52
- name: varName,
53
- prompt: `Enter ${varName}:`,
54
- required: true
55
- });
56
- }
57
- }
58
- return { newVariables };
59
- }
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';
60
8
  export async function update(sourcePath, templateName, options = {}) {
61
- // Determine if additive mode is disabled
62
9
  const isFullMode = options.noDiff;
63
10
  let resolvedPath;
64
11
  // Phase 1: Remote Check
@@ -83,46 +30,27 @@ export async function update(sourcePath, templateName, options = {}) {
83
30
  process.exit(1);
84
31
  }
85
32
  const config = loadConfig();
33
+ const existingNames = getTemplateNames(config);
86
34
  if (!config.templates[templateName]) {
87
35
  console.error(chalk.red(`Template "${templateName}" not found.`));
88
36
  process.exit(1);
89
37
  }
90
- // Check for template configuration JSON file (.pt-template.json or template.json)
91
- let fileTemplateConfig = {};
92
- const jsonConfigPaths = [
93
- path.join(resolvedPath, '.pt-template.json'),
94
- path.join(resolvedPath, 'template.json')
95
- ];
96
- for (const jPath of jsonConfigPaths) {
97
- if (fs.existsSync(jPath)) {
98
- try {
99
- const fileContent = fs.readFileSync(jPath, 'utf-8');
100
- fileTemplateConfig = JSON.parse(fileContent);
101
- if (!options.json)
102
- console.log(chalk.cyan(`Auto-detected template configurations from ${path.basename(jPath)}`));
38
+ // Check for template configuration JSON file
39
+ const fileTemplateConfig = loadJsonTemplateConfig(resolvedPath);
40
+ if (fileTemplateConfig && !options.json) {
41
+ const jsonConfigPaths = [
42
+ path.join(resolvedPath, '.pt-template.json'),
43
+ path.join(resolvedPath, 'template.json')
44
+ ];
45
+ for (const jPath of jsonConfigPaths) {
46
+ if (fs.existsSync(jPath)) {
47
+ console.log(chalk.cyan(`Auto-detected template configurations from ${path.basename(jPath)}`));
103
48
  break;
104
49
  }
105
- catch (e) {
106
- console.warn(chalk.yellow(`Warning: Failed to parse ${path.basename(jPath)}: ${e.message}`));
107
- }
108
50
  }
109
51
  }
110
52
  // Check for .info.md
111
- let infoName = '';
112
- let infoDesc = '';
113
- const infoPath = path.join(resolvedPath, '.info.md');
114
- if (fs.existsSync(infoPath)) {
115
- const infoContent = fs.readFileSync(infoPath, 'utf-8');
116
- const lines = infoContent.split('\n');
117
- for (const line of lines) {
118
- if (line.startsWith('# ')) {
119
- infoName = line.substring(2).trim();
120
- }
121
- else if (line.trim() !== '' && !infoDesc && !line.startsWith('#')) {
122
- infoDesc = line.trim();
123
- }
124
- }
125
- }
53
+ const { name: infoName, description: infoDesc } = parseInfoFile(path.join(resolvedPath, '.info.md'));
126
54
  let description = config.templates[templateName].description || '';
127
55
  let templateRoot = config.templates[templateName].templateRoot || resolvedPath;
128
56
  if (!options.yes && !options.json) {
@@ -176,11 +104,9 @@ export async function update(sourcePath, templateName, options = {}) {
176
104
  for (const v of fileTemplateConfig.variables) {
177
105
  const existingIndex = variables.findIndex(existing => existing.name === v.name);
178
106
  if (existingIndex !== -1) {
179
- // Update existing variable with JSON values (but preserve other fields)
180
107
  variables[existingIndex] = { ...variables[existingIndex], ...v };
181
108
  }
182
109
  else {
183
- // Add new variable
184
110
  variables.push({ ...v });
185
111
  }
186
112
  }
@@ -198,49 +124,29 @@ export async function update(sourcePath, templateName, options = {}) {
198
124
  // Variable handling - additive mode
199
125
  if (!isFullMode) {
200
126
  // Additive mode: only present new variables for selection
201
- const variableDiff = getNewVariables(config.templates[templateName].variables || [], resolvedPath);
127
+ const existingVarNames = new Set(config.templates[templateName].variables?.map(v => v.name) || []);
128
+ const newVars = variables.filter(v => !existingVarNames.has(v.name));
129
+ if (newVars.length > 0) {
130
+ console.log(chalk.cyan(`\n📊 New Variables:`));
131
+ console.log(chalk.green(` + ${newVars.length} new variable(s): ${newVars.map(v => v.name).join(', ')}`));
132
+ const selectedNewVars = await promptNewVariables(newVars, options);
133
+ variables.push(...selectedNewVars);
134
+ }
135
+ else {
136
+ printNoNewVariables();
137
+ }
202
138
  // Also include default/global variables that are not already in the template
203
139
  const globalVarsToPrompt = [];
204
140
  if (config.variables && Array.isArray(config.variables)) {
205
141
  for (const v of config.variables) {
206
- if (!variables.some(existing => existing.name === v.name) &&
207
- !variableDiff.newVariables.some(existing => existing.name === v.name)) {
142
+ if (!variables.some(existing => existing.name === v.name)) {
208
143
  globalVarsToPrompt.push({ ...v });
209
144
  }
210
145
  }
211
146
  }
212
- const combinedNewVars = [...variableDiff.newVariables, ...globalVarsToPrompt];
213
- if (combinedNewVars.length > 0) {
214
- console.log(chalk.cyan(`\n📊 New Variables:`));
215
- console.log(chalk.green(` + ${combinedNewVars.length} new variable(s): ${combinedNewVars.map(v => v.name).join(', ')}`));
216
- // Show interactive checkbox for new variables
217
- if (!options.yes && !options.json) {
218
- const { selectedVars } = await inquirer.prompt({
219
- type: 'checkbox',
220
- name: 'selectedVars',
221
- message: 'Select variables to include (space to toggle):',
222
- loop: false,
223
- theme: {
224
- icon: {
225
- checked: chalk.green('[x] '),
226
- unchecked: '[ ] ',
227
- }
228
- },
229
- choices: combinedNewVars.map(v => ({
230
- name: v.name,
231
- checked: true // Auto-select by default
232
- }))
233
- });
234
- // Only add selected variables
235
- variables.push(...combinedNewVars.filter(v => selectedVars.includes(v.name)));
236
- }
237
- else {
238
- // In --yes or --json mode, auto-add all
239
- variables.push(...combinedNewVars);
240
- }
241
- }
242
- else {
243
- console.log(chalk.cyan("No new variables detected"));
147
+ if (globalVarsToPrompt.length > 0) {
148
+ const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
149
+ variables.push(...selectedGlobals);
244
150
  }
245
151
  }
246
152
  else {
@@ -254,96 +160,30 @@ export async function update(sourcePath, templateName, options = {}) {
254
160
  }
255
161
  }
256
162
  if (globalVarsToPrompt.length > 0) {
257
- if (!options.yes && !options.json) {
258
- const { selectedGlobals } = await inquirer.prompt({
259
- type: 'checkbox',
260
- name: 'selectedGlobals',
261
- message: 'Select default/global variables to include (space to toggle):',
262
- loop: false,
263
- theme: {
264
- icon: {
265
- checked: chalk.green('[x] '),
266
- unchecked: '[ ] ',
267
- }
268
- },
269
- choices: globalVarsToPrompt.map(v => ({ name: v.name, checked: true }))
270
- });
271
- variables.push(...globalVarsToPrompt.filter(v => selectedGlobals.includes(v.name)));
272
- }
273
- else {
274
- variables.push(...globalVarsToPrompt);
275
- }
276
- }
277
- let hasMoreVariables = false;
278
- if (!options.yes && !options.json) {
279
- const message = variables.length > 0
280
- ? `Detected/Existing variables: ${variables.map(v => v.name).join(', ')}. Define more?`
281
- : 'Define template variables (e.g., client_name, project_type)?';
282
- const response = await inquirer.prompt({
283
- type: 'confirm',
284
- name: 'hasMoreVariables',
285
- message: message,
286
- default: false
287
- });
288
- hasMoreVariables = response.hasMoreVariables;
289
- }
290
- if (hasMoreVariables) {
291
- const { variableDefs } = await inquirer.prompt({
292
- type: 'input',
293
- name: 'variableDefs',
294
- message: 'Define additional variables as comma-separated names:',
295
- });
296
- if (variableDefs) {
297
- const additionalVars = variableDefs.split(',').map((v) => v.trim()).filter(Boolean);
298
- for (const v of additionalVars) {
299
- if (!variables.some(existing => existing.name === v)) {
300
- variables.push({
301
- name: v,
302
- prompt: `Enter ${v}:`,
303
- required: true
304
- });
305
- }
306
- }
307
- }
163
+ const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
164
+ variables.push(...selectedGlobals);
308
165
  }
166
+ const additionalVars = await promptAdditionalVariables(variables, options);
167
+ variables.push(...additionalVars);
309
168
  }
310
169
  // 1. Structure (skeleton) - Additive mode
311
170
  let folders = [];
312
171
  if (!isFullMode) {
313
172
  // Additive mode: only add new folders
314
- const structureDiff = getNewFolders(config.templates[templateName].folders, resolvedPath, ignorePatterns);
315
- if (structureDiff.added.length > 0) {
173
+ const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
174
+ ? fileTemplateConfig.folders
175
+ : extractStructure(resolvedPath, resolvedPath, ignorePatterns);
176
+ const existingFolders = config.templates[templateName].folders || [];
177
+ const newFolders = detectedFolders.filter(f => !existingFolders.some(ef => ef.name === f.name));
178
+ if (newFolders.length > 0) {
316
179
  console.log(chalk.cyan(`\n📊 New Folders:`));
317
- console.log(chalk.green(` + ${structureDiff.added.length} new folder(s): ${structureDiff.added.map(f => f.name).join(', ')}`));
318
- // Show interactive checkbox for new folders
319
- if (!options.yes && !options.json) {
320
- const { selectedFolders } = await inquirer.prompt({
321
- type: 'checkbox',
322
- name: 'selectedFolders',
323
- message: 'Select folders to include in template structure (space to toggle):',
324
- loop: false,
325
- theme: {
326
- icon: {
327
- checked: chalk.green('[x] '),
328
- unchecked: '[ ] ',
329
- }
330
- },
331
- choices: structureDiff.added.map(f => ({
332
- name: f.name,
333
- checked: true // Auto-select by default
334
- }))
335
- });
336
- // Only add selected folders
337
- folders = [...config.templates[templateName].folders, ...structureDiff.added.filter(f => selectedFolders.includes(f.name))];
338
- }
339
- else {
340
- // In --yes or --json mode, auto-add all
341
- folders = [...config.templates[templateName].folders, ...structureDiff.added];
342
- }
180
+ console.log(chalk.green(` + ${newFolders.length} new folder(s): ${newFolders.map(f => f.name).join(', ')}`));
181
+ const addedFolders = await promptNewFolders(newFolders, options);
182
+ folders = [...existingFolders, ...addedFolders];
343
183
  }
344
184
  else {
345
- console.log(chalk.cyan("No new folders detected"));
346
- folders = config.templates[templateName].folders;
185
+ printNoNewFolders();
186
+ folders = existingFolders;
347
187
  }
348
188
  }
349
189
  else {
@@ -352,7 +192,7 @@ export async function update(sourcePath, templateName, options = {}) {
352
192
  ? fileTemplateConfig.folders
353
193
  : extractStructure(resolvedPath, resolvedPath, ignorePatterns);
354
194
  }
355
- // 2. Content Selection (Root only) - Additive mode
195
+ // 2. Content Selection (Root only)
356
196
  const rootEntries = fs.readdirSync(resolvedPath, { withFileTypes: true })
357
197
  .filter(e => !shouldExclude(resolvedPath, path.join(resolvedPath, e.name), ignorePatterns))
358
198
  .filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
@@ -363,173 +203,68 @@ export async function update(sourcePath, templateName, options = {}) {
363
203
  let selectedStructure = [];
364
204
  if (!isFullMode) {
365
205
  // Additive mode for files and folders
366
- // Start with existing files and folders, preserving their substitute_variables settings
367
206
  const existingCopyFiles = config.templates[templateName].copy_files || [];
368
- selectedFiles = existingCopyFiles.map(f => f.src);
369
- const fileDiff = getNewFiles(config.templates[templateName], resolvedPath);
370
- if (fileDiff.newFiles.length > 0) {
371
- console.log(chalk.cyan(`\n📊 New Files:`));
372
- console.log(chalk.green(` + ${fileDiff.newFiles.length} new file(s): ${fileDiff.newFiles.join(', ')}`));
373
- // Show interactive checkbox for new files
374
- if (!options.yes && !options.json) {
375
- const { selectedFileChoices } = await inquirer.prompt({
376
- type: 'checkbox',
377
- name: 'selectedFileChoices',
378
- message: 'Select files to include (space to toggle):',
379
- loop: false,
380
- theme: {
381
- icon: {
382
- checked: chalk.green('[x] '),
383
- unchecked: '[ ] ',
384
- }
385
- },
386
- choices: fileDiff.newFiles.map(f => ({
387
- name: f,
388
- checked: true // Auto-select by default
389
- }))
390
- });
391
- // Only add selected files
392
- selectedFiles.push(...fileDiff.newFiles.filter(f => selectedFileChoices.includes(f)));
393
- }
394
- else {
395
- // In --yes or --json mode, auto-add all
396
- selectedFiles.push(...fileDiff.newFiles);
397
- }
398
- }
399
- else {
400
- console.log(chalk.cyan("No new files detected"));
401
- }
402
- // For folders, use additive mode logic
403
- // Always preserve existing folders first
207
+ // New files
208
+ const newFiles = rootFiles.filter(f => !existingCopyFiles.some(cf => cf.src === f));
209
+ printNewFiles(newFiles.length, newFiles);
210
+ const addedFiles = await promptNewFiles(newFiles, options);
211
+ selectedFiles = [...existingCopyFiles.filter(cf => !rootDirs.includes(cf.src)).map(cf => cf.src), ...addedFiles];
212
+ // Structure
404
213
  selectedStructure = config.templates[templateName].folders?.map(f => f.name) || [];
405
- selectedFolders = selectedStructure.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
214
+ // Seed selectedFolders from existing copy_files directory entries
215
+ selectedFolders = existingCopyFiles
216
+ .filter(f => rootDirs.includes(f.src))
217
+ .map(f => f.src);
406
218
  if (rootDirs.length > 0) {
407
- const structureDiff = getNewFolders(config.templates[templateName].folders, resolvedPath, ignorePatterns);
408
- if (structureDiff.added.length > 0) {
409
- // Auto-select added folders for structure
410
- selectedStructure = [...new Set([...selectedStructure, ...structureDiff.added.map(f => f.name)])];
411
- // Auto-select for recursive copy if they're in the standard list
412
- selectedFolders = selectedStructure.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
413
- }
414
- else {
415
- // Keep existing folders
416
- selectedFolders = selectedStructure.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
417
- }
418
- }
419
- else {
420
- // Keep existing folders
421
- selectedFolders = selectedStructure.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
219
+ const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
220
+ ? fileTemplateConfig.folders
221
+ : extractStructure(resolvedPath, resolvedPath, ignorePatterns);
222
+ const newFolders = detectedFolders.filter(f => !config.templates[templateName].folders?.some(ef => ef.name === f.name));
223
+ const addedDirs = newFolders
224
+ .filter(f => ['APP', 'scripts', 'bin'].some(p => f.name === p))
225
+ .map(f => f.name);
226
+ selectedStructure = [...new Set([...selectedStructure, ...addedDirs])];
227
+ selectedFolders = [...new Set([...selectedFolders, ...addedDirs])];
422
228
  }
423
229
  }
424
230
  else {
425
231
  // Full mode: original behavior
426
232
  if (options.yes || options.json) {
427
- // If --yes, auto-select the defaults
428
233
  selectedFiles = rootFiles.filter(f => ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase()));
429
- selectedStructure = rootDirs; // Include all folders in structure
430
- selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p)); // Only copy specific ones recursively
234
+ selectedStructure = rootDirs;
235
+ selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
431
236
  }
432
237
  else {
433
- if (rootFiles.length > 0) {
434
- const filesResponse = await inquirer.prompt({
435
- type: 'checkbox',
436
- name: 'selectedFiles',
437
- message: 'Select root files to include as boilerplate:',
438
- loop: false,
439
- theme: {
440
- icon: {
441
- checked: chalk.green('[x] '),
442
- unchecked: '[ ] ',
443
- }
444
- },
445
- choices: rootFiles.map(f => ({
446
- name: f,
447
- checked: ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase())
448
- }))
449
- });
450
- selectedFiles = filesResponse.selectedFiles;
451
- }
452
- else {
453
- selectedFiles = [];
454
- }
455
- if (rootDirs.length > 0) {
456
- const foldersResponse = await inquirer.prompt({
457
- type: 'checkbox',
458
- name: 'selectedStructure',
459
- message: 'Select folders to include in the template structure (skeleton):',
460
- loop: false,
461
- theme: {
462
- icon: {
463
- checked: chalk.green('[x] '),
464
- unchecked: '[ ] ',
465
- }
466
- },
467
- choices: rootDirs.map(d => ({
468
- name: d,
469
- checked: true // Include all in structure by default
470
- }))
471
- });
472
- selectedStructure = foldersResponse.selectedStructure;
473
- }
474
- else {
475
- selectedStructure = [];
476
- }
477
- if (selectedStructure.length > 0) {
478
- const copyFoldersResponse = await inquirer.prompt({
479
- type: 'checkbox',
480
- name: 'selectedFolders',
481
- message: 'Select folders to copy RECURSIVELY as boilerplate (with contents):',
482
- loop: false,
483
- theme: {
484
- icon: {
485
- checked: chalk.green('[x] '),
486
- unchecked: '[ ] ',
487
- }
488
- },
489
- choices: selectedStructure.map((d) => ({
490
- name: d,
491
- checked: ['APP', 'scripts', 'bin'].some(p => d === p)
492
- }))
493
- });
494
- selectedFolders = copyFoldersResponse.selectedFolders;
495
- }
496
- else {
497
- selectedFolders = [];
498
- }
238
+ selectedFiles = await promptRootFiles(rootFiles, undefined, options);
239
+ selectedStructure = await promptStructureFolders(rootDirs, options);
240
+ selectedFolders = await promptCopyFolders(selectedStructure, undefined, options);
499
241
  }
500
242
  }
501
243
  const copy_files = [];
502
244
  if (fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
503
245
  copy_files.push(...fileTemplateConfig.copy_files);
504
246
  }
505
- else {
247
+ else if (!isFullMode) {
506
248
  const existingCopyFiles = config.templates[templateName].copy_files || [];
507
- const existingMap = new Map();
508
- for (const entry of existingCopyFiles) {
509
- existingMap.set(entry.src, entry);
510
- }
511
- const addedSrcs = new Set();
249
+ const existingSrcs = new Set(existingCopyFiles.map(e => e.src));
250
+ copy_files.push(...existingCopyFiles);
512
251
  for (const f of selectedFiles) {
513
- if (addedSrcs.has(f))
252
+ if (existingSrcs.has(f))
514
253
  continue;
515
- addedSrcs.add(f);
516
- if (existingMap.has(f)) {
517
- copy_files.push(existingMap.get(f));
518
- }
519
- else {
520
- copy_files.push({ src: f, dest: f, substitute_variables: true });
521
- }
254
+ copy_files.push({ src: f, dest: f, substitute_variables: true });
522
255
  }
523
256
  for (const d of selectedFolders) {
524
- if (addedSrcs.has(d))
257
+ if (existingSrcs.has(d))
525
258
  continue;
526
- addedSrcs.add(d);
527
- if (existingMap.has(d)) {
528
- copy_files.push(existingMap.get(d));
529
- }
530
- else {
531
- copy_files.push({ src: d, dest: d, substitute_variables: true });
532
- }
259
+ copy_files.push({ src: d, dest: d, substitute_variables: true });
260
+ }
261
+ }
262
+ else {
263
+ for (const f of selectedFiles) {
264
+ copy_files.push({ src: f, dest: f, substitute_variables: true });
265
+ }
266
+ for (const d of selectedFolders) {
267
+ copy_files.push({ src: d, dest: d, substitute_variables: true });
533
268
  }
534
269
  }
535
270
  const templateConfig = {
@@ -540,40 +275,11 @@ export async function update(sourcePath, templateName, options = {}) {
540
275
  variables: variables.length > 0 ? variables : undefined
541
276
  };
542
277
  // Check for post_config scripts
543
- let postConfigTasks = [];
544
- if (fileTemplateConfig.post_config && Array.isArray(fileTemplateConfig.post_config)) {
545
- postConfigTasks = [...fileTemplateConfig.post_config];
546
- }
547
- else {
548
- const shPath = path.join(resolvedPath, 'post_config.sh');
549
- const batPath = path.join(resolvedPath, 'post_config.bat');
550
- if (fs.existsSync(shPath)) {
551
- const lines = fs.readFileSync(shPath, 'utf-8').split('\n');
552
- let currentDesc = '';
553
- for (const line of lines) {
554
- if (line.startsWith('echo "Running: ')) {
555
- currentDesc = line.substring(15, line.length - 1).replace(/"$/, '');
556
- }
557
- else if (line.trim() && !line.startsWith('#') && !line.startsWith('echo ')) {
558
- postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
559
- currentDesc = '';
560
- }
561
- }
562
- }
563
- else if (fs.existsSync(batPath)) {
564
- const lines = fs.readFileSync(batPath, 'utf-8').split('\n');
565
- let currentDesc = '';
566
- for (const line of lines) {
567
- if (line.startsWith('echo Running: ')) {
568
- currentDesc = line.substring(14).trim();
569
- }
570
- else if (line.trim() && !line.startsWith('::') && !line.startsWith('@echo') && !line.startsWith('echo ')) {
571
- postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
572
- currentDesc = '';
573
- }
574
- }
575
- }
576
- }
278
+ const shPath = path.join(resolvedPath, 'post_config.sh');
279
+ const batPath = path.join(resolvedPath, 'post_config.bat');
280
+ const detectedTasks = parsePostConfigScript(shPath, batPath);
281
+ let postConfigTasks = config.templates[templateName].post_config || [];
282
+ postConfigTasks = mergePostConfigTasks(postConfigTasks, fileTemplateConfig.post_config, detectedTasks);
577
283
  if (postConfigTasks.length > 0) {
578
284
  templateConfig.post_config = postConfigTasks;
579
285
  if (!options.json && !fileTemplateConfig.post_config)
@@ -583,36 +289,7 @@ export async function update(sourcePath, templateName, options = {}) {
583
289
  const defaultPostConfig = getDefaultPostConfig(config);
584
290
  const defaultApplicableTasks = defaultPostConfig.filter(t => !t.type || t.type === templateName);
585
291
  if (defaultApplicableTasks.length > 0) {
586
- let selectedTaskNames = [];
587
- if (options.yes || options.json) {
588
- selectedTaskNames = defaultApplicableTasks.map(t => t.command || t.script || '');
589
- }
590
- else {
591
- const choices = [];
592
- for (const t of defaultApplicableTasks) {
593
- const cmd = t.command || t.script || '(no command)';
594
- const desc = t.description ? ` (${t.description})` : '';
595
- choices.push({
596
- name: `${cmd}${desc}`,
597
- value: cmd,
598
- checked: t.checked !== false
599
- });
600
- }
601
- const response = await inquirer.prompt({
602
- type: 'checkbox',
603
- name: 'selected',
604
- message: 'Select default post-config tasks to include in this template:',
605
- loop: false,
606
- theme: {
607
- icon: {
608
- checked: chalk.green('[x] '),
609
- unchecked: '[ ] ',
610
- }
611
- },
612
- choices
613
- });
614
- selectedTaskNames = response.selected || [];
615
- }
292
+ const selectedTaskNames = await promptPostConfigTasks(defaultApplicableTasks, options);
616
293
  if (selectedTaskNames.length > 0) {
617
294
  if (!templateConfig.post_config)
618
295
  templateConfig.post_config = [];
@@ -628,46 +305,11 @@ export async function update(sourcePath, templateName, options = {}) {
628
305
  }
629
306
  }
630
307
  // 3. Detect executables at root
631
- const detectedExecutables = [];
632
- for (const file of rootFiles) {
633
- const fullPath = path.join(resolvedPath, file);
634
- if (isExecutable(fullPath, file)) {
635
- detectedExecutables.push(file);
636
- }
637
- }
308
+ const detectedExecutables = rootFiles
309
+ .filter(file => isExecutable(path.join(resolvedPath, file), file))
310
+ .filter(file => !shouldExcludeFile(file));
638
311
  const existingPostCopy = config.templates[templateName].post_copy || [];
639
- let post_copy = [...existingPostCopy];
640
- if (fileTemplateConfig.post_copy && Array.isArray(fileTemplateConfig.post_copy)) {
641
- for (const pc of fileTemplateConfig.post_copy) {
642
- if (!post_copy.some(existing => existing.src === pc.src)) {
643
- post_copy.push(pc);
644
- }
645
- }
646
- }
647
- const newExecutables = detectedExecutables.filter(file => !post_copy.some(existing => existing.src === file));
648
- if (newExecutables.length > 0) {
649
- if (!options.json) {
650
- console.log(chalk.cyan("\nAuto-detected " + newExecutables.length + " new executable file(s) at root:"));
651
- for (const file of newExecutables) {
652
- console.log(chalk.gray(" - " + file));
653
- }
654
- }
655
- let addPostCopy = true;
656
- if (!options.yes && !options.json) {
657
- const response = await inquirer.prompt({
658
- type: 'confirm',
659
- name: 'addPostCopy',
660
- message: 'Add these to post_copy (auto-chmod)?',
661
- default: true
662
- });
663
- addPostCopy = response.addPostCopy;
664
- }
665
- if (addPostCopy) {
666
- for (const file of newExecutables) {
667
- post_copy.push({ src: file, dest: file });
668
- }
669
- }
670
- }
312
+ const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, detectedExecutables);
671
313
  if (post_copy.length > 0) {
672
314
  templateConfig.post_copy = post_copy;
673
315
  const postCopySrcs = post_copy.map(f => f.src);
@@ -678,8 +320,6 @@ export async function update(sourcePath, templateName, options = {}) {
678
320
  name: templateName,
679
321
  ...templateConfig
680
322
  };
681
- // Force the application to wait until every single byte of this JSON string
682
- // safely clears the operating system's pipe buffer before letting the process die.
683
323
  process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
684
324
  process.exit(0);
685
325
  });
@@ -689,96 +329,3 @@ export async function update(sourcePath, templateName, options = {}) {
689
329
  saveConfig(config);
690
330
  console.log(chalk.green(`\n✓ Template saved as "${templateName}"`));
691
331
  }
692
- function isExecutable(fullPath, fileName) {
693
- if (shouldExcludeFile(fileName))
694
- return false;
695
- const ext = path.extname(fileName).toLowerCase();
696
- if (['.sh', '.py', '.bash', '.bat', '.cmd'].includes(ext))
697
- return true;
698
- if (fileName.toLowerCase() === 'makefile')
699
- return true;
700
- try {
701
- const stat = fs.statSync(fullPath);
702
- return !!(stat.mode & 0o111);
703
- }
704
- catch {
705
- return false;
706
- }
707
- }
708
- function extractStructure(dirPath, rootPath, ignorePatterns) {
709
- let nodes = [];
710
- try {
711
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
712
- for (const entry of entries) {
713
- const fullPath = path.join(dirPath, entry.name);
714
- const relativePath = path.relative(rootPath, fullPath);
715
- // Only include directories in the structure skeleton
716
- const isDirectory = entry.isDirectory() || (entry.isSymbolicLink() && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory());
717
- if (!isDirectory)
718
- continue;
719
- if (shouldIgnore(entry.name, relativePath, ignorePatterns))
720
- continue;
721
- if (shouldExclude(dirPath, fullPath))
722
- continue;
723
- const children = extractStructure(fullPath, rootPath, ignorePatterns);
724
- let info = "";
725
- const gitkeepPath = path.join(fullPath, '.gitkeep.md');
726
- const infoPath = path.join(fullPath, '.info.md');
727
- if (fs.existsSync(infoPath))
728
- info = fs.readFileSync(infoPath, 'utf-8').trim();
729
- else if (fs.existsSync(gitkeepPath))
730
- info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
731
- nodes.push({ name: entry.name, info: info, children: children });
732
- }
733
- }
734
- catch (e) { }
735
- return nodes;
736
- }
737
- /**
738
- * Scan text files in top-level and 1st-level subdirectories for {{ variable_name }} placeholders.
739
- */
740
- function findVariablesInFiles(dirPath, rootPath, ignorePatterns) {
741
- const variables = new Set();
742
- const regex = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
743
- const textExtensions = ['.md', '.txt', '.makerc', '.json', '.yaml', '.yml', '.ini', '.conf', '.config', '.sh', '.py', '.js', '.ts', '.html', '.css', '.makefile'];
744
- const scan = (currentPath, depth) => {
745
- if (depth > 1)
746
- return; // Top level (0) and 1st level subfolders (1)
747
- try {
748
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
749
- for (const entry of entries) {
750
- const fullPath = path.join(currentPath, entry.name);
751
- const relativePath = path.relative(rootPath, fullPath);
752
- if (entry.isDirectory()) {
753
- if (shouldIgnore(entry.name, relativePath, ignorePatterns))
754
- continue;
755
- if (shouldExclude(currentPath, fullPath))
756
- continue;
757
- scan(fullPath, depth + 1);
758
- }
759
- else if (entry.isFile()) {
760
- const ext = path.extname(entry.name).toLowerCase();
761
- const isMakefile = entry.name.toLowerCase() === 'makefile';
762
- if (textExtensions.includes(ext) || isMakefile || ext === '') {
763
- try {
764
- const content = fs.readFileSync(fullPath, 'utf-8');
765
- let match;
766
- regex.lastIndex = 0;
767
- while ((match = regex.exec(content)) !== null) {
768
- variables.add(match[1]);
769
- }
770
- }
771
- catch (e) {
772
- // Skip files that can't be read or aren't text
773
- }
774
- }
775
- }
776
- }
777
- }
778
- catch (e) {
779
- // Ignore directory read errors
780
- }
781
- };
782
- scan(dirPath, 0);
783
- return Array.from(variables);
784
- }