@garyr/pt-cli 0.40.1 → 0.42.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/.test-home-remote/.pt/config.yaml +10 -0
- package/README.md +85 -65
- package/dist/commands/initCommand.js +4 -4
- package/dist/commands/learnCommand.js +120 -432
- package/dist/commands/template-detection.js +256 -0
- package/dist/commands/template-prompts.js +332 -0
- package/dist/commands/template-utils.js +565 -0
- package/dist/commands/updateCommand.js +78 -532
- package/dist/config.js +38 -5
- package/dist/safety.js +203 -42
- package/doc/security.md +96 -36
- package/package.json +1 -1
- package/skills/agency-pt-operator/SKILL.md +54 -9
- package/src/commands/initCommand.ts +9 -9
- package/src/commands/learnCommand.ts +157 -423
- package/src/commands/template-detection.ts +269 -0
- package/src/commands/template-prompts.ts +406 -0
- package/src/commands/template-utils.ts +651 -0
- package/src/commands/updateCommand.ts +136 -577
- package/src/config.ts +29 -5
- package/src/safety.ts +239 -53
- package/tests/remote.test.ts +110 -0
- package/tests/safety.test.ts +296 -0
- package/tests/update.test.ts +417 -0
|
@@ -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
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
315
|
-
|
|
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(` + ${
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
346
|
-
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)
|
|
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,141 +203,41 @@ 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 copy_files entries as the base (preserves all settings including substitute_variables)
|
|
367
206
|
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
console.log(chalk.green(` + ${fileDiff.newFiles.length} new file(s): ${fileDiff.newFiles.join(', ')}`));
|
|
375
|
-
// Show interactive checkbox for new files
|
|
376
|
-
if (!options.yes && !options.json) {
|
|
377
|
-
const { selectedFileChoices } = await inquirer.prompt({
|
|
378
|
-
type: 'checkbox',
|
|
379
|
-
name: 'selectedFileChoices',
|
|
380
|
-
message: 'Select files to include (space to toggle):',
|
|
381
|
-
loop: false,
|
|
382
|
-
theme: {
|
|
383
|
-
icon: {
|
|
384
|
-
checked: chalk.green('[x] '),
|
|
385
|
-
unchecked: '[ ] ',
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
choices: fileDiff.newFiles.map(f => ({
|
|
389
|
-
name: f,
|
|
390
|
-
checked: true // Auto-select by default
|
|
391
|
-
}))
|
|
392
|
-
});
|
|
393
|
-
newSelectedFiles = fileDiff.newFiles.filter(f => selectedFileChoices.includes(f));
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
// In --yes or --json mode, auto-add all
|
|
397
|
-
newSelectedFiles = [...fileDiff.newFiles];
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
console.log(chalk.cyan("No new files detected"));
|
|
402
|
-
}
|
|
403
|
-
// For folders, use additive mode logic
|
|
404
|
-
// Preserve existing folder structure
|
|
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
|
|
405
213
|
selectedStructure = config.templates[templateName].folders?.map(f => f.name) || [];
|
|
406
|
-
// Seed selectedFolders from existing copy_files directory entries
|
|
214
|
+
// Seed selectedFolders from existing copy_files directory entries
|
|
407
215
|
selectedFolders = existingCopyFiles
|
|
408
216
|
.filter(f => rootDirs.includes(f.src))
|
|
409
217
|
.map(f => f.src);
|
|
410
218
|
if (rootDirs.length > 0) {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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])];
|
|
420
228
|
}
|
|
421
|
-
// Merge: existing folder copy_files + newly selected dirs
|
|
422
|
-
selectedFolders = [...new Set([...selectedFolders, ...newSelectedFolderDirs])];
|
|
423
|
-
// Also carry forward file names for use in copy_files construction below
|
|
424
|
-
selectedFiles = [...existingCopyFiles.filter(f => !rootDirs.includes(f.src)).map(f => f.src), ...newSelectedFiles];
|
|
425
229
|
}
|
|
426
230
|
else {
|
|
427
231
|
// Full mode: original behavior
|
|
428
232
|
if (options.yes || options.json) {
|
|
429
|
-
// If --yes, auto-select the defaults
|
|
430
233
|
selectedFiles = rootFiles.filter(f => ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase()));
|
|
431
|
-
selectedStructure = rootDirs;
|
|
432
|
-
selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
|
|
234
|
+
selectedStructure = rootDirs;
|
|
235
|
+
selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
|
|
433
236
|
}
|
|
434
237
|
else {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
name: 'selectedFiles',
|
|
439
|
-
message: 'Select root files to include as boilerplate:',
|
|
440
|
-
loop: false,
|
|
441
|
-
theme: {
|
|
442
|
-
icon: {
|
|
443
|
-
checked: chalk.green('[x] '),
|
|
444
|
-
unchecked: '[ ] ',
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
choices: rootFiles.map(f => ({
|
|
448
|
-
name: f,
|
|
449
|
-
checked: ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase())
|
|
450
|
-
}))
|
|
451
|
-
});
|
|
452
|
-
selectedFiles = filesResponse.selectedFiles;
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
selectedFiles = [];
|
|
456
|
-
}
|
|
457
|
-
if (rootDirs.length > 0) {
|
|
458
|
-
const foldersResponse = await inquirer.prompt({
|
|
459
|
-
type: 'checkbox',
|
|
460
|
-
name: 'selectedStructure',
|
|
461
|
-
message: 'Select folders to include in the template structure (skeleton):',
|
|
462
|
-
loop: false,
|
|
463
|
-
theme: {
|
|
464
|
-
icon: {
|
|
465
|
-
checked: chalk.green('[x] '),
|
|
466
|
-
unchecked: '[ ] ',
|
|
467
|
-
}
|
|
468
|
-
},
|
|
469
|
-
choices: rootDirs.map(d => ({
|
|
470
|
-
name: d,
|
|
471
|
-
checked: true // Include all in structure by default
|
|
472
|
-
}))
|
|
473
|
-
});
|
|
474
|
-
selectedStructure = foldersResponse.selectedStructure;
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
selectedStructure = [];
|
|
478
|
-
}
|
|
479
|
-
if (selectedStructure.length > 0) {
|
|
480
|
-
const copyFoldersResponse = await inquirer.prompt({
|
|
481
|
-
type: 'checkbox',
|
|
482
|
-
name: 'selectedFolders',
|
|
483
|
-
message: 'Select folders to copy RECURSIVELY as boilerplate (with contents):',
|
|
484
|
-
loop: false,
|
|
485
|
-
theme: {
|
|
486
|
-
icon: {
|
|
487
|
-
checked: chalk.green('[x] '),
|
|
488
|
-
unchecked: '[ ] ',
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
choices: selectedStructure.map((d) => ({
|
|
492
|
-
name: d,
|
|
493
|
-
checked: ['APP', 'scripts', 'bin'].some(p => d === p)
|
|
494
|
-
}))
|
|
495
|
-
});
|
|
496
|
-
selectedFolders = copyFoldersResponse.selectedFolders;
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
selectedFolders = [];
|
|
500
|
-
}
|
|
238
|
+
selectedFiles = await promptRootFiles(rootFiles, undefined, options);
|
|
239
|
+
selectedStructure = await promptStructureFolders(rootDirs, options);
|
|
240
|
+
selectedFolders = await promptCopyFolders(selectedStructure, undefined, options);
|
|
501
241
|
}
|
|
502
242
|
}
|
|
503
243
|
const copy_files = [];
|
|
@@ -505,27 +245,21 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
505
245
|
copy_files.push(...fileTemplateConfig.copy_files);
|
|
506
246
|
}
|
|
507
247
|
else if (!isFullMode) {
|
|
508
|
-
// Additive mode: start directly from the existing copy_files to preserve all settings,
|
|
509
|
-
// then append only brand-new entries (never rebuild from scratch).
|
|
510
248
|
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
511
249
|
const existingSrcs = new Set(existingCopyFiles.map(e => e.src));
|
|
512
|
-
// Begin with all existing entries untouched
|
|
513
250
|
copy_files.push(...existingCopyFiles);
|
|
514
|
-
// Append new files that were selected by the user
|
|
515
251
|
for (const f of selectedFiles) {
|
|
516
252
|
if (existingSrcs.has(f))
|
|
517
|
-
continue;
|
|
253
|
+
continue;
|
|
518
254
|
copy_files.push({ src: f, dest: f, substitute_variables: true });
|
|
519
255
|
}
|
|
520
|
-
// Append new directory entries that were selected
|
|
521
256
|
for (const d of selectedFolders) {
|
|
522
257
|
if (existingSrcs.has(d))
|
|
523
|
-
continue;
|
|
258
|
+
continue;
|
|
524
259
|
copy_files.push({ src: d, dest: d, substitute_variables: true });
|
|
525
260
|
}
|
|
526
261
|
}
|
|
527
262
|
else {
|
|
528
|
-
// Full mode: build from scratch using the selected files/folders
|
|
529
263
|
for (const f of selectedFiles) {
|
|
530
264
|
copy_files.push({ src: f, dest: f, substitute_variables: true });
|
|
531
265
|
}
|
|
@@ -541,40 +275,11 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
541
275
|
variables: variables.length > 0 ? variables : undefined
|
|
542
276
|
};
|
|
543
277
|
// Check for post_config scripts
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const shPath = path.join(resolvedPath, 'post_config.sh');
|
|
550
|
-
const batPath = path.join(resolvedPath, 'post_config.bat');
|
|
551
|
-
if (fs.existsSync(shPath)) {
|
|
552
|
-
const lines = fs.readFileSync(shPath, 'utf-8').split('\n');
|
|
553
|
-
let currentDesc = '';
|
|
554
|
-
for (const line of lines) {
|
|
555
|
-
if (line.startsWith('echo "Running: ')) {
|
|
556
|
-
currentDesc = line.substring(15, line.length - 1).replace(/"$/, '');
|
|
557
|
-
}
|
|
558
|
-
else if (line.trim() && !line.startsWith('#') && !line.startsWith('echo ')) {
|
|
559
|
-
postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
|
|
560
|
-
currentDesc = '';
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
else if (fs.existsSync(batPath)) {
|
|
565
|
-
const lines = fs.readFileSync(batPath, 'utf-8').split('\n');
|
|
566
|
-
let currentDesc = '';
|
|
567
|
-
for (const line of lines) {
|
|
568
|
-
if (line.startsWith('echo Running: ')) {
|
|
569
|
-
currentDesc = line.substring(14).trim();
|
|
570
|
-
}
|
|
571
|
-
else if (line.trim() && !line.startsWith('::') && !line.startsWith('@echo') && !line.startsWith('echo ')) {
|
|
572
|
-
postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
|
|
573
|
-
currentDesc = '';
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
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);
|
|
578
283
|
if (postConfigTasks.length > 0) {
|
|
579
284
|
templateConfig.post_config = postConfigTasks;
|
|
580
285
|
if (!options.json && !fileTemplateConfig.post_config)
|
|
@@ -584,36 +289,7 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
584
289
|
const defaultPostConfig = getDefaultPostConfig(config);
|
|
585
290
|
const defaultApplicableTasks = defaultPostConfig.filter(t => !t.type || t.type === templateName);
|
|
586
291
|
if (defaultApplicableTasks.length > 0) {
|
|
587
|
-
|
|
588
|
-
if (options.yes || options.json) {
|
|
589
|
-
selectedTaskNames = defaultApplicableTasks.map(t => t.command || t.script || '');
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
const choices = [];
|
|
593
|
-
for (const t of defaultApplicableTasks) {
|
|
594
|
-
const cmd = t.command || t.script || '(no command)';
|
|
595
|
-
const desc = t.description ? ` (${t.description})` : '';
|
|
596
|
-
choices.push({
|
|
597
|
-
name: `${cmd}${desc}`,
|
|
598
|
-
value: cmd,
|
|
599
|
-
checked: t.checked !== false
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
const response = await inquirer.prompt({
|
|
603
|
-
type: 'checkbox',
|
|
604
|
-
name: 'selected',
|
|
605
|
-
message: 'Select default post-config tasks to include in this template:',
|
|
606
|
-
loop: false,
|
|
607
|
-
theme: {
|
|
608
|
-
icon: {
|
|
609
|
-
checked: chalk.green('[x] '),
|
|
610
|
-
unchecked: '[ ] ',
|
|
611
|
-
}
|
|
612
|
-
},
|
|
613
|
-
choices
|
|
614
|
-
});
|
|
615
|
-
selectedTaskNames = response.selected || [];
|
|
616
|
-
}
|
|
292
|
+
const selectedTaskNames = await promptPostConfigTasks(defaultApplicableTasks, options);
|
|
617
293
|
if (selectedTaskNames.length > 0) {
|
|
618
294
|
if (!templateConfig.post_config)
|
|
619
295
|
templateConfig.post_config = [];
|
|
@@ -629,46 +305,11 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
629
305
|
}
|
|
630
306
|
}
|
|
631
307
|
// 3. Detect executables at root
|
|
632
|
-
const detectedExecutables =
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (isExecutable(fullPath, file)) {
|
|
636
|
-
detectedExecutables.push(file);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
308
|
+
const detectedExecutables = rootFiles
|
|
309
|
+
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
310
|
+
.filter(file => !shouldExcludeFile(file));
|
|
639
311
|
const existingPostCopy = config.templates[templateName].post_copy || [];
|
|
640
|
-
|
|
641
|
-
if (fileTemplateConfig.post_copy && Array.isArray(fileTemplateConfig.post_copy)) {
|
|
642
|
-
for (const pc of fileTemplateConfig.post_copy) {
|
|
643
|
-
if (!post_copy.some(existing => existing.src === pc.src)) {
|
|
644
|
-
post_copy.push(pc);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
const newExecutables = detectedExecutables.filter(file => !post_copy.some(existing => existing.src === file));
|
|
649
|
-
if (newExecutables.length > 0) {
|
|
650
|
-
if (!options.json) {
|
|
651
|
-
console.log(chalk.cyan("\nAuto-detected " + newExecutables.length + " new executable file(s) at root:"));
|
|
652
|
-
for (const file of newExecutables) {
|
|
653
|
-
console.log(chalk.gray(" - " + file));
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
let addPostCopy = true;
|
|
657
|
-
if (!options.yes && !options.json) {
|
|
658
|
-
const response = await inquirer.prompt({
|
|
659
|
-
type: 'confirm',
|
|
660
|
-
name: 'addPostCopy',
|
|
661
|
-
message: 'Add these to post_copy (auto-chmod)?',
|
|
662
|
-
default: true
|
|
663
|
-
});
|
|
664
|
-
addPostCopy = response.addPostCopy;
|
|
665
|
-
}
|
|
666
|
-
if (addPostCopy) {
|
|
667
|
-
for (const file of newExecutables) {
|
|
668
|
-
post_copy.push({ src: file, dest: file });
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
312
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, detectedExecutables);
|
|
672
313
|
if (post_copy.length > 0) {
|
|
673
314
|
templateConfig.post_copy = post_copy;
|
|
674
315
|
const postCopySrcs = post_copy.map(f => f.src);
|
|
@@ -679,8 +320,6 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
679
320
|
name: templateName,
|
|
680
321
|
...templateConfig
|
|
681
322
|
};
|
|
682
|
-
// Force the application to wait until every single byte of this JSON string
|
|
683
|
-
// safely clears the operating system's pipe buffer before letting the process die.
|
|
684
323
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
|
|
685
324
|
process.exit(0);
|
|
686
325
|
});
|
|
@@ -690,96 +329,3 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
690
329
|
saveConfig(config);
|
|
691
330
|
console.log(chalk.green(`\n✓ Template saved as "${templateName}"`));
|
|
692
331
|
}
|
|
693
|
-
function isExecutable(fullPath, fileName) {
|
|
694
|
-
if (shouldExcludeFile(fileName))
|
|
695
|
-
return false;
|
|
696
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
697
|
-
if (['.sh', '.py', '.bash', '.bat', '.cmd'].includes(ext))
|
|
698
|
-
return true;
|
|
699
|
-
if (fileName.toLowerCase() === 'makefile')
|
|
700
|
-
return true;
|
|
701
|
-
try {
|
|
702
|
-
const stat = fs.statSync(fullPath);
|
|
703
|
-
return !!(stat.mode & 0o111);
|
|
704
|
-
}
|
|
705
|
-
catch {
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
function extractStructure(dirPath, rootPath, ignorePatterns) {
|
|
710
|
-
let nodes = [];
|
|
711
|
-
try {
|
|
712
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
713
|
-
for (const entry of entries) {
|
|
714
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
715
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
716
|
-
// Only include directories in the structure skeleton
|
|
717
|
-
const isDirectory = entry.isDirectory() || (entry.isSymbolicLink() && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory());
|
|
718
|
-
if (!isDirectory)
|
|
719
|
-
continue;
|
|
720
|
-
if (shouldIgnore(entry.name, relativePath, ignorePatterns))
|
|
721
|
-
continue;
|
|
722
|
-
if (shouldExclude(dirPath, fullPath))
|
|
723
|
-
continue;
|
|
724
|
-
const children = extractStructure(fullPath, rootPath, ignorePatterns);
|
|
725
|
-
let info = "";
|
|
726
|
-
const gitkeepPath = path.join(fullPath, '.gitkeep.md');
|
|
727
|
-
const infoPath = path.join(fullPath, '.info.md');
|
|
728
|
-
if (fs.existsSync(infoPath))
|
|
729
|
-
info = fs.readFileSync(infoPath, 'utf-8').trim();
|
|
730
|
-
else if (fs.existsSync(gitkeepPath))
|
|
731
|
-
info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
|
|
732
|
-
nodes.push({ name: entry.name, info: info, children: children });
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
catch (e) { }
|
|
736
|
-
return nodes;
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Scan text files in top-level and 1st-level subdirectories for {{ variable_name }} placeholders.
|
|
740
|
-
*/
|
|
741
|
-
function findVariablesInFiles(dirPath, rootPath, ignorePatterns) {
|
|
742
|
-
const variables = new Set();
|
|
743
|
-
const regex = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
744
|
-
const textExtensions = ['.md', '.txt', '.makerc', '.json', '.yaml', '.yml', '.ini', '.conf', '.config', '.sh', '.py', '.js', '.ts', '.html', '.css', '.makefile'];
|
|
745
|
-
const scan = (currentPath, depth) => {
|
|
746
|
-
if (depth > 1)
|
|
747
|
-
return; // Top level (0) and 1st level subfolders (1)
|
|
748
|
-
try {
|
|
749
|
-
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
750
|
-
for (const entry of entries) {
|
|
751
|
-
const fullPath = path.join(currentPath, entry.name);
|
|
752
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
753
|
-
if (entry.isDirectory()) {
|
|
754
|
-
if (shouldIgnore(entry.name, relativePath, ignorePatterns))
|
|
755
|
-
continue;
|
|
756
|
-
if (shouldExclude(currentPath, fullPath))
|
|
757
|
-
continue;
|
|
758
|
-
scan(fullPath, depth + 1);
|
|
759
|
-
}
|
|
760
|
-
else if (entry.isFile()) {
|
|
761
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
762
|
-
const isMakefile = entry.name.toLowerCase() === 'makefile';
|
|
763
|
-
if (textExtensions.includes(ext) || isMakefile || ext === '') {
|
|
764
|
-
try {
|
|
765
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
766
|
-
let match;
|
|
767
|
-
regex.lastIndex = 0;
|
|
768
|
-
while ((match = regex.exec(content)) !== null) {
|
|
769
|
-
variables.add(match[1]);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
catch (e) {
|
|
773
|
-
// Skip files that can't be read or aren't text
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
catch (e) {
|
|
780
|
-
// Ignore directory read errors
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
scan(dirPath, 0);
|
|
784
|
-
return Array.from(variables);
|
|
785
|
-
}
|