@garyr/pt-cli 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { FolderNode, TemplateConfig, CopyFileEntry, PostConfigTask, PostCopyFile, shouldIgnore, shouldExclude, shouldExcludeFile } from '../config.js';
4
+
5
+ /**
6
+ * Extract folder structure skeleton from a directory
7
+ * Only includes directories, with optional .info.md content
8
+ */
9
+ export function extractStructure(dirPath: string, rootPath: string, ignorePatterns?: string[]): FolderNode[] {
10
+ const nodes: FolderNode[] = [];
11
+ try {
12
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
13
+ for (const entry of entries) {
14
+ const fullPath = path.join(dirPath, entry.name);
15
+ const relativePath = path.relative(rootPath, fullPath);
16
+
17
+ // Only include directories in the structure skeleton
18
+ const isDirectory = entry.isDirectory() || (entry.isSymbolicLink() && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory());
19
+ if (!isDirectory) continue;
20
+
21
+ if (shouldIgnore(entry.name, relativePath, ignorePatterns)) continue;
22
+ if (shouldExclude(dirPath, fullPath)) continue;
23
+
24
+ const children = extractStructure(fullPath, rootPath, ignorePatterns);
25
+ let info = '';
26
+ const gitkeepPath = path.join(fullPath, '.gitkeep.md');
27
+ const infoPath = path.join(fullPath, '.info.md');
28
+ if (fs.existsSync(infoPath)) info = fs.readFileSync(infoPath, 'utf-8').trim();
29
+ else if (fs.existsSync(gitkeepPath)) info = fs.readFileSync(gitkeepPath, 'utf-8').trim();
30
+ nodes.push({ name: entry.name, info, children });
31
+ }
32
+ } catch (e) {
33
+ // Ignore directory read errors
34
+ }
35
+ return nodes;
36
+ }
37
+
38
+ /**
39
+ * Scan text files in top-level and 1st-level subdirectories for {{ variable_name }} placeholders.
40
+ */
41
+ export function findVariablesInFiles(dirPath: string, rootPath: string, ignorePatterns?: string[]): string[] {
42
+ const variables = new Set<string>();
43
+ const regex = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
44
+
45
+ const textExtensions = ['.md', '.txt', '.makerc', '.json', '.yaml', '.yml', '.ini', '.conf', '.config', '.sh', '.py', '.js', '.ts', '.html', '.css', '.makefile'];
46
+
47
+ const scan = (currentPath: string, depth: number) => {
48
+ if (depth > 1) return; // Top level (0) and 1st level subfolders (1)
49
+
50
+ try {
51
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ const fullPath = path.join(currentPath, entry.name);
54
+ const relativePath = path.relative(rootPath, fullPath);
55
+
56
+ if (entry.isDirectory()) {
57
+ if (shouldIgnore(entry.name, relativePath, ignorePatterns)) continue;
58
+ if (shouldExclude(currentPath, fullPath)) continue;
59
+ scan(fullPath, depth + 1);
60
+ } else if (entry.isFile()) {
61
+ const ext = path.extname(entry.name).toLowerCase();
62
+ const isMakefile = entry.name.toLowerCase() === 'makefile';
63
+
64
+ if (textExtensions.includes(ext) || isMakefile || ext === '') {
65
+ try {
66
+ const content = fs.readFileSync(fullPath, 'utf-8');
67
+ let match;
68
+ regex.lastIndex = 0;
69
+ while ((match = regex.exec(content)) !== null) {
70
+ variables.add(match[1]);
71
+ }
72
+ } catch (e) {
73
+ // Skip files that can't be read or aren't text
74
+ }
75
+ }
76
+ }
77
+ }
78
+ } catch (e) {
79
+ // Ignore directory read errors
80
+ }
81
+ };
82
+
83
+ scan(dirPath, 0);
84
+ return Array.from(variables);
85
+ }
86
+
87
+ /**
88
+ * Check if a file is executable (by extension or permissions)
89
+ */
90
+ export function isExecutable(fullPath: string, fileName: string): boolean {
91
+ if (shouldExcludeFile(fileName)) return false;
92
+ const ext = path.extname(fileName).toLowerCase();
93
+ if (['.sh', '.py', '.bash', '.bat', '.cmd'].includes(ext)) return true;
94
+ if (fileName.toLowerCase() === 'makefile') return true;
95
+ try {
96
+ const stat = fs.statSync(fullPath);
97
+ return !!(stat.mode & 0o111);
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Parse .info.md file for name and description
105
+ */
106
+ export function parseInfoFile(infoPath: string): { name: string; description: string } {
107
+ let name = '';
108
+ let description = '';
109
+ if (fs.existsSync(infoPath)) {
110
+ const content = fs.readFileSync(infoPath, 'utf-8');
111
+ const lines = content.split('\n');
112
+ for (const line of lines) {
113
+ if (line.startsWith('# ')) {
114
+ name = line.substring(2).trim();
115
+ } else if (line.trim() !== '' && !description && !line.startsWith('#')) {
116
+ description = line.trim();
117
+ }
118
+ }
119
+ }
120
+ return { name, description };
121
+ }
122
+
123
+ /**
124
+ * Load JSON template config from .pt-template.json or template.json
125
+ */
126
+ export function loadJsonTemplateConfig(dirPath: string): Partial<TemplateConfig> & { name?: string } {
127
+ const jsonConfigPaths = [
128
+ path.join(dirPath, '.pt-template.json'),
129
+ path.join(dirPath, 'template.json')
130
+ ];
131
+ for (const jPath of jsonConfigPaths) {
132
+ if (fs.existsSync(jPath)) {
133
+ try {
134
+ const content = fs.readFileSync(jPath, 'utf-8');
135
+ return JSON.parse(content);
136
+ } catch (e) {
137
+ console.warn(`Warning: Failed to parse ${path.basename(jPath)}: ${(e as Error).message}`);
138
+ }
139
+ }
140
+ }
141
+ return {};
142
+ }
143
+
144
+ /**
145
+ * Get root-level files and directories (for copy_files selection)
146
+ */
147
+ export function getRootEntries(dirPath: string, ignorePatterns?: string[]): { files: string[]; dirs: string[] } {
148
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true })
149
+ .filter(e => !shouldExclude(dirPath, path.join(dirPath, e.name), ignorePatterns))
150
+ .filter(e => !shouldIgnore(e.name, e.name, ignorePatterns));
151
+
152
+ const files = entries.filter(e => e.isFile()).map(e => e.name);
153
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
154
+ return { files, dirs };
155
+ }
156
+
157
+ /**
158
+ * Detect executable files at root level
159
+ */
160
+ export function detectRootExecutables(dirPath: string, ignorePatterns?: string[]): string[] {
161
+ const { files } = getRootEntries(dirPath, ignorePatterns);
162
+ return files.filter(file => isExecutable(path.join(dirPath, file), file));
163
+ }
164
+
165
+ /**
166
+ * Parse post_config.sh or post_config.bat for commands
167
+ */
168
+ export function parsePostConfigScript(shPath: string, batPath: string): PostConfigTask[] {
169
+ const tasks: PostConfigTask[] = [];
170
+
171
+ if (fs.existsSync(shPath)) {
172
+ const lines = fs.readFileSync(shPath, 'utf-8').split('\n');
173
+ let currentDesc = '';
174
+ for (const line of lines) {
175
+ if (line.startsWith('echo "Running: ')) {
176
+ currentDesc = line.substring(15, line.length - 1).replace(/"$/, '');
177
+ } else if (line.trim() && !line.startsWith('#') && !line.startsWith('echo ')) {
178
+ tasks.push({ command: line.trim(), description: currentDesc || line.trim() });
179
+ currentDesc = '';
180
+ }
181
+ }
182
+ } else if (fs.existsSync(batPath)) {
183
+ const lines = fs.readFileSync(batPath, 'utf-8').split('\n');
184
+ let currentDesc = '';
185
+ for (const line of lines) {
186
+ if (line.startsWith('echo Running: ')) {
187
+ currentDesc = line.substring(14).trim();
188
+ } else if (line.trim() && !line.startsWith('::') && !line.startsWith('@echo') && !line.startsWith('echo ')) {
189
+ tasks.push({ command: line.trim(), description: currentDesc || line.trim() });
190
+ currentDesc = '';
191
+ }
192
+ }
193
+ }
194
+ return tasks;
195
+ }
196
+
197
+ /**
198
+ * Build copy_files array from selected files and folders
199
+ */
200
+ export function buildCopyFiles(
201
+ selectedFiles: string[],
202
+ selectedFolders: string[],
203
+ existingCopyFiles: CopyFileEntry[] = []
204
+ ): CopyFileEntry[] {
205
+ const copyFiles: CopyFileEntry[] = [];
206
+ const existingSrcs = new Set(existingCopyFiles.map(e => e.src));
207
+
208
+ // Start with existing entries (preserves substitute_variables, chmod settings)
209
+ copyFiles.push(...existingCopyFiles);
210
+
211
+ // Add new files
212
+ for (const f of selectedFiles) {
213
+ if (existingSrcs.has(f)) continue;
214
+ copyFiles.push({ src: f, dest: f, substitute_variables: true });
215
+ }
216
+ // Add new directories
217
+ for (const d of selectedFolders) {
218
+ if (existingSrcs.has(d)) continue;
219
+ copyFiles.push({ src: d, dest: d, substitute_variables: true });
220
+ }
221
+ return copyFiles;
222
+ }
223
+
224
+ /**
225
+ * Merge post_config tasks from JSON config and/or detected scripts
226
+ */
227
+ export function mergePostConfigTasks(
228
+ existingTasks: PostConfigTask[],
229
+ jsonTasks: PostConfigTask[] | undefined,
230
+ detectedTasks: PostConfigTask[]
231
+ ): PostConfigTask[] {
232
+ let tasks = [...existingTasks];
233
+
234
+ // JSON config takes precedence (full replacement)
235
+ if (jsonTasks && jsonTasks.length > 0) {
236
+ tasks = [...jsonTasks];
237
+ } else if (detectedTasks.length > 0) {
238
+ // Add detected tasks that don't already exist
239
+ for (const dt of detectedTasks) {
240
+ const exists = tasks.some(t => t.command === dt.command || (t.script && t.script === dt.script));
241
+ if (!exists) tasks.push(dt);
242
+ }
243
+ }
244
+ return tasks;
245
+ }
246
+
247
+ /**
248
+ * Merge post_copy files from JSON config and/or detected executables
249
+ */
250
+ export function mergePostCopyFiles(
251
+ existingFiles: PostCopyFile[],
252
+ jsonFiles: PostCopyFile[] | undefined,
253
+ detectedFiles: string[]
254
+ ): PostCopyFile[] {
255
+ let files = [...existingFiles];
256
+
257
+ if (jsonFiles && jsonFiles.length > 0) {
258
+ for (const jf of jsonFiles) {
259
+ if (!files.some(ef => ef.src === jf.src)) files.push(jf);
260
+ }
261
+ }
262
+
263
+ for (const df of detectedFiles) {
264
+ if (!files.some(ef => ef.src === df)) {
265
+ files.push({ src: df, dest: df });
266
+ }
267
+ }
268
+ return files;
269
+ }
@@ -0,0 +1,406 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { TemplateVariable, FolderNode, CopyFileEntry, PostConfigTask } from '../config.js';
4
+
5
+ /**
6
+ * Prompt for template name with auto-detection and confirmation
7
+ */
8
+ export async function promptTemplateName(
9
+ currentName: string,
10
+ existingNames: string[],
11
+ sourceName: string | null,
12
+ options: { yes?: boolean; json?: boolean } = {}
13
+ ): Promise<string> {
14
+ if (options.yes || options.json) return currentName;
15
+
16
+ // If name came from source (.info.md or JSON), confirm it
17
+ if (sourceName && sourceName === currentName) {
18
+ const { confirmName } = await inquirer.prompt({
19
+ type: 'confirm',
20
+ name: 'confirmName',
21
+ message: `Use "${currentName}" as the template name?`,
22
+ default: true
23
+ });
24
+ if (!confirmName) {
25
+ const { newName } = await inquirer.prompt({
26
+ type: 'input',
27
+ name: 'newName',
28
+ message: 'Name this template:',
29
+ default: currentName
30
+ });
31
+ return newName;
32
+ }
33
+ }
34
+
35
+ // Check for existing template name
36
+ if (existingNames.includes(currentName)) {
37
+ console.warn(chalk.yellow(`⚠ Warning: "${currentName}" already exists. Using this name will overwrite.`));
38
+ }
39
+ return currentName;
40
+ }
41
+
42
+ /**
43
+ * Prompt for template description
44
+ */
45
+ export async function promptDescription(
46
+ currentDesc: string,
47
+ options: { yes?: boolean; json?: boolean } = {}
48
+ ): Promise<string> {
49
+ if (options.yes || options.json) return currentDesc;
50
+
51
+ const { description } = await inquirer.prompt({
52
+ type: 'input',
53
+ name: 'description',
54
+ message: 'Template description:',
55
+ default: currentDesc
56
+ });
57
+ return description;
58
+ }
59
+
60
+ /**
61
+ * Prompt for template root directory
62
+ */
63
+ export async function promptTemplateRoot(
64
+ currentRoot: string,
65
+ options: { yes?: boolean; json?: boolean } = {}
66
+ ): Promise<string> {
67
+ if (options.yes || options.json) return currentRoot;
68
+
69
+ const { templateRoot } = await inquirer.prompt({
70
+ type: 'input',
71
+ name: 'templateRoot',
72
+ message: 'Template root folder:',
73
+ default: currentRoot
74
+ });
75
+ return templateRoot;
76
+ }
77
+
78
+ /**
79
+ * Prompt for new variables (checkbox selection)
80
+ */
81
+ export async function promptNewVariables(
82
+ newVars: TemplateVariable[],
83
+ options: { yes?: boolean; json?: boolean } = {}
84
+ ): Promise<TemplateVariable[]> {
85
+ if (newVars.length === 0) return [];
86
+
87
+ if (options.yes || options.json) return newVars;
88
+
89
+ console.log(chalk.cyan(`\nšŸ“Š New Variables:`));
90
+ console.log(chalk.green(` + ${newVars.length} new variable(s): ${newVars.map(v => v.name).join(', ')}`));
91
+
92
+ const { selectedVars } = await inquirer.prompt({
93
+ type: 'checkbox',
94
+ name: 'selectedVars',
95
+ message: 'Select variables to include (space to toggle):',
96
+ loop: false,
97
+ theme: {
98
+ icon: {
99
+ checked: chalk.green('[x] '),
100
+ unchecked: '[ ] ',
101
+ }
102
+ },
103
+ choices: newVars.map(v => ({ name: v.name, checked: true }))
104
+ });
105
+
106
+ return newVars.filter(v => selectedVars.includes(v.name));
107
+ }
108
+
109
+ /**
110
+ * Prompt for global/default variables to include
111
+ */
112
+ export async function promptGlobalVariables(
113
+ globalVars: TemplateVariable[],
114
+ options: { yes?: boolean; json?: boolean } = {}
115
+ ): Promise<TemplateVariable[]> {
116
+ if (globalVars.length === 0) return [];
117
+
118
+ if (options.yes || options.json) return globalVars;
119
+
120
+ const { selectedGlobals } = await inquirer.prompt({
121
+ type: 'checkbox',
122
+ name: 'selectedGlobals',
123
+ message: 'Select default/global variables to include (space to toggle):',
124
+ loop: false,
125
+ theme: {
126
+ icon: {
127
+ checked: chalk.green('[x] '),
128
+ unchecked: '[ ] ',
129
+ }
130
+ },
131
+ choices: globalVars.map(v => ({ name: v.name, checked: true }))
132
+ });
133
+
134
+ return globalVars.filter(v => selectedGlobals.includes(v.name));
135
+ }
136
+
137
+ /**
138
+ * Prompt for additional custom variables
139
+ */
140
+ export async function promptAdditionalVariables(
141
+ existingVars: TemplateVariable[],
142
+ options: { yes?: boolean; json?: boolean } = {}
143
+ ): Promise<TemplateVariable[]> {
144
+ if (options.yes || options.json) return [];
145
+
146
+ const message = existingVars.length > 0
147
+ ? `Existing variables: ${existingVars.map(v => v.name).join(', ')}. Define more?`
148
+ : 'Define template variables (e.g., client_name, project_type)?';
149
+
150
+ const { hasMore } = await inquirer.prompt({
151
+ type: 'confirm',
152
+ name: 'hasMore',
153
+ message,
154
+ default: false
155
+ });
156
+
157
+ if (!hasMore) return [];
158
+
159
+ const { variableDefs } = await inquirer.prompt({
160
+ type: 'input',
161
+ name: 'variableDefs',
162
+ message: 'Define additional variables as comma-separated names:'
163
+ });
164
+
165
+ if (!variableDefs) return [];
166
+
167
+ const additionalVars = variableDefs.split(',').map((v: string) => v.trim()).filter(Boolean);
168
+ return additionalVars
169
+ .filter((v: string) => !existingVars.some((existing: TemplateVariable) => existing.name === v))
170
+ .map((v: string) => ({ name: v, prompt: `Enter ${v}:`, required: true }));
171
+ }
172
+
173
+ /**
174
+ * Prompt for new folders (checkbox selection)
175
+ */
176
+ export async function promptNewFolders(
177
+ newFolders: FolderNode[],
178
+ options: { yes?: boolean; json?: boolean } = {}
179
+ ): Promise<FolderNode[]> {
180
+ if (newFolders.length === 0) return [];
181
+
182
+ if (options.yes || options.json) return newFolders;
183
+
184
+ console.log(chalk.cyan(`\nšŸ“Š New Folders:`));
185
+ console.log(chalk.green(` + ${newFolders.length} new folder(s): ${newFolders.map(f => f.name).join(', ')}`));
186
+
187
+ const { selectedFolders } = await inquirer.prompt({
188
+ type: 'checkbox',
189
+ name: 'selectedFolders',
190
+ message: 'Select folders to include in template structure (space to toggle):',
191
+ loop: false,
192
+ theme: {
193
+ icon: {
194
+ checked: chalk.green('[x] '),
195
+ unchecked: '[ ] ',
196
+ }
197
+ },
198
+ choices: newFolders.map(f => ({ name: f.name, checked: true }))
199
+ });
200
+
201
+ return newFolders.filter(f => selectedFolders.includes(f.name));
202
+ }
203
+
204
+ /**
205
+ * Prompt for root files (checkbox selection)
206
+ */
207
+ export async function promptRootFiles(
208
+ files: string[],
209
+ defaults: string[] = ['.makerc', 'readme.md', 'README.md', '.gitattributes', '.gitignore', 'Makefile', 'makefile', 'package.json'],
210
+ options: { yes?: boolean; json?: boolean } = {}
211
+ ): Promise<string[]> {
212
+ if (files.length === 0) return [];
213
+
214
+ if (options.yes || options.json) {
215
+ return files.filter(f => defaults.some(d => f.toLowerCase() === d.toLowerCase()));
216
+ }
217
+
218
+ const { selectedFiles } = await inquirer.prompt({
219
+ type: 'checkbox',
220
+ name: 'selectedFiles',
221
+ message: 'Select root files to include as boilerplate:',
222
+ loop: false,
223
+ theme: {
224
+ icon: {
225
+ checked: chalk.green('[x] '),
226
+ unchecked: '[ ] ',
227
+ }
228
+ },
229
+ choices: files.map(f => ({
230
+ name: f,
231
+ checked: defaults.some(d => f.toLowerCase() === d.toLowerCase())
232
+ }))
233
+ });
234
+
235
+ return selectedFiles;
236
+ }
237
+
238
+ /**
239
+ * Prompt for folders in structure (checkbox selection)
240
+ */
241
+ export async function promptStructureFolders(
242
+ dirs: string[],
243
+ options: { yes?: boolean; json?: boolean } = {}
244
+ ): Promise<string[]> {
245
+ if (dirs.length === 0) return [];
246
+
247
+ if (options.yes || options.json) return dirs;
248
+
249
+ const { selectedStructure } = await inquirer.prompt({
250
+ type: 'checkbox',
251
+ name: 'selectedStructure',
252
+ message: 'Select folders to include in the template structure (skeleton):',
253
+ loop: false,
254
+ theme: {
255
+ icon: {
256
+ checked: chalk.green('[x] '),
257
+ unchecked: '[ ] ',
258
+ }
259
+ },
260
+ choices: dirs.map(d => ({ name: d, checked: true }))
261
+ });
262
+
263
+ return selectedStructure;
264
+ }
265
+
266
+ /**
267
+ * Prompt for folders to copy recursively (checkbox selection)
268
+ */
269
+ export async function promptCopyFolders(
270
+ structureFolders: string[],
271
+ defaults: string[] = ['APP', 'scripts', 'bin'],
272
+ options: { yes?: boolean; json?: boolean } = {}
273
+ ): Promise<string[]> {
274
+ if (structureFolders.length === 0) return [];
275
+
276
+ if (options.yes || options.json) {
277
+ return structureFolders.filter(d => defaults.includes(d));
278
+ }
279
+
280
+ const { selectedFolders } = await inquirer.prompt({
281
+ type: 'checkbox',
282
+ name: 'selectedFolders',
283
+ message: 'Select folders to copy RECURSIVELY as boilerplate (with contents):',
284
+ loop: false,
285
+ theme: {
286
+ icon: {
287
+ checked: chalk.green('[x] '),
288
+ unchecked: '[ ] ',
289
+ }
290
+ },
291
+ choices: structureFolders.map(d => ({
292
+ name: d,
293
+ checked: defaults.includes(d)
294
+ }))
295
+ });
296
+
297
+ return selectedFolders;
298
+ }
299
+
300
+ /**
301
+ * Prompt for post-config tasks
302
+ */
303
+ export async function promptPostConfigTasks(
304
+ tasks: PostConfigTask[],
305
+ options: { yes?: boolean; json?: boolean; dryRun?: boolean } = {}
306
+ ): Promise<string[]> {
307
+ if (tasks.length === 0) return [];
308
+
309
+ if (options.dryRun) return tasks.map(t => t.command || `./${t.script}` || '');
310
+ if (options.yes || options.json) return tasks.map(t => t.command || `./${t.script}` || '');
311
+
312
+ const choices = tasks.map(t => ({
313
+ name: `${t.command || `./${t.script}` || '(no command)'}${t.description ? ` (${t.description})` : ''}`,
314
+ value: t.command || `./${t.script}` || '',
315
+ checked: true
316
+ }));
317
+
318
+ const { selected } = await inquirer.prompt({
319
+ type: 'checkbox',
320
+ name: 'selected',
321
+ message: 'Select post-config tasks to run:',
322
+ loop: false,
323
+ theme: {
324
+ icon: {
325
+ checked: chalk.green('[x] '),
326
+ unchecked: '[ ] ',
327
+ }
328
+ },
329
+ choices
330
+ });
331
+
332
+ return selected || [];
333
+ }
334
+
335
+ /**
336
+ * Prompt to add detected executables to post_copy
337
+ */
338
+ export async function promptAddPostCopy(
339
+ executables: string[],
340
+ options: { yes?: boolean; json?: boolean } = {}
341
+ ): Promise<boolean> {
342
+ if (executables.length === 0) return false;
343
+ if (options.yes || options.json) return true;
344
+
345
+ const { addPostCopy } = await inquirer.prompt({
346
+ type: 'confirm',
347
+ name: 'addPostCopy',
348
+ message: 'Add these to post_copy (auto-chmod)?',
349
+ default: true
350
+ });
351
+ return addPostCopy;
352
+ }
353
+
354
+ /**
355
+ * Print new files detection message
356
+ */
357
+ export function printNewFiles(fileCount: number, fileNames: string[]): void {
358
+ if (fileCount > 0) {
359
+ console.log(chalk.cyan(`\nšŸ“Š New Files:`));
360
+ console.log(chalk.green(` + ${fileCount} new file(s): ${fileNames.join(', ')}`));
361
+ } else {
362
+ console.log(chalk.cyan("No new files detected"));
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Print no new folders message
368
+ */
369
+ export function printNoNewFolders(): void {
370
+ console.log(chalk.cyan("No new folders detected"));
371
+ }
372
+
373
+ /**
374
+ * Print no new variables message
375
+ */
376
+ export function printNoNewVariables(): void {
377
+ console.log(chalk.cyan("No new variables detected"));
378
+ }
379
+
380
+ /**
381
+ * Prompt for new files (checkbox selection)
382
+ */
383
+ export async function promptNewFiles(
384
+ newFiles: string[],
385
+ options: { yes?: boolean; json?: boolean } = {}
386
+ ): Promise<string[]> {
387
+ if (newFiles.length === 0) return [];
388
+
389
+ if (options.yes || options.json) return newFiles;
390
+
391
+ const { selectedFileChoices } = await inquirer.prompt({
392
+ type: 'checkbox',
393
+ name: 'selectedFileChoices',
394
+ message: 'Select files to include (space to toggle):',
395
+ loop: false,
396
+ theme: {
397
+ icon: {
398
+ checked: chalk.green('[x] '),
399
+ unchecked: '[ ] ',
400
+ }
401
+ },
402
+ choices: newFiles.map(f => ({ name: f, checked: true }))
403
+ });
404
+
405
+ return newFiles.filter(f => selectedFileChoices.includes(f));
406
+ }