@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,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
|
-
import { loadConfig, saveConfig, getTemplateNames,
|
|
4
|
+
import { loadConfig, saveConfig, getTemplateNames, shouldExcludeFile, getDefaultPostConfig } from '../config.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { downloadAndExtract } from '../remote.js';
|
|
7
|
+
import { extractStructure, findVariablesInFiles, isExecutable, parseInfoFile, loadJsonTemplateConfig, getRootEntries, parsePostConfigScript, mergePostConfigTasks, mergePostCopyFiles, buildCopyFiles, promptTemplateName, promptDescription, promptAdditionalVariables, promptNewVariables, promptGlobalVariables, printNoNewVariables, promptNewFolders, printNoNewFolders, printNewFiles, promptNewFiles, promptRootFiles, promptStructureFolders, promptCopyFolders, promptPostConfigTasks } from './template-utils.js';
|
|
7
8
|
export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
8
9
|
let resolvedPath;
|
|
9
10
|
// Phase 1: Remote Check
|
|
@@ -30,51 +31,34 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
30
31
|
const isUpdate = !!updateTemplate;
|
|
31
32
|
const config = loadConfig();
|
|
32
33
|
const existingNames = getTemplateNames(config);
|
|
33
|
-
// Check for template configuration JSON file
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!options.json)
|
|
45
|
-
console.log(chalk.cyan(`Auto-detected template configurations from ${path.basename(jPath)}`));
|
|
34
|
+
// Check for template configuration JSON file
|
|
35
|
+
const fileTemplateConfig = loadJsonTemplateConfig(resolvedPath);
|
|
36
|
+
if (fileTemplateConfig && !options.json) {
|
|
37
|
+
// Find which file was loaded
|
|
38
|
+
const jsonConfigPaths = [
|
|
39
|
+
path.join(resolvedPath, '.pt-template.json'),
|
|
40
|
+
path.join(resolvedPath, 'template.json')
|
|
41
|
+
];
|
|
42
|
+
for (const jPath of jsonConfigPaths) {
|
|
43
|
+
if (fs.existsSync(jPath)) {
|
|
44
|
+
console.log(chalk.cyan(`Auto-detected template configurations from ${path.basename(jPath)}`));
|
|
46
45
|
break;
|
|
47
46
|
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
console.warn(chalk.yellow(`Warning: Failed to parse ${path.basename(jPath)}: ${e.message}`));
|
|
50
|
-
}
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
49
|
// Check for .info.md
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const infoPath = path.join(resolvedPath, '.info.md');
|
|
57
|
-
if (fs.existsSync(infoPath)) {
|
|
58
|
-
const infoContent = fs.readFileSync(infoPath, 'utf-8');
|
|
59
|
-
const lines = infoContent.split('\n');
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
if (line.startsWith('# ')) {
|
|
62
|
-
infoName = line.substring(2).trim();
|
|
63
|
-
}
|
|
64
|
-
else if (line.trim() !== '' && !infoDesc && !line.startsWith('#')) {
|
|
65
|
-
infoDesc = line.trim();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
50
|
+
const { name: infoName, description: infoDesc } = parseInfoFile(path.join(resolvedPath, '.info.md'));
|
|
51
|
+
// --- NAME ---
|
|
69
52
|
let targetName = updateTemplate || '';
|
|
70
53
|
if (isUpdate) {
|
|
54
|
+
// When updating, use the provided template name directly
|
|
55
|
+
// No auto-detection from files needed for updates
|
|
71
56
|
if (!targetName || !config.templates[targetName]) {
|
|
72
57
|
console.error(chalk.red(`Template "${targetName}" not found.`));
|
|
73
58
|
process.exit(1);
|
|
74
59
|
}
|
|
75
60
|
}
|
|
76
61
|
else {
|
|
77
|
-
// Track whether name came from .info.md or JSON (not user-provided)
|
|
78
62
|
const nameFromSource = !options.name && (fileTemplateConfig.name || infoName);
|
|
79
63
|
if (options.name) {
|
|
80
64
|
targetName = options.name;
|
|
@@ -89,50 +73,24 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
89
73
|
if (!options.json)
|
|
90
74
|
console.log(chalk.cyan(`Auto-detected template name from .info.md: ${targetName}`));
|
|
91
75
|
}
|
|
92
|
-
else {
|
|
93
|
-
|
|
94
|
-
targetName = path.basename(resolvedPath);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const { newName } = await inquirer.prompt({
|
|
98
|
-
type: 'input',
|
|
99
|
-
name: 'newName',
|
|
100
|
-
message: 'Name this template:',
|
|
101
|
-
default: path.basename(resolvedPath)
|
|
102
|
-
});
|
|
103
|
-
targetName = newName;
|
|
104
|
-
}
|
|
76
|
+
else if (options.yes || options.json) {
|
|
77
|
+
targetName = path.basename(resolvedPath);
|
|
105
78
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
const { confirmName } = await inquirer.prompt({
|
|
118
|
-
type: 'confirm',
|
|
119
|
-
name: 'confirmName',
|
|
120
|
-
message: `Use "${targetName}" as the template name?`,
|
|
121
|
-
default: true
|
|
122
|
-
});
|
|
123
|
-
if (!confirmName) {
|
|
124
|
-
const { newName } = await inquirer.prompt({
|
|
125
|
-
type: 'input',
|
|
126
|
-
name: 'newName',
|
|
127
|
-
message: 'Name this template:',
|
|
128
|
-
default: targetName
|
|
129
|
-
});
|
|
130
|
-
targetName = newName;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
79
|
+
else {
|
|
80
|
+
const { newName } = await inquirer.prompt({
|
|
81
|
+
type: 'input',
|
|
82
|
+
name: 'newName',
|
|
83
|
+
message: 'Name this template:',
|
|
84
|
+
default: path.basename(resolvedPath)
|
|
85
|
+
});
|
|
86
|
+
targetName = newName;
|
|
133
87
|
}
|
|
88
|
+
// Confirm name if auto-detected
|
|
89
|
+
targetName = await promptTemplateName(targetName, existingNames, nameFromSource ? targetName : null, options);
|
|
134
90
|
}
|
|
91
|
+
// --- DESCRIPTION ---
|
|
135
92
|
let description = '';
|
|
93
|
+
const descFromSource = !options.desc && (fileTemplateConfig.description || infoDesc);
|
|
136
94
|
if (options.desc) {
|
|
137
95
|
description = options.desc;
|
|
138
96
|
}
|
|
@@ -152,19 +110,11 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
152
110
|
description = currentDesc;
|
|
153
111
|
}
|
|
154
112
|
else {
|
|
155
|
-
|
|
156
|
-
type: 'input',
|
|
157
|
-
name: 'newDesc',
|
|
158
|
-
message: 'Purpose/Description of this template:',
|
|
159
|
-
default: currentDesc
|
|
160
|
-
});
|
|
161
|
-
description = newDesc;
|
|
113
|
+
description = await promptDescription(currentDesc, options);
|
|
162
114
|
}
|
|
163
115
|
}
|
|
164
116
|
}
|
|
165
117
|
else {
|
|
166
|
-
// Track whether description came from .info.md or JSON (not user-provided)
|
|
167
|
-
const descFromSource = !options.desc && (fileTemplateConfig.description || infoDesc);
|
|
168
118
|
if (fileTemplateConfig.description) {
|
|
169
119
|
description = fileTemplateConfig.description;
|
|
170
120
|
if (!options.json)
|
|
@@ -175,29 +125,12 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
175
125
|
if (!options.json)
|
|
176
126
|
console.log(chalk.cyan(`Auto-detected template description from .info.md: ${description}`));
|
|
177
127
|
}
|
|
178
|
-
else if (options.yes || options.json) {
|
|
179
|
-
description = infoDesc || '';
|
|
180
|
-
}
|
|
181
128
|
else {
|
|
182
|
-
|
|
183
|
-
type: 'input',
|
|
184
|
-
name: 'newDesc',
|
|
185
|
-
message: 'Purpose/Description of this template:',
|
|
186
|
-
default: targetName
|
|
187
|
-
});
|
|
188
|
-
description = newDesc;
|
|
129
|
+
description = await promptDescription(infoDesc || targetName, options);
|
|
189
130
|
}
|
|
190
|
-
//
|
|
191
|
-
// This prevents accidental overwrites when creating new templates based on existing ones
|
|
131
|
+
// Confirm description if auto-detected
|
|
192
132
|
if (descFromSource && !options.json) {
|
|
193
|
-
if (options.yes) {
|
|
194
|
-
// In --yes mode, keep the auto-detected description but warn
|
|
195
|
-
const existingNames = getTemplateNames(config);
|
|
196
|
-
if (existingNames.includes(targetName)) {
|
|
197
|
-
console.warn(chalk.yellow(`⚠ Warning: "${targetName}" already exists. Using this name will overwrite the existing template.`));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
133
|
+
if (!options.yes) {
|
|
201
134
|
const { confirmDesc } = await inquirer.prompt({
|
|
202
135
|
type: 'confirm',
|
|
203
136
|
name: 'confirmDesc',
|
|
@@ -205,26 +138,20 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
205
138
|
default: true
|
|
206
139
|
});
|
|
207
140
|
if (!confirmDesc) {
|
|
208
|
-
|
|
209
|
-
type: 'input',
|
|
210
|
-
name: 'newDesc',
|
|
211
|
-
message: 'Purpose/Description of this template:',
|
|
212
|
-
default: description
|
|
213
|
-
});
|
|
214
|
-
description = newDesc;
|
|
141
|
+
description = await promptDescription(description, options);
|
|
215
142
|
}
|
|
216
143
|
}
|
|
217
144
|
}
|
|
218
145
|
}
|
|
146
|
+
// --- IGNORE PATTERNS ---
|
|
219
147
|
const cliIgnore = options.ignore ? options.ignore.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
220
148
|
const ignorePatterns = [...(config.ignore || []), ...cliIgnore];
|
|
221
|
-
//
|
|
149
|
+
// --- VARIABLES ---
|
|
222
150
|
const detectedVars = findVariablesInFiles(resolvedPath, resolvedPath, ignorePatterns);
|
|
223
151
|
if (detectedVars.length > 0 && !options.json) {
|
|
224
152
|
console.log(chalk.cyan(`Auto-detected ${detectedVars.length} variable(s): ${detectedVars.join(', ')}`));
|
|
225
153
|
}
|
|
226
154
|
let variables = [];
|
|
227
|
-
// During updates, merge existing template variables with JSON file variables
|
|
228
155
|
if (isUpdate) {
|
|
229
156
|
// Start with existing template variables
|
|
230
157
|
if (config.templates[updateTemplate].variables) {
|
|
@@ -235,11 +162,9 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
235
162
|
for (const v of fileTemplateConfig.variables) {
|
|
236
163
|
const existingIndex = variables.findIndex(existing => existing.name === v.name);
|
|
237
164
|
if (existingIndex !== -1) {
|
|
238
|
-
// Update existing variable with JSON values (but preserve other fields)
|
|
239
165
|
variables[existingIndex] = { ...variables[existingIndex], ...v };
|
|
240
166
|
}
|
|
241
167
|
else {
|
|
242
|
-
// Add new variable
|
|
243
168
|
variables.push({ ...v });
|
|
244
169
|
}
|
|
245
170
|
}
|
|
@@ -254,11 +179,7 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
254
179
|
// Add detected variables if not already present
|
|
255
180
|
for (const varName of detectedVars) {
|
|
256
181
|
if (!variables.some(v => v.name === varName)) {
|
|
257
|
-
variables.push({
|
|
258
|
-
name: varName,
|
|
259
|
-
prompt: `Enter ${varName}:`,
|
|
260
|
-
required: true
|
|
261
|
-
});
|
|
182
|
+
variables.push({ name: varName, prompt: `Enter ${varName}:`, required: true });
|
|
262
183
|
}
|
|
263
184
|
}
|
|
264
185
|
// Include global variables as suggestions
|
|
@@ -269,137 +190,83 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
269
190
|
}
|
|
270
191
|
}
|
|
271
192
|
}
|
|
272
|
-
|
|
273
|
-
if (!options.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const response = await inquirer.prompt({
|
|
278
|
-
type: 'confirm',
|
|
279
|
-
name: 'hasMoreVariables',
|
|
280
|
-
message: message,
|
|
281
|
-
default: false
|
|
282
|
-
});
|
|
283
|
-
hasMoreVariables = response.hasMoreVariables;
|
|
193
|
+
// Variable prompting
|
|
194
|
+
if (!isUpdate || options.noDiff === undefined) {
|
|
195
|
+
// New template or full mode - prompt for additional variables
|
|
196
|
+
const additionalVars = await promptAdditionalVariables(variables, options);
|
|
197
|
+
variables.push(...additionalVars);
|
|
284
198
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
199
|
+
else {
|
|
200
|
+
// Additive mode - only prompt for new variables
|
|
201
|
+
const existingVarNames = new Set(config.templates[updateTemplate].variables?.map(v => v.name) || []);
|
|
202
|
+
const newVars = variables.filter(v => !existingVarNames.has(v.name));
|
|
203
|
+
if (newVars.length > 0) {
|
|
204
|
+
const selectedNewVars = await promptNewVariables(newVars, options);
|
|
205
|
+
variables.push(...selectedNewVars);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
printNoNewVariables();
|
|
209
|
+
}
|
|
210
|
+
// Also prompt for global variables not already included
|
|
211
|
+
const globalVarsToPrompt = (config.variables || []).filter(gv => !variables.some(v => v.name === gv.name));
|
|
212
|
+
if (globalVarsToPrompt.length > 0) {
|
|
213
|
+
const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
|
|
214
|
+
variables.push(...selectedGlobals);
|
|
302
215
|
}
|
|
303
216
|
}
|
|
304
|
-
//
|
|
305
|
-
const
|
|
217
|
+
// --- STRUCTURE ---
|
|
218
|
+
const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
306
219
|
? fileTemplateConfig.folders
|
|
307
220
|
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
308
|
-
//
|
|
309
|
-
const
|
|
310
|
-
.filter(e => !shouldExclude(resolvedPath, path.join(resolvedPath, e.name), ignorePatterns))
|
|
311
|
-
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
312
|
-
const rootFiles = rootEntries.filter(e => e.isFile()).map(e => e.name);
|
|
313
|
-
const rootDirs = rootEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
221
|
+
// --- CONTENT SELECTION (Root only) ---
|
|
222
|
+
const { files: rootFiles, dirs: rootDirs } = getRootEntries(resolvedPath, ignorePatterns);
|
|
314
223
|
let selectedFiles = [];
|
|
315
224
|
let selectedFolders = [];
|
|
316
225
|
let selectedStructure = [];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
name: 'selectedFiles',
|
|
328
|
-
message: 'Select root files to include as boilerplate:',
|
|
329
|
-
loop: false,
|
|
330
|
-
theme: {
|
|
331
|
-
icon: {
|
|
332
|
-
checked: chalk.green('[x] '),
|
|
333
|
-
unchecked: '[ ] ',
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
choices: rootFiles.map(f => ({
|
|
337
|
-
name: f,
|
|
338
|
-
checked: ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'].some(p => f.toLowerCase() === p.toLowerCase())
|
|
339
|
-
}))
|
|
340
|
-
});
|
|
341
|
-
selectedFiles = filesResponse.selectedFiles;
|
|
226
|
+
let folders;
|
|
227
|
+
if (isUpdate) {
|
|
228
|
+
// Additive mode for updates
|
|
229
|
+
const existingTemplate = config.templates[updateTemplate];
|
|
230
|
+
// New folders
|
|
231
|
+
const newFolders = detectedFolders.filter((f) => !existingTemplate.folders?.some((ef) => ef.name === f.name));
|
|
232
|
+
if (newFolders.length > 0) {
|
|
233
|
+
const addedFolders = await promptNewFolders(newFolders, options);
|
|
234
|
+
// Create new folders array preserving existing
|
|
235
|
+
folders = [...existingTemplate.folders, ...addedFolders];
|
|
342
236
|
}
|
|
343
237
|
else {
|
|
344
|
-
|
|
345
|
-
|
|
238
|
+
printNoNewFolders();
|
|
239
|
+
folders = existingTemplate.folders;
|
|
240
|
+
}
|
|
241
|
+
// New files
|
|
242
|
+
const newFiles = rootFiles.filter((f) => !existingTemplate.copy_files?.some((cf) => cf.src === f));
|
|
243
|
+
printNewFiles(newFiles.length, newFiles);
|
|
244
|
+
const addedFiles = await promptNewFiles(newFiles, options);
|
|
245
|
+
selectedFiles = [...(existingTemplate.copy_files?.filter((cf) => !rootDirs.includes(cf.src)).map((cf) => cf.src) || []), ...addedFiles];
|
|
246
|
+
// Structure
|
|
247
|
+
selectedStructure = existingTemplate.folders?.map((f) => f.name) || [];
|
|
248
|
+
// Seed selectedFolders from existing copy_files directory entries
|
|
249
|
+
selectedFolders = (existingTemplate.copy_files || [])
|
|
250
|
+
.filter((f) => rootDirs.includes(f.src))
|
|
251
|
+
.map((f) => f.src);
|
|
346
252
|
if (rootDirs.length > 0) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
name
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
theme: {
|
|
353
|
-
icon: {
|
|
354
|
-
checked: chalk.green('[x] '),
|
|
355
|
-
unchecked: '[ ] ',
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
choices: rootDirs.map(d => ({
|
|
359
|
-
name: d,
|
|
360
|
-
checked: true // Include all in structure by default
|
|
361
|
-
}))
|
|
362
|
-
});
|
|
363
|
-
selectedStructure = foldersResponse.selectedStructure;
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
selectedStructure = [];
|
|
253
|
+
const addedDirs = newFolders
|
|
254
|
+
.filter((f) => ['APP', 'scripts', 'bin'].some(p => f.name === p))
|
|
255
|
+
.map((f) => f.name);
|
|
256
|
+
selectedStructure = [...new Set([...selectedStructure, ...addedDirs])];
|
|
257
|
+
selectedFolders = [...new Set([...selectedFolders, ...addedDirs])];
|
|
367
258
|
}
|
|
368
|
-
if (selectedStructure.length > 0) {
|
|
369
|
-
const copyFoldersResponse = await inquirer.prompt({
|
|
370
|
-
type: 'checkbox',
|
|
371
|
-
name: 'selectedFolders',
|
|
372
|
-
message: 'Select folders to copy RECURSIVELY as boilerplate (with contents):',
|
|
373
|
-
loop: false,
|
|
374
|
-
theme: {
|
|
375
|
-
icon: {
|
|
376
|
-
checked: chalk.green('[x] '),
|
|
377
|
-
unchecked: '[ ] ',
|
|
378
|
-
}
|
|
379
|
-
},
|
|
380
|
-
choices: selectedStructure.map((d) => ({
|
|
381
|
-
name: d,
|
|
382
|
-
checked: ['APP', 'scripts', 'bin'].some(p => d === p)
|
|
383
|
-
}))
|
|
384
|
-
});
|
|
385
|
-
selectedFolders = copyFoldersResponse.selectedFolders;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
selectedFolders = [];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
const copy_files = [];
|
|
392
|
-
if (fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
|
|
393
|
-
copy_files.push(...fileTemplateConfig.copy_files);
|
|
394
259
|
}
|
|
395
260
|
else {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
261
|
+
// New template - original behavior
|
|
262
|
+
selectedFiles = await promptRootFiles(rootFiles, undefined, options);
|
|
263
|
+
selectedStructure = await promptStructureFolders(rootDirs, options);
|
|
264
|
+
selectedFolders = await promptCopyFolders(selectedStructure, undefined, options);
|
|
265
|
+
folders = detectedFolders;
|
|
266
|
+
}
|
|
267
|
+
// --- COPY FILES ---
|
|
268
|
+
const existingCopyFiles = isUpdate ? config.templates[updateTemplate].copy_files || [] : [];
|
|
269
|
+
const copy_files = buildCopyFiles(selectedFiles, selectedFolders, existingCopyFiles);
|
|
403
270
|
const templateConfig = {
|
|
404
271
|
description: description,
|
|
405
272
|
templateRoot: resolvedPath,
|
|
@@ -407,41 +274,12 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
407
274
|
copy_files: copy_files,
|
|
408
275
|
variables: variables.length > 0 ? variables : undefined
|
|
409
276
|
};
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const shPath = path.join(resolvedPath, 'post_config.sh');
|
|
417
|
-
const batPath = path.join(resolvedPath, 'post_config.bat');
|
|
418
|
-
if (fs.existsSync(shPath)) {
|
|
419
|
-
const lines = fs.readFileSync(shPath, 'utf-8').split('\n');
|
|
420
|
-
let currentDesc = '';
|
|
421
|
-
for (const line of lines) {
|
|
422
|
-
if (line.startsWith('echo "Running: ')) {
|
|
423
|
-
currentDesc = line.substring(15, line.length - 1).replace(/"$/, '');
|
|
424
|
-
}
|
|
425
|
-
else if (line.trim() && !line.startsWith('#') && !line.startsWith('echo ')) {
|
|
426
|
-
postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
|
|
427
|
-
currentDesc = '';
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else if (fs.existsSync(batPath)) {
|
|
432
|
-
const lines = fs.readFileSync(batPath, 'utf-8').split('\n');
|
|
433
|
-
let currentDesc = '';
|
|
434
|
-
for (const line of lines) {
|
|
435
|
-
if (line.startsWith('echo Running: ')) {
|
|
436
|
-
currentDesc = line.substring(14).trim();
|
|
437
|
-
}
|
|
438
|
-
else if (line.trim() && !line.startsWith('::') && !line.startsWith('@echo') && !line.startsWith('echo ')) {
|
|
439
|
-
postConfigTasks.push({ command: line.trim(), description: currentDesc || line.trim() });
|
|
440
|
-
currentDesc = '';
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
277
|
+
// --- POST_CONFIG ---
|
|
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 = isUpdate ? config.templates[updateTemplate].post_config || [] : [];
|
|
282
|
+
postConfigTasks = mergePostConfigTasks(postConfigTasks, fileTemplateConfig.post_config, detectedTasks);
|
|
445
283
|
if (postConfigTasks.length > 0) {
|
|
446
284
|
templateConfig.post_config = postConfigTasks;
|
|
447
285
|
if (!options.json && !fileTemplateConfig.post_config)
|
|
@@ -451,36 +289,7 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
451
289
|
const defaultPostConfig = getDefaultPostConfig(config);
|
|
452
290
|
const defaultApplicableTasks = defaultPostConfig.filter(t => !t.type || t.type === targetName);
|
|
453
291
|
if (defaultApplicableTasks.length > 0) {
|
|
454
|
-
|
|
455
|
-
if (options.yes || options.json) {
|
|
456
|
-
selectedTaskNames = defaultApplicableTasks.map(t => t.command || t.script || '');
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
const choices = [];
|
|
460
|
-
for (const t of defaultApplicableTasks) {
|
|
461
|
-
const cmd = t.command || t.script || '(no command)';
|
|
462
|
-
const desc = t.description ? ` (${t.description})` : '';
|
|
463
|
-
choices.push({
|
|
464
|
-
name: `${cmd}${desc}`,
|
|
465
|
-
value: cmd,
|
|
466
|
-
checked: t.checked !== false
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
const response = await inquirer.prompt({
|
|
470
|
-
type: 'checkbox',
|
|
471
|
-
name: 'selected',
|
|
472
|
-
message: 'Select default post-config tasks to include in this template:',
|
|
473
|
-
loop: false,
|
|
474
|
-
theme: {
|
|
475
|
-
icon: {
|
|
476
|
-
checked: chalk.green('[x] '),
|
|
477
|
-
unchecked: '[ ] ',
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
choices
|
|
481
|
-
});
|
|
482
|
-
selectedTaskNames = response.selected || [];
|
|
483
|
-
}
|
|
292
|
+
const selectedTaskNames = await promptPostConfigTasks(defaultApplicableTasks, options);
|
|
484
293
|
if (selectedTaskNames.length > 0) {
|
|
485
294
|
if (!templateConfig.post_config)
|
|
486
295
|
templateConfig.post_config = [];
|
|
@@ -495,48 +304,20 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
495
304
|
}
|
|
496
305
|
}
|
|
497
306
|
}
|
|
498
|
-
//
|
|
499
|
-
const detectedExecutables =
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
templateConfig.post_copy = fileTemplateConfig.post_copy;
|
|
508
|
-
const postCopySrcs = fileTemplateConfig.post_copy.map(f => f.src);
|
|
307
|
+
// --- POST_COPY (executables) ---
|
|
308
|
+
const detectedExecutables = rootFiles
|
|
309
|
+
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
310
|
+
.filter(file => !shouldExcludeFile(file));
|
|
311
|
+
const existingPostCopy = isUpdate ? config.templates[updateTemplate].post_copy || [] : [];
|
|
312
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, detectedExecutables);
|
|
313
|
+
if (post_copy.length > 0) {
|
|
314
|
+
templateConfig.post_copy = post_copy;
|
|
315
|
+
const postCopySrcs = post_copy.map(f => f.src);
|
|
509
316
|
templateConfig.copy_files = templateConfig.copy_files?.filter(cf => !postCopySrcs.includes(cf.src));
|
|
510
317
|
}
|
|
511
|
-
|
|
512
|
-
if (!options.json) {
|
|
513
|
-
console.log(chalk.cyan("\nAuto-detected " + detectedExecutables.length + " executable file(s) at root:"));
|
|
514
|
-
for (const file of detectedExecutables) {
|
|
515
|
-
console.log(chalk.gray(" - " + file));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
let addPostCopy = true;
|
|
519
|
-
if (!options.yes && !options.json) {
|
|
520
|
-
const response = await inquirer.prompt({
|
|
521
|
-
type: 'confirm',
|
|
522
|
-
name: 'addPostCopy',
|
|
523
|
-
message: 'Add these to post_copy (auto-chmod)?',
|
|
524
|
-
default: true
|
|
525
|
-
});
|
|
526
|
-
addPostCopy = response.addPostCopy;
|
|
527
|
-
}
|
|
528
|
-
if (addPostCopy) {
|
|
529
|
-
templateConfig.post_copy = detectedExecutables.map(f => ({ src: f, dest: f }));
|
|
530
|
-
templateConfig.copy_files = templateConfig.copy_files?.filter(cf => !detectedExecutables.includes(cf.src));
|
|
531
|
-
}
|
|
532
|
-
}
|
|
318
|
+
// --- OUTPUT ---
|
|
533
319
|
if (options.json) {
|
|
534
|
-
const output = {
|
|
535
|
-
name: targetName,
|
|
536
|
-
...templateConfig
|
|
537
|
-
};
|
|
538
|
-
// Force the application to wait until every single byte of this JSON string
|
|
539
|
-
// safely clears the operating system's pipe buffer before letting the process die.
|
|
320
|
+
const output = { name: targetName, ...templateConfig };
|
|
540
321
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n', () => {
|
|
541
322
|
process.exit(0);
|
|
542
323
|
});
|
|
@@ -546,96 +327,3 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
546
327
|
saveConfig(config);
|
|
547
328
|
console.log(chalk.green(`\n✓ Template saved as "${targetName}"`));
|
|
548
329
|
}
|
|
549
|
-
function isExecutable(fullPath, fileName) {
|
|
550
|
-
if (shouldExcludeFile(fileName))
|
|
551
|
-
return false;
|
|
552
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
553
|
-
if (['.sh', '.py', '.bash', '.bat', '.cmd'].includes(ext))
|
|
554
|
-
return true;
|
|
555
|
-
if (fileName.toLowerCase() === 'makefile')
|
|
556
|
-
return true;
|
|
557
|
-
try {
|
|
558
|
-
const stat = fs.statSync(fullPath);
|
|
559
|
-
return !!(stat.mode & 0o111);
|
|
560
|
-
}
|
|
561
|
-
catch {
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function extractStructure(dirPath, rootPath, ignorePatterns) {
|
|
566
|
-
let nodes = [];
|
|
567
|
-
try {
|
|
568
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
569
|
-
for (const entry of entries) {
|
|
570
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
571
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
572
|
-
// Only include directories in the structure skeleton
|
|
573
|
-
const isDirectory = entry.isDirectory() || (entry.isSymbolicLink() && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory());
|
|
574
|
-
if (!isDirectory)
|
|
575
|
-
continue;
|
|
576
|
-
if (shouldIgnore(entry.name, relativePath, ignorePatterns))
|
|
577
|
-
continue;
|
|
578
|
-
if (shouldExclude(dirPath, fullPath))
|
|
579
|
-
continue;
|
|
580
|
-
const children = extractStructure(fullPath, rootPath, ignorePatterns);
|
|
581
|
-
let info = "";
|
|
582
|
-
const gitkeepPath = path.join(fullPath, '.gitkeep.md');
|
|
583
|
-
const infoPath = path.join(fullPath, '.info.md');
|
|
584
|
-
if (fs.existsSync(infoPath))
|
|
585
|
-
info = fs.readFileSync(infoPath, 'utf-8').trim();
|
|
586
|
-
else if (fs.existsSync(gitkeepPath))
|
|
587
|
-
info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
|
|
588
|
-
nodes.push({ name: entry.name, info: info, children: children });
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
catch (e) { }
|
|
592
|
-
return nodes;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Scan text files in top-level and 1st-level subdirectories for {{ variable_name }} placeholders.
|
|
596
|
-
*/
|
|
597
|
-
function findVariablesInFiles(dirPath, rootPath, ignorePatterns) {
|
|
598
|
-
const variables = new Set();
|
|
599
|
-
const regex = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
600
|
-
const textExtensions = ['.md', '.txt', '.makerc', '.json', '.yaml', '.yml', '.ini', '.conf', '.config', '.sh', '.py', '.js', '.ts', '.html', '.css', '.makefile'];
|
|
601
|
-
const scan = (currentPath, depth) => {
|
|
602
|
-
if (depth > 1)
|
|
603
|
-
return; // Top level (0) and 1st level subfolders (1)
|
|
604
|
-
try {
|
|
605
|
-
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
606
|
-
for (const entry of entries) {
|
|
607
|
-
const fullPath = path.join(currentPath, entry.name);
|
|
608
|
-
const relativePath = path.relative(rootPath, fullPath);
|
|
609
|
-
if (entry.isDirectory()) {
|
|
610
|
-
if (shouldIgnore(entry.name, relativePath, ignorePatterns))
|
|
611
|
-
continue;
|
|
612
|
-
if (shouldExclude(currentPath, fullPath))
|
|
613
|
-
continue;
|
|
614
|
-
scan(fullPath, depth + 1);
|
|
615
|
-
}
|
|
616
|
-
else if (entry.isFile()) {
|
|
617
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
618
|
-
const isMakefile = entry.name.toLowerCase() === 'makefile';
|
|
619
|
-
if (textExtensions.includes(ext) || isMakefile || ext === '') {
|
|
620
|
-
try {
|
|
621
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
622
|
-
let match;
|
|
623
|
-
regex.lastIndex = 0;
|
|
624
|
-
while ((match = regex.exec(content)) !== null) {
|
|
625
|
-
variables.add(match[1]);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
catch (e) {
|
|
629
|
-
// Skip files that can't be read or aren't text
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
catch (e) {
|
|
636
|
-
// Ignore directory read errors
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
scan(dirPath, 0);
|
|
640
|
-
return Array.from(variables);
|
|
641
|
-
}
|