@garyr/pt-cli 1.1.0 → 1.1.1
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/README.md +21 -15
- package/dist/commands/completionCommand.js +0 -12
- package/dist/commands/learnCommand.js +28 -7
- package/dist/commands/template-utils.js +17 -7
- package/dist/commands/updateCommand.js +56 -25
- package/dist/config.js +3 -0
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/src/commands/completionCommand.ts +0 -11
- package/src/commands/learnCommand.ts +29 -7
- package/src/commands/template-utils.ts +17 -8
- package/src/commands/updateCommand.ts +125 -91
- package/src/config.ts +3 -0
- package/src/index.ts +1 -1
- package/tests/config-utils.test.ts +1 -1
- package/tests/learn.test.ts +5 -4
- package/tests/update.test.ts +63 -9
package/README.md
CHANGED
|
@@ -32,21 +32,27 @@ graph LR
|
|
|
32
32
|
<!-- TOC -->
|
|
33
33
|
|
|
34
34
|
- [pt - Project Template CLI](#pt---project-template-cli)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
- [Why pt-cli?](#why-pt-cli)
|
|
36
|
+
- [Core Benefits \& Uses](#core-benefits--uses)
|
|
37
|
+
- [🚀 Low-Friction Templating](#-low-friction-templating)
|
|
38
|
+
- [🧠 Reduces Cognitive Load](#-reduces-cognitive-load)
|
|
39
|
+
- [📦 Sharing is Caring](#-sharing-is-caring)
|
|
40
|
+
- [🤖 Agentic and API Friendly](#-agentic-and-api-friendly)
|
|
41
|
+
- [Features at a Glance](#features-at-a-glance)
|
|
42
|
+
- [Quick Start](#quick-start)
|
|
43
|
+
- [Installation](#installation)
|
|
44
|
+
- [Basic Commands](#basic-commands)
|
|
45
|
+
- [Shell Completions](#shell-completions)
|
|
46
|
+
- [Agent Integration](#agent-integration)
|
|
47
|
+
- [Documentation](#documentation)
|
|
48
|
+
- [Development](#development)
|
|
49
|
+
- [Where are the Templates?](#where-are-the-templates)
|
|
50
|
+
- [1.0 Release \& API Stability](#10-release--api-stability)
|
|
51
|
+
- [🔒 Stability Guarantee (1.x series)](#-stability-guarantee-1x-series)
|
|
52
|
+
- [📦 Versioning Policy](#-versioning-policy)
|
|
53
|
+
- [📋 What's Locked in 1.0](#-whats-locked-in-10)
|
|
54
|
+
- [📖 Migration from 0.x to 1.0](#-migration-from-0x-to-10)
|
|
55
|
+
- [Documentation](#documentation-1)
|
|
50
56
|
|
|
51
57
|
<!-- /TOC -->
|
|
52
58
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
2
|
import YAML from 'yaml';
|
|
5
3
|
import { getConfigPath } from '../config.js';
|
|
6
4
|
export function getTemplatesForCompletion() {
|
|
@@ -396,13 +394,3 @@ export async function completionCommand(shellArg, options) {
|
|
|
396
394
|
process.exit(1);
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
|
-
// Allow direct execution via tsx src/commands/completionCommand.ts <shell>
|
|
400
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
401
|
-
const shell = process.argv[2];
|
|
402
|
-
if (shell === '--templates' || shell === '_templates') {
|
|
403
|
-
completionCommand(shell, { templates: true });
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
completionCommand(shell);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
@@ -238,13 +238,19 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
238
238
|
printNoNewFolders();
|
|
239
239
|
folders = existingTemplate.folders;
|
|
240
240
|
}
|
|
241
|
+
// selectedStructure should include ALL folders (existing + added) in additive mode
|
|
242
|
+
// so that the final filter doesn't drop user-selected new folders
|
|
243
|
+
selectedStructure = [
|
|
244
|
+
...new Set([
|
|
245
|
+
...existingTemplate.folders?.map((f) => f.name) || [],
|
|
246
|
+
...folders.filter(f => !existingTemplate.folders?.some(ef => ef.name === f.name)).map(f => f.name),
|
|
247
|
+
]),
|
|
248
|
+
];
|
|
241
249
|
// New files
|
|
242
250
|
const newFiles = rootFiles.filter((f) => !existingTemplate.copy_files?.some((cf) => cf.src === f));
|
|
243
251
|
printNewFiles(newFiles.length, newFiles);
|
|
244
252
|
const addedFiles = await promptNewFiles(newFiles, options);
|
|
245
253
|
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
254
|
// Seed selectedFolders from existing copy_files directory entries
|
|
249
255
|
selectedFolders = (existingTemplate.copy_files || [])
|
|
250
256
|
.filter((f) => rootDirs.includes(f.src))
|
|
@@ -266,7 +272,14 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
266
272
|
}
|
|
267
273
|
// --- COPY FILES ---
|
|
268
274
|
const existingCopyFiles = isUpdate ? config.templates[updateTemplate].copy_files || [] : [];
|
|
269
|
-
|
|
275
|
+
// If JSON template config has copy_files, use those directly (for new templates)
|
|
276
|
+
let copy_files;
|
|
277
|
+
if (!isUpdate && fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
|
|
278
|
+
copy_files = [...fileTemplateConfig.copy_files];
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
copy_files = buildCopyFiles(selectedFiles, selectedFolders, existingCopyFiles);
|
|
282
|
+
}
|
|
270
283
|
const templateConfig = {
|
|
271
284
|
description: description,
|
|
272
285
|
templateRoot: resolvedPath,
|
|
@@ -308,8 +321,18 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
308
321
|
const detectedExecutables = rootFiles
|
|
309
322
|
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
310
323
|
.filter(file => !shouldExcludeFile(file));
|
|
324
|
+
// In additive mode (isUpdate), only add executables that were explicitly selected by the user
|
|
325
|
+
let selectedExecutables = [];
|
|
326
|
+
if (isUpdate) {
|
|
327
|
+
// In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
|
|
328
|
+
selectedExecutables = detectedExecutables.filter(exec => selectedFiles.includes(exec));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// In new template mode, include all detected executables (original behavior)
|
|
332
|
+
selectedExecutables = detectedExecutables;
|
|
333
|
+
}
|
|
311
334
|
const existingPostCopy = isUpdate ? config.templates[updateTemplate].post_copy || [] : [];
|
|
312
|
-
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy,
|
|
335
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
|
|
313
336
|
if (post_copy.length > 0) {
|
|
314
337
|
templateConfig.post_copy = post_copy;
|
|
315
338
|
const postCopySrcs = post_copy.map(f => f.src);
|
|
@@ -318,9 +341,7 @@ export async function learn(sourcePath, updateTemplate = null, options = {}) {
|
|
|
318
341
|
// --- OUTPUT ---
|
|
319
342
|
if (options.json) {
|
|
320
343
|
const output = { name: targetName, ...templateConfig };
|
|
321
|
-
process.stdout.write(JSON.stringify(output, null, 2) + '\n'
|
|
322
|
-
process.exit(0);
|
|
323
|
-
});
|
|
344
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
324
345
|
return;
|
|
325
346
|
}
|
|
326
347
|
config.templates[targetName] = templateConfig;
|
|
@@ -153,7 +153,9 @@ export function getRootEntries(dirPath, ignorePatterns) {
|
|
|
153
153
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
154
154
|
.filter(e => !shouldExclude(dirPath, path.join(dirPath, e.name), ignorePatterns))
|
|
155
155
|
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
156
|
-
const files = entries.filter(e => e.isFile())
|
|
156
|
+
const files = entries.filter(e => e.isFile())
|
|
157
|
+
.map(e => e.name)
|
|
158
|
+
.filter(fileName => !shouldExcludeFile(fileName));
|
|
157
159
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
158
160
|
return { files, dirs };
|
|
159
161
|
}
|
|
@@ -204,12 +206,21 @@ export function mergePostConfigTasks(existingTasks, jsonTasks, detectedTasks) {
|
|
|
204
206
|
if (jsonTasks && Array.isArray(jsonTasks)) {
|
|
205
207
|
return [...jsonTasks];
|
|
206
208
|
}
|
|
207
|
-
|
|
209
|
+
// Merge detected tasks with existing, avoiding duplicates
|
|
210
|
+
const merged = [...existingTasks];
|
|
211
|
+
for (const dt of detectedTasks) {
|
|
212
|
+
const exists = merged.some(et => et.command === dt.command && et.script === dt.script);
|
|
213
|
+
if (!exists) {
|
|
214
|
+
merged.push(dt);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return merged;
|
|
208
218
|
}
|
|
209
219
|
/**
|
|
210
220
|
* Merge post_copy files from existing, JSON file, and detected executables
|
|
221
|
+
* Only adds executables that were explicitly selected by the user
|
|
211
222
|
*/
|
|
212
|
-
export function mergePostCopyFiles(existingPostCopy, jsonPostCopy,
|
|
223
|
+
export function mergePostCopyFiles(existingPostCopy, jsonPostCopy, selectedExecutables) {
|
|
213
224
|
let post_copy = [...existingPostCopy];
|
|
214
225
|
if (jsonPostCopy && Array.isArray(jsonPostCopy)) {
|
|
215
226
|
for (const pc of jsonPostCopy) {
|
|
@@ -218,10 +229,9 @@ export function mergePostCopyFiles(existingPostCopy, jsonPostCopy, detectedExecu
|
|
|
218
229
|
}
|
|
219
230
|
}
|
|
220
231
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
for (const file of newExecutables) {
|
|
232
|
+
// Only add executables that were explicitly selected by the user
|
|
233
|
+
for (const file of selectedExecutables) {
|
|
234
|
+
if (!post_copy.some(existing => existing.src === file)) {
|
|
225
235
|
post_copy.push({ src: file, dest: file });
|
|
226
236
|
}
|
|
227
237
|
}
|
|
@@ -4,7 +4,7 @@ import inquirer from 'inquirer';
|
|
|
4
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
|
-
import { extractStructure, findVariablesInFiles, isExecutable, parseInfoFile, loadJsonTemplateConfig, parsePostConfigScript, mergePostConfigTasks, mergePostCopyFiles,
|
|
7
|
+
import { extractStructure, findVariablesInFiles, isExecutable, parseInfoFile, loadJsonTemplateConfig, parsePostConfigScript, mergePostConfigTasks, mergePostCopyFiles, promptNewVariables, promptGlobalVariables, printNoNewVariables, promptNewFolders, printNoNewFolders, printNewFiles, promptNewFiles, promptRootFiles, promptStructureFolders, promptCopyFolders, promptPostConfigTasks } from './template-utils.js';
|
|
8
8
|
export async function update(sourcePath, templateName, options = {}) {
|
|
9
9
|
const isFullMode = options.noDiff;
|
|
10
10
|
let resolvedPath;
|
|
@@ -125,11 +125,13 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
125
125
|
if (!isFullMode) {
|
|
126
126
|
// Additive mode: only present new variables for selection
|
|
127
127
|
const existingVarNames = new Set(config.templates[templateName].variables?.map(v => v.name) || []);
|
|
128
|
+
// newVars are NOT added to variables yet - only existing variables + JSON variables are in variables
|
|
128
129
|
const newVars = variables.filter(v => !existingVarNames.has(v.name));
|
|
129
130
|
if (newVars.length > 0) {
|
|
130
131
|
console.log(chalk.cyan(`\n📊 New Variables:`));
|
|
131
132
|
console.log(chalk.green(` + ${newVars.length} new variable(s): ${newVars.map(v => v.name).join(', ')}`));
|
|
132
133
|
const selectedNewVars = await promptNewVariables(newVars, options);
|
|
134
|
+
// Only add the selected new variables (not all newVars)
|
|
133
135
|
variables.push(...selectedNewVars);
|
|
134
136
|
}
|
|
135
137
|
else {
|
|
@@ -150,24 +152,34 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
else {
|
|
153
|
-
// Full mode:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
// Full mode: replace all variables with detected ones (no additive)
|
|
156
|
+
variables = [];
|
|
157
|
+
// Add detected variables
|
|
158
|
+
for (const varName of detectedVars) {
|
|
159
|
+
variables.push({
|
|
160
|
+
name: varName,
|
|
161
|
+
prompt: `Enter ${varName}:`,
|
|
162
|
+
required: true
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// Also include JSON variables
|
|
166
|
+
if (fileTemplateConfig.variables && Array.isArray(fileTemplateConfig.variables)) {
|
|
167
|
+
for (const v of fileTemplateConfig.variables) {
|
|
168
|
+
const existingIndex = variables.findIndex(existing => existing.name === v.name);
|
|
169
|
+
if (existingIndex !== -1) {
|
|
170
|
+
variables[existingIndex] = { ...variables[existingIndex], ...v };
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
variables.push({ ...v });
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
|
-
if (globalVarsToPrompt.length > 0) {
|
|
163
|
-
const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
|
|
164
|
-
variables.push(...selectedGlobals);
|
|
165
|
-
}
|
|
166
|
-
const additionalVars = await promptAdditionalVariables(variables, options);
|
|
167
|
-
variables.push(...additionalVars);
|
|
168
177
|
}
|
|
169
178
|
// 1. Structure (skeleton) - Additive mode
|
|
170
179
|
let folders = [];
|
|
180
|
+
let selectedStructure = [];
|
|
181
|
+
let selectedFiles = [];
|
|
182
|
+
let selectedFolders = [];
|
|
171
183
|
if (!isFullMode) {
|
|
172
184
|
// Additive mode: only add new folders
|
|
173
185
|
const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
@@ -185,6 +197,14 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
185
197
|
printNoNewFolders();
|
|
186
198
|
folders = existingFolders;
|
|
187
199
|
}
|
|
200
|
+
// selectedStructure should include ALL folders (existing + added) in additive mode
|
|
201
|
+
// so that the final filter doesn't drop user-selected new folders
|
|
202
|
+
selectedStructure = [
|
|
203
|
+
...new Set([
|
|
204
|
+
...existingFolders.map(f => f.name),
|
|
205
|
+
...folders.filter(f => !existingFolders.some(ef => ef.name === f.name)).map(f => f.name),
|
|
206
|
+
]),
|
|
207
|
+
];
|
|
188
208
|
}
|
|
189
209
|
else {
|
|
190
210
|
// Full mode: original behavior
|
|
@@ -196,11 +216,10 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
196
216
|
const rootEntries = fs.readdirSync(resolvedPath, { withFileTypes: true })
|
|
197
217
|
.filter(e => !shouldExclude(resolvedPath, path.join(resolvedPath, e.name), ignorePatterns))
|
|
198
218
|
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
199
|
-
const rootFiles = rootEntries.filter(e => e.isFile())
|
|
219
|
+
const rootFiles = rootEntries.filter(e => e.isFile())
|
|
220
|
+
.map(e => e.name)
|
|
221
|
+
.filter(fileName => !shouldExcludeFile(fileName));
|
|
200
222
|
const rootDirs = rootEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
201
|
-
let selectedFiles = [];
|
|
202
|
-
let selectedFolders = [];
|
|
203
|
-
let selectedStructure = [];
|
|
204
223
|
if (!isFullMode) {
|
|
205
224
|
// Additive mode for files and folders
|
|
206
225
|
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
@@ -209,8 +228,6 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
209
228
|
printNewFiles(newFiles.length, newFiles);
|
|
210
229
|
const addedFiles = await promptNewFiles(newFiles, options);
|
|
211
230
|
selectedFiles = [...existingCopyFiles.filter(cf => !rootDirs.includes(cf.src)).map(cf => cf.src), ...addedFiles];
|
|
212
|
-
// Structure
|
|
213
|
-
selectedStructure = config.templates[templateName].folders?.map(f => f.name) || [];
|
|
214
231
|
// Seed selectedFolders from existing copy_files directory entries
|
|
215
232
|
selectedFolders = existingCopyFiles
|
|
216
233
|
.filter(f => rootDirs.includes(f.src))
|
|
@@ -228,9 +245,9 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
else {
|
|
231
|
-
// Full mode: original behavior
|
|
248
|
+
// Full mode: original behavior - pick up all files like a fresh learn
|
|
232
249
|
if (options.yes || options.json) {
|
|
233
|
-
selectedFiles = rootFiles
|
|
250
|
+
selectedFiles = rootFiles;
|
|
234
251
|
selectedStructure = rootDirs;
|
|
235
252
|
selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
|
|
236
253
|
}
|
|
@@ -308,8 +325,24 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
308
325
|
const detectedExecutables = rootFiles
|
|
309
326
|
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
310
327
|
.filter(file => !shouldExcludeFile(file));
|
|
328
|
+
// In additive mode, only add executables that were explicitly selected by the user
|
|
329
|
+
let selectedExecutables = [];
|
|
330
|
+
if (!isFullMode) {
|
|
331
|
+
// In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
|
|
332
|
+
// BUT exclude files that already exist in copy_files with custom settings (chmod, substitute_variables: false)
|
|
333
|
+
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
334
|
+
const existingCopySrcs = new Set(existingCopyFiles.map(cf => cf.src));
|
|
335
|
+
const existingCustomSettings = new Set(existingCopyFiles
|
|
336
|
+
.filter(cf => cf.chmod || cf.substitute_variables === false)
|
|
337
|
+
.map(cf => cf.src));
|
|
338
|
+
selectedExecutables = detectedExecutables.filter(exec => selectedFiles.includes(exec) && !existingCustomSettings.has(exec));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// In full mode, include all detected executables (original behavior)
|
|
342
|
+
selectedExecutables = detectedExecutables;
|
|
343
|
+
}
|
|
311
344
|
const existingPostCopy = config.templates[templateName].post_copy || [];
|
|
312
|
-
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy,
|
|
345
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
|
|
313
346
|
if (post_copy.length > 0) {
|
|
314
347
|
templateConfig.post_copy = post_copy;
|
|
315
348
|
const postCopySrcs = post_copy.map(f => f.src);
|
|
@@ -320,9 +353,7 @@ export async function update(sourcePath, templateName, options = {}) {
|
|
|
320
353
|
name: templateName,
|
|
321
354
|
...templateConfig
|
|
322
355
|
};
|
|
323
|
-
process.stdout.write(JSON.stringify(output, null, 2) + '\n'
|
|
324
|
-
process.exit(0);
|
|
325
|
-
});
|
|
356
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
326
357
|
return;
|
|
327
358
|
}
|
|
328
359
|
config.templates[templateName] = templateConfig;
|
package/dist/config.js
CHANGED
|
@@ -223,6 +223,7 @@ export const DEFAULT_EXCLUDES = [
|
|
|
223
223
|
'dist',
|
|
224
224
|
'build',
|
|
225
225
|
'bin',
|
|
226
|
+
'.vscode',
|
|
226
227
|
'.DS_Store',
|
|
227
228
|
'Thumbs.db',
|
|
228
229
|
];
|
|
@@ -327,6 +328,8 @@ export function shouldExcludeFile(fileName) {
|
|
|
327
328
|
'yarn.lock',
|
|
328
329
|
'pnpm-lock.yaml',
|
|
329
330
|
'composer.lock',
|
|
331
|
+
'post_config.sh',
|
|
332
|
+
'post_config.bat',
|
|
330
333
|
];
|
|
331
334
|
for (const pattern of excludePatterns) {
|
|
332
335
|
if (pattern.startsWith('*')) {
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const program = new Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name('pt')
|
|
20
20
|
.description('Project Template CLI - Learn project structures and initialize new ones')
|
|
21
|
-
.version(pkg.version, '-v', 'output the version number');
|
|
21
|
+
.version(pkg.version, '-v, --version', 'output the version number');
|
|
22
22
|
program
|
|
23
23
|
.command('learn [path]')
|
|
24
24
|
.description('Learn a project structure from an existing directory')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@garyr/pt-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Project Template CLI - Learn structures and initialize projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"dev": "tsx src/index.ts",
|
|
14
14
|
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
15
15
|
"test:sequential": "node --import tsx --test tests/config.test.ts && node --import tsx --test tests/init.test.ts && node --import tsx --test tests/learn.test.ts && node --import tsx --test tests/substitute.test.ts && node --import tsx --test tests/config-utils.test.ts",
|
|
16
|
-
"completion:bash": "tsx src/
|
|
17
|
-
"completion:zsh": "tsx src/
|
|
18
|
-
"completion:fish": "tsx src/
|
|
16
|
+
"completion:bash": "tsx src/index.ts completion bash",
|
|
17
|
+
"completion:zsh": "tsx src/index.ts completion zsh",
|
|
18
|
+
"completion:fish": "tsx src/index.ts completion fish",
|
|
19
19
|
"prepublishOnly": "npm run build",
|
|
20
20
|
"build:linux": "bun build ./src/index.ts --compile --minify --target=bun-linux-x64 --outfile ../BUILD/pt-cli/pt-linux",
|
|
21
21
|
"build:macos": "bun build ./src/index.ts --compile --minify --target=bun-darwin-x64 --outfile ../BUILD/pt-cli/pt-macos",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
2
|
import YAML from 'yaml';
|
|
5
3
|
import { getConfigPath } from '../config.js';
|
|
6
4
|
|
|
@@ -404,12 +402,3 @@ export async function completionCommand(shellArg?: string, options?: { templates
|
|
|
404
402
|
}
|
|
405
403
|
}
|
|
406
404
|
|
|
407
|
-
// Allow direct execution via tsx src/commands/completionCommand.ts <shell>
|
|
408
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
409
|
-
const shell = process.argv[2];
|
|
410
|
-
if (shell === '--templates' || shell === '_templates') {
|
|
411
|
-
completionCommand(shell, { templates: true });
|
|
412
|
-
} else {
|
|
413
|
-
completionCommand(shell);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
@@ -281,14 +281,21 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
281
281
|
folders = existingTemplate.folders;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// selectedStructure should include ALL folders (existing + added) in additive mode
|
|
285
|
+
// so that the final filter doesn't drop user-selected new folders
|
|
286
|
+
selectedStructure = [
|
|
287
|
+
...new Set([
|
|
288
|
+
...existingTemplate.folders?.map((f: FolderNode) => f.name) || [],
|
|
289
|
+
...folders.filter(f => !existingTemplate.folders?.some(ef => ef.name === f.name)).map(f => f.name),
|
|
290
|
+
]),
|
|
291
|
+
];
|
|
292
|
+
|
|
284
293
|
// New files
|
|
285
294
|
const newFiles = rootFiles.filter((f: string) => !existingTemplate.copy_files?.some((cf: CopyFileEntry) => cf.src === f));
|
|
286
295
|
printNewFiles(newFiles.length, newFiles);
|
|
287
296
|
const addedFiles = await promptNewFiles(newFiles, options);
|
|
288
297
|
selectedFiles = [...(existingTemplate.copy_files?.filter((cf: CopyFileEntry) => !rootDirs.includes(cf.src)).map((cf: CopyFileEntry) => cf.src) || []), ...addedFiles];
|
|
289
298
|
|
|
290
|
-
// Structure
|
|
291
|
-
selectedStructure = existingTemplate.folders?.map((f: FolderNode) => f.name) || [];
|
|
292
299
|
// Seed selectedFolders from existing copy_files directory entries
|
|
293
300
|
selectedFolders = (existingTemplate.copy_files || [])
|
|
294
301
|
.filter((f: CopyFileEntry) => rootDirs.includes(f.src))
|
|
@@ -311,7 +318,14 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
311
318
|
|
|
312
319
|
// --- COPY FILES ---
|
|
313
320
|
const existingCopyFiles = isUpdate ? config.templates[updateTemplate].copy_files || [] : [];
|
|
314
|
-
|
|
321
|
+
|
|
322
|
+
// If JSON template config has copy_files, use those directly (for new templates)
|
|
323
|
+
let copy_files: CopyFileEntry[];
|
|
324
|
+
if (!isUpdate && fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
|
|
325
|
+
copy_files = [...fileTemplateConfig.copy_files];
|
|
326
|
+
} else {
|
|
327
|
+
copy_files = buildCopyFiles(selectedFiles, selectedFolders, existingCopyFiles);
|
|
328
|
+
}
|
|
315
329
|
|
|
316
330
|
const templateConfig: TemplateConfig = {
|
|
317
331
|
description: description,
|
|
@@ -360,8 +374,18 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
360
374
|
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
361
375
|
.filter(file => !shouldExcludeFile(file));
|
|
362
376
|
|
|
377
|
+
// In additive mode (isUpdate), only add executables that were explicitly selected by the user
|
|
378
|
+
let selectedExecutables: string[] = [];
|
|
379
|
+
if (isUpdate) {
|
|
380
|
+
// In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
|
|
381
|
+
selectedExecutables = detectedExecutables.filter(exec => selectedFiles.includes(exec));
|
|
382
|
+
} else {
|
|
383
|
+
// In new template mode, include all detected executables (original behavior)
|
|
384
|
+
selectedExecutables = detectedExecutables;
|
|
385
|
+
}
|
|
386
|
+
|
|
363
387
|
const existingPostCopy = isUpdate ? config.templates[updateTemplate].post_copy || [] : [];
|
|
364
|
-
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy,
|
|
388
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
|
|
365
389
|
|
|
366
390
|
if (post_copy.length > 0) {
|
|
367
391
|
templateConfig.post_copy = post_copy;
|
|
@@ -372,9 +396,7 @@ export async function learn(sourcePath: string, updateTemplate: string | null =
|
|
|
372
396
|
// --- OUTPUT ---
|
|
373
397
|
if (options.json) {
|
|
374
398
|
const output = { name: targetName, ...templateConfig };
|
|
375
|
-
process.stdout.write(JSON.stringify(output, null, 2) + '\n'
|
|
376
|
-
process.exit(0);
|
|
377
|
-
});
|
|
399
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
378
400
|
return;
|
|
379
401
|
}
|
|
380
402
|
|
|
@@ -151,7 +151,9 @@ export function getRootEntries(dirPath: string, ignorePatterns?: string[]): { fi
|
|
|
151
151
|
.filter(e => !shouldExclude(dirPath, path.join(dirPath, e.name), ignorePatterns))
|
|
152
152
|
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
153
153
|
|
|
154
|
-
const files = entries.filter(e => e.isFile())
|
|
154
|
+
const files = entries.filter(e => e.isFile())
|
|
155
|
+
.map(e => e.name)
|
|
156
|
+
.filter(fileName => !shouldExcludeFile(fileName));
|
|
155
157
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
156
158
|
return { files, dirs };
|
|
157
159
|
}
|
|
@@ -207,16 +209,25 @@ export function mergePostConfigTasks(
|
|
|
207
209
|
if (jsonTasks && Array.isArray(jsonTasks)) {
|
|
208
210
|
return [...jsonTasks];
|
|
209
211
|
}
|
|
210
|
-
|
|
212
|
+
// Merge detected tasks with existing, avoiding duplicates
|
|
213
|
+
const merged = [...existingTasks];
|
|
214
|
+
for (const dt of detectedTasks) {
|
|
215
|
+
const exists = merged.some(et => et.command === dt.command && et.script === dt.script);
|
|
216
|
+
if (!exists) {
|
|
217
|
+
merged.push(dt);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return merged;
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
/**
|
|
214
224
|
* Merge post_copy files from existing, JSON file, and detected executables
|
|
225
|
+
* Only adds executables that were explicitly selected by the user
|
|
215
226
|
*/
|
|
216
227
|
export function mergePostCopyFiles(
|
|
217
228
|
existingPostCopy: PostCopyFile[],
|
|
218
229
|
jsonPostCopy: PostCopyFile[] | undefined,
|
|
219
|
-
|
|
230
|
+
selectedExecutables: string[]
|
|
220
231
|
): PostCopyFile[] {
|
|
221
232
|
let post_copy = [...existingPostCopy];
|
|
222
233
|
|
|
@@ -228,11 +239,9 @@ export function mergePostCopyFiles(
|
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// In interactive mode, we'd prompt to add these - for now just auto-add
|
|
235
|
-
for (const file of newExecutables) {
|
|
242
|
+
// Only add executables that were explicitly selected by the user
|
|
243
|
+
for (const file of selectedExecutables) {
|
|
244
|
+
if (!post_copy.some(existing => existing.src === file)) {
|
|
236
245
|
post_copy.push({ src: file, dest: file });
|
|
237
246
|
}
|
|
238
247
|
}
|
|
@@ -170,6 +170,7 @@ export async function update(sourcePath: string, templateName: string, options:
|
|
|
170
170
|
if (!isFullMode) {
|
|
171
171
|
// Additive mode: only present new variables for selection
|
|
172
172
|
const existingVarNames = new Set(config.templates[templateName].variables?.map(v => v.name) || []);
|
|
173
|
+
// newVars are NOT added to variables yet - only existing variables + JSON variables are in variables
|
|
173
174
|
const newVars = variables.filter(v => !existingVarNames.has(v.name));
|
|
174
175
|
|
|
175
176
|
if (newVars.length > 0) {
|
|
@@ -177,6 +178,7 @@ export async function update(sourcePath: string, templateName: string, options:
|
|
|
177
178
|
console.log(chalk.green(` + ${newVars.length} new variable(s): ${newVars.map(v => v.name).join(', ')}`));
|
|
178
179
|
|
|
179
180
|
const selectedNewVars = await promptNewVariables(newVars, options);
|
|
181
|
+
// Only add the selected new variables (not all newVars)
|
|
180
182
|
variables.push(...selectedNewVars);
|
|
181
183
|
} else {
|
|
182
184
|
printNoNewVariables();
|
|
@@ -197,106 +199,119 @@ export async function update(sourcePath: string, templateName: string, options:
|
|
|
197
199
|
variables.push(...selectedGlobals);
|
|
198
200
|
}
|
|
199
201
|
} else {
|
|
200
|
-
// Full mode:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
// Full mode: replace all variables with detected ones (no additive)
|
|
203
|
+
variables = [];
|
|
204
|
+
// Add detected variables
|
|
205
|
+
for (const varName of detectedVars) {
|
|
206
|
+
variables.push({
|
|
207
|
+
name: varName,
|
|
208
|
+
prompt: `Enter ${varName}:`,
|
|
209
|
+
required: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Also include JSON variables
|
|
213
|
+
if (fileTemplateConfig.variables && Array.isArray(fileTemplateConfig.variables)) {
|
|
214
|
+
for (const v of fileTemplateConfig.variables) {
|
|
215
|
+
const existingIndex = variables.findIndex(existing => existing.name === v.name);
|
|
216
|
+
if (existingIndex !== -1) {
|
|
217
|
+
variables[existingIndex] = { ...variables[existingIndex], ...v };
|
|
218
|
+
} else {
|
|
219
|
+
variables.push({ ...v });
|
|
206
220
|
}
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
|
-
|
|
210
|
-
if (globalVarsToPrompt.length > 0) {
|
|
211
|
-
const selectedGlobals = await promptGlobalVariables(globalVarsToPrompt, options);
|
|
212
|
-
variables.push(...selectedGlobals);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const additionalVars = await promptAdditionalVariables(variables, options);
|
|
216
|
-
variables.push(...additionalVars);
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
// 1. Structure (skeleton) - Additive mode
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
let folders: FolderNode[] = [];
|
|
227
|
+
let selectedStructure: string[] = [];
|
|
228
|
+
let selectedFiles: string[] = [];
|
|
229
|
+
let selectedFolders: string[] = [];
|
|
230
|
+
|
|
231
|
+
if (!isFullMode) {
|
|
232
|
+
// Additive mode: only add new folders
|
|
233
|
+
const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
234
|
+
? fileTemplateConfig.folders
|
|
235
|
+
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
226
236
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (newFolders.length > 0) {
|
|
231
|
-
console.log(chalk.cyan(`\n📊 New Folders:`));
|
|
232
|
-
console.log(chalk.green(` + ${newFolders.length} new folder(s): ${newFolders.map(f => f.name).join(', ')}`));
|
|
233
|
-
|
|
234
|
-
const addedFolders = await promptNewFolders(newFolders, options);
|
|
235
|
-
folders = [...existingFolders, ...addedFolders];
|
|
236
|
-
} else {
|
|
237
|
-
printNoNewFolders();
|
|
238
|
-
folders = existingFolders;
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
// Full mode: original behavior
|
|
242
|
-
folders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
243
|
-
? fileTemplateConfig.folders
|
|
244
|
-
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
245
|
-
}
|
|
237
|
+
const existingFolders = config.templates[templateName].folders || [];
|
|
238
|
+
const newFolders = detectedFolders.filter(f => !existingFolders.some(ef => ef.name === f.name));
|
|
246
239
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
240
|
+
if (newFolders.length > 0) {
|
|
241
|
+
console.log(chalk.cyan(`\n📊 New Folders:`));
|
|
242
|
+
console.log(chalk.green(` + ${newFolders.length} new folder(s): ${newFolders.map(f => f.name).join(', ')}`));
|
|
251
243
|
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
const addedFolders = await promptNewFolders(newFolders, options);
|
|
245
|
+
folders = [...existingFolders, ...addedFolders];
|
|
246
|
+
} else {
|
|
247
|
+
printNoNewFolders();
|
|
248
|
+
folders = existingFolders;
|
|
249
|
+
}
|
|
254
250
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
// selectedStructure should include ALL folders (existing + added) in additive mode
|
|
252
|
+
// so that the final filter doesn't drop user-selected new folders
|
|
253
|
+
selectedStructure = [
|
|
254
|
+
...new Set([
|
|
255
|
+
...existingFolders.map(f => f.name),
|
|
256
|
+
...folders.filter(f => !existingFolders.some(ef => ef.name === f.name)).map(f => f.name),
|
|
257
|
+
]),
|
|
258
|
+
];
|
|
259
|
+
} else {
|
|
260
|
+
// Full mode: original behavior
|
|
261
|
+
folders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
262
|
+
? fileTemplateConfig.folders
|
|
263
|
+
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
264
|
+
}
|
|
258
265
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
// 2. Content Selection (Root only)
|
|
267
|
+
const rootEntries = fs.readdirSync(resolvedPath, { withFileTypes: true })
|
|
268
|
+
.filter(e => !shouldExclude(resolvedPath, path.join(resolvedPath, e.name), ignorePatterns))
|
|
269
|
+
.filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
|
|
270
|
+
|
|
271
|
+
const rootFiles = rootEntries.filter(e => e.isFile())
|
|
272
|
+
.map(e => e.name)
|
|
273
|
+
.filter(fileName => !shouldExcludeFile(fileName));
|
|
274
|
+
const rootDirs = rootEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
275
|
+
|
|
276
|
+
if (!isFullMode) {
|
|
277
|
+
// Additive mode for files and folders
|
|
278
|
+
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
280
|
-
const newFolders = detectedFolders.filter(f => !config.templates[templateName].folders?.some(ef => ef.name === f.name));
|
|
280
|
+
// New files
|
|
281
|
+
const newFiles = rootFiles.filter(f => !existingCopyFiles.some(cf => cf.src === f));
|
|
282
|
+
printNewFiles(newFiles.length, newFiles);
|
|
283
|
+
const addedFiles = await promptNewFiles(newFiles, options);
|
|
284
|
+
selectedFiles = [...existingCopyFiles.filter(cf => !rootDirs.includes(cf.src)).map(cf => cf.src), ...addedFiles];
|
|
285
|
+
|
|
286
|
+
// Seed selectedFolders from existing copy_files directory entries
|
|
287
|
+
selectedFolders = existingCopyFiles
|
|
288
|
+
.filter(f => rootDirs.includes(f.src))
|
|
289
|
+
.map(f => f.src);
|
|
290
|
+
|
|
291
|
+
if (rootDirs.length > 0) {
|
|
292
|
+
const detectedFolders = fileTemplateConfig.folders && Array.isArray(fileTemplateConfig.folders)
|
|
293
|
+
? fileTemplateConfig.folders
|
|
294
|
+
: extractStructure(resolvedPath, resolvedPath, ignorePatterns);
|
|
295
|
+
const newFolders = detectedFolders.filter(f => !config.templates[templateName].folders?.some(ef => ef.name === f.name));
|
|
281
296
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
const addedDirs = newFolders
|
|
298
|
+
.filter(f => ['APP', 'scripts', 'bin'].some(p => f.name === p))
|
|
299
|
+
.map(f => f.name);
|
|
300
|
+
selectedStructure = [...new Set([...selectedStructure, ...addedDirs])];
|
|
301
|
+
selectedFolders = [...new Set([...selectedFolders, ...addedDirs])];
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
// Full mode: original behavior - pick up all files like a fresh learn
|
|
305
|
+
if (options.yes || options.json) {
|
|
306
|
+
selectedFiles = rootFiles;
|
|
307
|
+
selectedStructure = rootDirs;
|
|
308
|
+
selectedFolders = rootDirs.filter(d => ['APP', 'scripts', 'bin'].some(p => d === p));
|
|
309
|
+
} else {
|
|
310
|
+
selectedFiles = await promptRootFiles(rootFiles, undefined, options);
|
|
311
|
+
selectedStructure = await promptStructureFolders(rootDirs, options);
|
|
312
|
+
selectedFolders = await promptCopyFolders(selectedStructure, undefined, options);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
300
315
|
|
|
301
316
|
const copy_files: CopyFileEntry[] = [];
|
|
302
317
|
if (fileTemplateConfig.copy_files && Array.isArray(fileTemplateConfig.copy_files)) {
|
|
@@ -371,8 +386,29 @@ export async function update(sourcePath: string, templateName: string, options:
|
|
|
371
386
|
.filter(file => isExecutable(path.join(resolvedPath, file), file))
|
|
372
387
|
.filter(file => !shouldExcludeFile(file));
|
|
373
388
|
|
|
389
|
+
// In additive mode, only add executables that were explicitly selected by the user
|
|
390
|
+
let selectedExecutables: string[] = [];
|
|
391
|
+
if (!isFullMode) {
|
|
392
|
+
// In additive mode, only include executables that are in selectedFiles (user explicitly chose them)
|
|
393
|
+
// BUT exclude files that already exist in copy_files with custom settings (chmod, substitute_variables: false)
|
|
394
|
+
const existingCopyFiles = config.templates[templateName].copy_files || [];
|
|
395
|
+
const existingCopySrcs = new Set(existingCopyFiles.map(cf => cf.src));
|
|
396
|
+
const existingCustomSettings = new Set(
|
|
397
|
+
existingCopyFiles
|
|
398
|
+
.filter(cf => cf.chmod || cf.substitute_variables === false)
|
|
399
|
+
.map(cf => cf.src)
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
selectedExecutables = detectedExecutables.filter(exec =>
|
|
403
|
+
selectedFiles.includes(exec) && !existingCustomSettings.has(exec)
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
// In full mode, include all detected executables (original behavior)
|
|
407
|
+
selectedExecutables = detectedExecutables;
|
|
408
|
+
}
|
|
409
|
+
|
|
374
410
|
const existingPostCopy = config.templates[templateName].post_copy || [];
|
|
375
|
-
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy,
|
|
411
|
+
const post_copy = mergePostCopyFiles(existingPostCopy, fileTemplateConfig.post_copy, selectedExecutables);
|
|
376
412
|
|
|
377
413
|
if (post_copy.length > 0) {
|
|
378
414
|
templateConfig.post_copy = post_copy;
|
|
@@ -386,9 +422,7 @@ export async function update(sourcePath: string, templateName: string, options:
|
|
|
386
422
|
...templateConfig
|
|
387
423
|
};
|
|
388
424
|
|
|
389
|
-
process.stdout.write(JSON.stringify(output, null, 2) + '\n'
|
|
390
|
-
process.exit(0);
|
|
391
|
-
});
|
|
425
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
392
426
|
return;
|
|
393
427
|
}
|
|
394
428
|
|
package/src/config.ts
CHANGED
|
@@ -285,6 +285,7 @@ export const DEFAULT_EXCLUDES = [
|
|
|
285
285
|
'dist',
|
|
286
286
|
'build',
|
|
287
287
|
'bin',
|
|
288
|
+
'.vscode',
|
|
288
289
|
'.DS_Store',
|
|
289
290
|
'Thumbs.db',
|
|
290
291
|
];
|
|
@@ -397,6 +398,8 @@ export function shouldExcludeFile(fileName: string): boolean {
|
|
|
397
398
|
'yarn.lock',
|
|
398
399
|
'pnpm-lock.yaml',
|
|
399
400
|
'composer.lock',
|
|
401
|
+
'post_config.sh',
|
|
402
|
+
'post_config.bat',
|
|
400
403
|
];
|
|
401
404
|
|
|
402
405
|
for (const pattern of excludePatterns) {
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ const program = new Command();
|
|
|
25
25
|
program
|
|
26
26
|
.name('pt')
|
|
27
27
|
.description('Project Template CLI - Learn project structures and initialize new ones')
|
|
28
|
-
.version(pkg.version, '-v', 'output the version number');
|
|
28
|
+
.version(pkg.version, '-v, --version', 'output the version number');
|
|
29
29
|
|
|
30
30
|
program
|
|
31
31
|
.command('learn [path]')
|
|
@@ -147,9 +147,9 @@ test('DEFAULT_EXCLUDES contains expected patterns', () => {
|
|
|
147
147
|
assert.ok(DEFAULT_EXCLUDES.includes('node_modules'), 'Should include node_modules');
|
|
148
148
|
assert.ok(DEFAULT_EXCLUDES.includes('dist'), 'Should include dist');
|
|
149
149
|
assert.ok(DEFAULT_EXCLUDES.includes('build'), 'Should include build');
|
|
150
|
+
assert.ok(DEFAULT_EXCLUDES.includes('.vscode'), 'Should include .vscode');
|
|
150
151
|
assert.ok(DEFAULT_EXCLUDES.includes('.DS_Store'), 'Should include .DS_Store');
|
|
151
152
|
assert.ok(DEFAULT_EXCLUDES.includes('Thumbs.db'), 'Should include Thumbs.db');
|
|
152
|
-
assert.ok(!DEFAULT_EXCLUDES.includes('.vscode'), 'Should NOT include .vscode');
|
|
153
153
|
assert.ok(!DEFAULT_EXCLUDES.includes('.gitea'), 'Should NOT include .gitea');
|
|
154
154
|
assert.ok(!DEFAULT_EXCLUDES.includes('.stignore'), 'Should NOT include .stignore');
|
|
155
155
|
});
|
package/tests/learn.test.ts
CHANGED
|
@@ -38,15 +38,16 @@ function cleanConfig() {
|
|
|
38
38
|
|
|
39
39
|
/** Capture console.log output during an async callback. */
|
|
40
40
|
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
|
41
|
-
const
|
|
41
|
+
const originalWrite = process.stdout.write;
|
|
42
42
|
let captured = '';
|
|
43
|
-
|
|
44
|
-
captured +=
|
|
43
|
+
process.stdout.write = (chunk: any) => {
|
|
44
|
+
captured += chunk.toString();
|
|
45
|
+
return true;
|
|
45
46
|
};
|
|
46
47
|
try {
|
|
47
48
|
await fn();
|
|
48
49
|
} finally {
|
|
49
|
-
|
|
50
|
+
process.stdout.write = originalWrite;
|
|
50
51
|
}
|
|
51
52
|
return captured;
|
|
52
53
|
}
|
package/tests/update.test.ts
CHANGED
|
@@ -366,6 +366,51 @@ test('update: handles post_copy executables additively', async () => {
|
|
|
366
366
|
}
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
test('update: does not add post_config.sh/.bat to post_copy when unchecked', async () => {
|
|
370
|
+
const srcDir = createSourceDir('.test-update-postconfig-exclude');
|
|
371
|
+
cleanup(testHome);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const initialConfig: PtConfig = {
|
|
375
|
+
version: '3.0',
|
|
376
|
+
templates: {
|
|
377
|
+
'update-test-pc-exclude': {
|
|
378
|
+
description: 'Test',
|
|
379
|
+
templateRoot: '/old/path',
|
|
380
|
+
folders: [],
|
|
381
|
+
copy_files: [],
|
|
382
|
+
variables: [],
|
|
383
|
+
post_copy: []
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
saveConfig(initialConfig);
|
|
388
|
+
|
|
389
|
+
// Create post_config scripts - these should be excluded by shouldExcludeFile
|
|
390
|
+
fs.writeFileSync(path.join(srcDir, 'post_config.sh'), '#!/bin/bash\necho "Running: Build"\nnpm run build\n');
|
|
391
|
+
fs.writeFileSync(path.join(srcDir, 'post_config.bat'), '@echo off\necho Running: Build\nnpm run build\n');
|
|
392
|
+
fs.chmodSync(path.join(srcDir, 'post_config.sh'), 0o755);
|
|
393
|
+
fs.chmodSync(path.join(srcDir, 'post_config.bat'), 0o755);
|
|
394
|
+
|
|
395
|
+
await update(srcDir, 'update-test-pc-exclude', { yes: true });
|
|
396
|
+
|
|
397
|
+
const config = loadConfig();
|
|
398
|
+
const tpl = config.templates['update-test-pc-exclude'];
|
|
399
|
+
|
|
400
|
+
// post_config.sh and post_config.bat should NOT be in post_copy
|
|
401
|
+
const postCopySrcs = (tpl.post_copy || []).map(pc => pc.src);
|
|
402
|
+
assert.ok(!postCopySrcs.includes('post_config.sh'), 'post_config.sh should not be in post_copy');
|
|
403
|
+
assert.ok(!postCopySrcs.includes('post_config.bat'), 'post_config.bat should not be in post_copy');
|
|
404
|
+
|
|
405
|
+
// They should also not be in copy_files
|
|
406
|
+
const copySrcs = (tpl.copy_files || []).map(cf => cf.src);
|
|
407
|
+
assert.ok(!copySrcs.includes('post_config.sh'), 'post_config.sh should not be in copy_files');
|
|
408
|
+
assert.ok(!copySrcs.includes('post_config.bat'), 'post_config.bat should not be in copy_files');
|
|
409
|
+
} finally {
|
|
410
|
+
cleanup(srcDir, testHome);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
369
414
|
test('update: JSON mode outputs template config', async () => {
|
|
370
415
|
const srcDir = createSourceDir('.test-update-json');
|
|
371
416
|
cleanup(testHome);
|
|
@@ -390,21 +435,30 @@ test('update: JSON mode outputs template config', async () => {
|
|
|
390
435
|
fs.writeFileSync(path.join(srcDir, 'template.txt'), '{{ var }}');
|
|
391
436
|
|
|
392
437
|
// Capture stdout using a proper approach
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
438
|
+
const originalWrite = process.stdout.write;
|
|
439
|
+
let stdoutOutput = '';
|
|
440
|
+
process.stdout.write = (chunk: any) => {
|
|
441
|
+
stdoutOutput += chunk.toString();
|
|
442
|
+
return true;
|
|
397
443
|
};
|
|
398
|
-
|
|
444
|
+
|
|
399
445
|
try {
|
|
400
446
|
await update(srcDir, 'update-test-json', { yes: true, json: true });
|
|
401
447
|
} finally {
|
|
402
|
-
|
|
448
|
+
process.stdout.write = originalWrite;
|
|
403
449
|
}
|
|
404
450
|
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
451
|
+
// Debug output
|
|
452
|
+
// console.error('stdoutOutput:', JSON.stringify(stdoutOutput));
|
|
453
|
+
|
|
454
|
+
// Find the JSON (starts with { and goes to end)
|
|
455
|
+
const firstBrace = stdoutOutput.indexOf('{');
|
|
456
|
+
if (firstBrace === -1) {
|
|
457
|
+
console.error('No JSON found. Full output:', stdoutOutput);
|
|
458
|
+
assert.fail('No JSON output found');
|
|
459
|
+
}
|
|
460
|
+
const jsonString = stdoutOutput.substring(firstBrace);
|
|
461
|
+
const parsed = JSON.parse(jsonString);
|
|
408
462
|
assert.strictEqual(parsed.name, 'update-test-json');
|
|
409
463
|
assert.ok(parsed.folders);
|
|
410
464
|
assert.ok(parsed.copy_files);
|