@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.
- 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 +94 -547
- package/dist/config.js +2 -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 +146 -582
- package/src/config.ts +2 -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,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
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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;
|
|
430
|
-
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));
|
|
431
236
|
}
|
|
432
237
|
else {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
508
|
-
|
|
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 (
|
|
252
|
+
if (existingSrcs.has(f))
|
|
514
253
|
continue;
|
|
515
|
-
|
|
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 (
|
|
257
|
+
if (existingSrcs.has(d))
|
|
525
258
|
continue;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
}
|