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