@garyr/pt-cli 0.25.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,41 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { loadConfig, saveConfig } from '../config.js';
5
+
6
+ export interface AddOptions {
7
+ file?: string;
8
+ }
9
+
10
+ export function addCommand(name: string, jsonStr: string | undefined, options: AddOptions = {}) {
11
+ const config = loadConfig();
12
+ try {
13
+ let data;
14
+ if (options.file) {
15
+ const filePath = path.resolve(options.file);
16
+ data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
17
+ } else if (jsonStr) {
18
+ data = JSON.parse(jsonStr);
19
+ } else {
20
+ console.error('Error: Either a JSON string or --file <path> must be provided.');
21
+ process.exit(1);
22
+ }
23
+
24
+ if (!config.templates) config.templates = {};
25
+
26
+ // Basic validation: ensure we aren't accidentally adding a full config object
27
+ if (data && data.templates && typeof data.templates === 'object') {
28
+ console.error(chalk.red('Error: The provided JSON appears to be a full configuration file, not a single template.'));
29
+ console.error(chalk.gray('If you want to import a specific template from it, extract that template object first.'));
30
+ process.exit(1);
31
+ }
32
+
33
+ config.templates[name] = data;
34
+ saveConfig(config);
35
+ console.log(chalk.green(`āœ“ Template "${name}" saved successfully.`));
36
+ } catch (e) {
37
+ const error = e as Error;
38
+ console.error(chalk.red(`Failed to parse template JSON: ${error.message}`));
39
+ process.exit(1);
40
+ }
41
+ }
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, getTemplateNames, CONFIG_PATH } from '../config.js';
3
+
4
+ export interface ConfigOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ export function configCommand(templateName: string | undefined, options: ConfigOptions = {}) {
9
+ const config = loadConfig();
10
+
11
+ if (options.json) {
12
+ if (templateName) {
13
+ if (config.templates && config.templates[templateName]) {
14
+ const output = {
15
+ name: templateName,
16
+ ...config.templates[templateName]
17
+ };
18
+ console.log(JSON.stringify(output, null, 2));
19
+ } else {
20
+ console.error(chalk.red(`Error: Template "${templateName}" not found.`));
21
+ process.exit(1);
22
+ }
23
+ } else {
24
+ console.log(JSON.stringify(config, null, 2));
25
+ }
26
+ return;
27
+ }
28
+
29
+ const names = getTemplateNames(config);
30
+
31
+ console.log(chalk.cyan('Config Location:'), CONFIG_PATH);
32
+ console.log(chalk.cyan('\nLearned Templates:'));
33
+ if (names.length === 0) {
34
+ console.log(chalk.gray(' (none)'));
35
+ } else {
36
+ for (const name of names) {
37
+ const t = config.templates[name];
38
+ if (!t) continue;
39
+ console.log(chalk.white(` - ${name}`), chalk.gray(`(${t.description})`));
40
+ if (t.templateRoot) {
41
+ console.log(chalk.gray(` Source: ${t.templateRoot}`));
42
+ }
43
+ if (t.post_config && t.post_config.length > 0) {
44
+ console.log(chalk.cyan(' Post-config:'));
45
+ for (const task of t.post_config) {
46
+ const cmd = task.command || task.script || '(unknown)';
47
+ const typeFilter = task.type ? ` [type: ${task.type}]` : '';
48
+ console.log(chalk.gray(` - ${cmd}${typeFilter}`));
49
+ }
50
+ }
51
+ if (t.post_copy && t.post_copy.length > 0) {
52
+ console.log(chalk.cyan(' post_copy:'));
53
+ for (const f of t.post_copy) {
54
+ console.log(chalk.gray(` - ${f.src} → ${(f.dest || f.src)}`));
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ // Show global ignore patterns
61
+ if (config.ignore && config.ignore.length > 0) {
62
+ console.log(chalk.cyan('\nIgnore Patterns (pt learn):'));
63
+ for (const p of config.ignore) {
64
+ console.log(chalk.gray(` - ${p}`));
65
+ }
66
+ }
67
+
68
+ // Show default post-config tasks
69
+ if (config.default_post_config && config.default_post_config.length > 0) {
70
+ console.log(chalk.cyan('\nDefault Post-Config Tasks:'));
71
+ for (const task of config.default_post_config) {
72
+ const cmd = task.command || task.script || '(unknown)';
73
+ const desc = task.description ? ` — ${task.description}` : '';
74
+ const checked = task.checked !== false ? '[default: on]' : '[default: off]';
75
+ const typeFilter = task.type ? ` [type: ${task.type}]` : '';
76
+ console.log(chalk.gray(` - ${cmd}${desc}`));
77
+ console.log(chalk.gray(` ${checked}${typeFilter}`));
78
+ }
79
+ }
80
+
81
+ // Show global variables
82
+ if (config.variables && config.variables.length > 0) {
83
+ console.log(chalk.cyan('\nGlobal Variables:'));
84
+ for (const v of config.variables) {
85
+ console.log(chalk.white(` - ${v.name}:`), chalk.gray(v.default || '(no default)'));
86
+ if (v.prompt) console.log(chalk.gray(` Prompt: ${v.prompt}`));
87
+ if (v.required) console.log(chalk.yellow(` [Required]`));
88
+ }
89
+ }
90
+
91
+ console.log(chalk.cyan('\nExample post-config in config.yaml:'));
92
+ console.log(chalk.gray(`
93
+ my_template:
94
+ description: "My standard web project"
95
+ post_config:
96
+ - command: "git init"
97
+ description: "Initialize git repository"
98
+ - command: "npm install"
99
+ description: "Install npm dependencies"
100
+ type: "javascript"
101
+ `));
102
+ }
@@ -0,0 +1,17 @@
1
+ import { loadConfig, saveConfig } from '../config.js';
2
+
3
+ export interface IgnoreOptions {
4
+ set?: boolean;
5
+ }
6
+
7
+ export function ignoreCommand(patterns: string | undefined, options: IgnoreOptions = {}) {
8
+ const config = loadConfig();
9
+
10
+ if (options.set) {
11
+ config.ignore = patterns ? patterns.split(',').map((s: string) => s.trim()).filter((s: string) => s !== '') : [];
12
+ saveConfig(config);
13
+ console.log('Ignore patterns updated:', config.ignore);
14
+ } else {
15
+ console.log('Current ignore patterns:', config.ignore || []);
16
+ }
17
+ }
@@ -0,0 +1,311 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import inquirer from 'inquirer';
4
+ import { loadConfig, FolderNode } from '../config.js';
5
+ import chalk from 'chalk';
6
+ import { processCopyFiles } from '../substitute.js';
7
+ import { execSync } from 'child_process';
8
+
9
+ export interface InitOptions {
10
+ skipPostConfig?: boolean;
11
+ dryRun?: boolean;
12
+ yes?: boolean;
13
+ vars?: string;
14
+ }
15
+
16
+ export async function init(targetName: string | undefined, destPath: string | undefined, options: InitOptions = {}) {
17
+ const config = loadConfig();
18
+
19
+ let typeName: string | undefined = targetName;
20
+
21
+ // If no name provided, list templates
22
+ if (!typeName) {
23
+ const names = Object.keys(config.templates);
24
+ if (names.length === 0) {
25
+ console.log(chalk.red("No templates found. Run 'pt learn <path>' first."));
26
+ return;
27
+ }
28
+
29
+ if (options.yes) {
30
+ console.error(chalk.red("No project type specified and running in non-interactive mode."));
31
+ process.exit(1);
32
+ }
33
+ const { selected } = await inquirer.prompt({
34
+ type: 'list',
35
+ name: 'selected',
36
+ message: 'Select Project Type:',
37
+ loop: false,
38
+ theme: {
39
+ icon: {
40
+ cursor: chalk.green('[x] ')
41
+ }
42
+ },
43
+ choices: names.map(n => ({ name: n, value: n }))
44
+ });
45
+ typeName = selected;
46
+ }
47
+
48
+ const template = config.templates[typeName!];
49
+ if (!template) {
50
+ console.error(chalk.red(`Template "${typeName}" not found.`));
51
+ process.exit(1);
52
+ }
53
+
54
+ let dest: string | undefined = destPath;
55
+ if (!dest) {
56
+ if (options.yes) {
57
+ console.error(chalk.red("No destination path specified and running in non-interactive mode."));
58
+ process.exit(1);
59
+ }
60
+ const { name } = await inquirer.prompt({
61
+ type: 'input',
62
+ name: 'name',
63
+ message: 'Project path/folder name:'
64
+ });
65
+ dest = name;
66
+ }
67
+
68
+ const resolvedDest = path.resolve(dest!);
69
+
70
+ if (fs.existsSync(resolvedDest) && !options.dryRun) {
71
+ console.error(chalk.red(`Error: Destination "${resolvedDest}" already exists.`));
72
+ process.exit(1);
73
+ }
74
+
75
+ if (options.dryRun) {
76
+ console.log(chalk.yellow(`\n[DRY RUN] Initializing project "${template.description}" at: ${resolvedDest}`));
77
+ } else {
78
+ console.log(chalk.cyan(`\nInitializing project "${template.description}" at: ${resolvedDest}`));
79
+ }
80
+
81
+ // Handle Variables
82
+ let variables: Record<string, string> = {};
83
+ if (template.variables && template.variables.length > 0) {
84
+ if (options.vars) {
85
+ // Parse --vars "key=val,key2=val2"
86
+ const pairs = options.vars.split(',').map((p: string) => p.trim());
87
+ for (const pair of pairs) {
88
+ const [k, ...v] = pair.split('=');
89
+ if (k && v.length > 0) {
90
+ variables[k.trim()] = v.join('=').trim();
91
+ }
92
+ }
93
+ }
94
+
95
+ if (!options.yes) {
96
+ // Prompt for any missing variables
97
+ for (const v of template.variables) {
98
+ if (!variables[v.name]) {
99
+ const answer = await inquirer.prompt({
100
+ type: 'input',
101
+ name: v.name,
102
+ message: v.prompt || `Enter ${v.name}:`,
103
+ default: v.default || ''
104
+ });
105
+ variables[v.name] = answer[v.name];
106
+ }
107
+ }
108
+ } else {
109
+ // Non-interactive mode: check required
110
+ for (const v of template.variables) {
111
+ if (!variables[v.name]) {
112
+ if (v.required) {
113
+ console.error(chalk.red(`Error: Variable "${v.name}" is required but was not provided in non-interactive mode. Use --vars ${v.name}=value`));
114
+ process.exit(1);
115
+ } else {
116
+ variables[v.name] = v.default || '';
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // 1. Create structure
124
+ createStructure(resolvedDest, template.folders, options.dryRun);
125
+
126
+ // Check if templateRoot exists (if it's defined)
127
+ const templateRootExists = template.templateRoot && fs.existsSync(template.templateRoot);
128
+ if (template.templateRoot && !templateRootExists) {
129
+ console.warn(chalk.yellow(`\nWarning: Template source directory not found: ${template.templateRoot}`));
130
+ console.warn(chalk.gray("Folder structure created, but files/boilerplate will be skipped."));
131
+ }
132
+
133
+ // 2. Process copy_files
134
+ if (template.copy_files && templateRootExists) {
135
+ if (options.dryRun) console.log(chalk.yellow("[DRY RUN] Processing copy_files..."));
136
+ else console.log(chalk.cyan("Processing copy_files..."));
137
+ await processCopyFiles(template.templateRoot!, resolvedDest, template, variables, options.dryRun);
138
+ }
139
+
140
+
141
+ // 3. Process post_copy (executable scripts)
142
+ if (template.post_copy && templateRootExists) {
143
+ if (options.dryRun) console.log(chalk.yellow("[DRY RUN] Processing post_copy..."));
144
+ else console.log(chalk.cyan("Processing post_copy..."));
145
+
146
+ for (const file of template.post_copy) {
147
+ const srcPath = path.join(template.templateRoot!, file.src);
148
+ const destPath = path.join(resolvedDest, file.dest || file.src);
149
+
150
+ if (fs.existsSync(srcPath)) {
151
+ if (options.dryRun) {
152
+ console.log(chalk.gray(` [DRY RUN] Would copy ${file.src} → ${file.dest || file.src}`));
153
+ const ext = path.extname(file.src);
154
+ if (['.sh', '.py', '.bash', '.bat'].includes(ext)) {
155
+ console.log(chalk.gray(` [DRY RUN] Would chmod +x ${file.dest || file.src}`));
156
+ }
157
+ continue;
158
+ }
159
+
160
+ const fileContent = fs.readFileSync(srcPath, 'utf-8');
161
+ const destDir = path.dirname(destPath);
162
+ fs.mkdirSync(destDir, { recursive: true });
163
+ fs.writeFileSync(destPath, fileContent);
164
+
165
+ // Auto-chmod for executables
166
+ const ext = path.extname(file.src);
167
+ if (['.sh', '.py', '.bash', '.bat'].includes(ext)) {
168
+ try {
169
+ fs.chmodSync(destPath, 0o755);
170
+ } catch (e) {
171
+ // chmod not available (Windows)
172
+ }
173
+ }
174
+ console.log(chalk.green(" āœ“ " + (file.dest || file.src)));
175
+ } else {
176
+ console.warn(chalk.yellow(" ! " + file.src + " not found, skipping"));
177
+ }
178
+ }
179
+ }
180
+ // Write .info.md
181
+ if (!options.dryRun) {
182
+ const infoContent = `# ${typeName}\n\n${template.description || ''}\n`;
183
+ fs.writeFileSync(path.join(resolvedDest, '.info.md'), infoContent);
184
+ } else {
185
+ console.log(chalk.gray(` [DRY RUN] Would create .info.md`));
186
+ }
187
+
188
+ // Use template post_config tasks
189
+ const allTasks = template.post_config?.filter(t => !t.type || t.type === typeName!) || [];
190
+
191
+ if (allTasks.length > 0) {
192
+ // Determine which tasks to include
193
+ let selectedTaskNames: string[] = [];
194
+
195
+ if (options.skipPostConfig) {
196
+ // Skip entirely
197
+ selectedTaskNames = [];
198
+ } else if (options.dryRun) {
199
+ // In dry-run, select all (for display)
200
+ selectedTaskNames = allTasks.map(t => t.command || t.script || '');
201
+ console.log(chalk.yellow(`\n[DRY RUN] Applicable post-config tasks:`));
202
+ for (const t of allTasks) {
203
+ const desc = t.description ? ` (${t.description})` : '';
204
+ console.log(chalk.gray(` [template] - ${t.command || t.script}${desc}`));
205
+ }
206
+ } else if (options.yes) {
207
+ // All tasks selected
208
+ selectedTaskNames = allTasks.map(t => t.command || t.script || '');
209
+ } else if (allTasks.length === 0) {
210
+ selectedTaskNames = [];
211
+ } else {
212
+ // Checkbox prompt
213
+ const choices: Array<{name: string; value: string; checked?: boolean}> = [];
214
+
215
+ for (const t of allTasks) {
216
+ const cmd = t.command || t.script || '(no command)';
217
+ const desc = t.description ? ` (${t.description})` : '';
218
+ choices.push({
219
+ name: `${cmd}${desc}`,
220
+ value: cmd,
221
+ checked: true
222
+ });
223
+ }
224
+
225
+ const response = await inquirer.prompt({
226
+ type: 'checkbox',
227
+ name: 'selected',
228
+ message: 'Select post-config tasks to run:',
229
+ loop: false,
230
+ theme: {
231
+ icon: {
232
+ cursor: chalk.green('[x] ')
233
+ }
234
+ },
235
+ choices
236
+ });
237
+ selectedTaskNames = response.selected || [];
238
+ }
239
+
240
+ // Write post_config scripts for selected tasks
241
+ if (selectedTaskNames.length > 0 && !options.dryRun) {
242
+ let bashContent = '#!/bin/bash\n# Auto-generated post_config script\n\n';
243
+ let batContent = '@echo off\n:: Auto-generated post_config script\n\n';
244
+ for (const t of allTasks) {
245
+ // Determine the actual command/script to use
246
+ let cmd = '';
247
+ if (t.command) {
248
+ cmd = t.command;
249
+ } else if (t.script) {
250
+ cmd = `./${t.script}`;
251
+ }
252
+ // Match against selected names (use command if available, else script)
253
+ const taskKey = t.command || (t.script ? `./${t.script}` : '');
254
+ if (selectedTaskNames.includes(taskKey)) {
255
+ if (cmd) {
256
+ bashContent += `echo "Running: ${t.description || taskKey}"\n${cmd}\n`;
257
+ batContent += `echo Running: ${t.description || taskKey}\n${cmd}\n`;
258
+ }
259
+ }
260
+ }
261
+ fs.writeFileSync(path.join(resolvedDest, 'post_config.sh'), bashContent);
262
+ try { fs.chmodSync(path.join(resolvedDest, 'post_config.sh'), 0o755); } catch(e) {}
263
+ fs.writeFileSync(path.join(resolvedDest, 'post_config.bat'), batContent);
264
+
265
+ // Execute the appropriate script
266
+ console.log(chalk.cyan("\nExecuting post-config tasks..."));
267
+ try {
268
+ const scriptCmd = process.platform === 'win32' ? 'post_config.bat' : './post_config.sh';
269
+ execSync(scriptCmd, {
270
+ cwd: resolvedDest,
271
+ stdio: 'inherit'
272
+ });
273
+ } catch (e) {
274
+ console.error(chalk.red("\nError: Some post-config tasks failed. Check the output above."));
275
+ }
276
+ }
277
+ }
278
+
279
+ if (options.dryRun) {
280
+ console.log(chalk.yellow(`\n[DRY RUN] Project initialization preview complete.`));
281
+ } else {
282
+ console.log(chalk.green(`\nāœ“ Project created successfully.`));
283
+ }
284
+ }
285
+
286
+ function createStructure(dirPath: string, folders: FolderNode[], dryRun: boolean = false) {
287
+ for (const folder of folders) {
288
+ const fullDirPath = path.join(dirPath, folder.name);
289
+
290
+ if (dryRun) {
291
+ console.log(chalk.gray(` [DRY RUN] Would create directory: ${fullDirPath}`));
292
+ } else {
293
+ fs.mkdirSync(fullDirPath, { recursive: true });
294
+ }
295
+
296
+ // Create .info.md if content exists
297
+ if (folder.info) {
298
+ const infoPath = path.join(fullDirPath, '.info.md');
299
+ if (dryRun) {
300
+ console.log(chalk.gray(` [DRY RUN] Would create info file: ${infoPath}`));
301
+ } else {
302
+ fs.writeFileSync(infoPath, folder.info);
303
+ }
304
+ }
305
+
306
+ // Recurse children
307
+ if (folder.children && folder.children.length > 0) {
308
+ createStructure(fullDirPath, folder.children, dryRun);
309
+ }
310
+ }
311
+ }