@garyr/pt-cli 0.30.1 → 0.32.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.
@@ -8,18 +8,34 @@ const testHome = path.join(process.cwd(), '.test-home-init');
8
8
  process.env.HOME = testHome;
9
9
 
10
10
  import { init } from '../src/commands/initCommand.js';
11
+ import { loadConfig, saveConfig, PtConfig, CONFIG_PATH } from '../src/config.js';
12
+
13
+ // Helper to clean up test directories
14
+ function cleanup(...paths: string[]) {
15
+ for (const p of paths) {
16
+ if (fs.existsSync(p)) {
17
+ fs.rmSync(p, { recursive: true, force: true });
18
+ }
19
+ }
20
+ }
21
+
22
+ // Helper to set up a config with a template for testing
23
+ function setupTestConfig(templateName: string, template: any): PtConfig {
24
+ const config: PtConfig = {
25
+ version: '3.0',
26
+ templates: {
27
+ [templateName]: template
28
+ }
29
+ };
30
+ saveConfig(config);
31
+ return config;
32
+ }
11
33
 
12
34
  test('direct JSON template initialization via --file', async () => {
13
35
  const jsonFilePath = path.join(process.cwd(), 'test-direct-template.json');
14
36
  const projectDest = path.join(process.cwd(), 'test-scaffolded-project');
15
37
 
16
- // Ensure clean state
17
- if (fs.existsSync(jsonFilePath)) {
18
- fs.unlinkSync(jsonFilePath);
19
- }
20
- if (fs.existsSync(projectDest)) {
21
- fs.rmSync(projectDest, { recursive: true, force: true });
22
- }
38
+ cleanup(projectDest);
23
39
 
24
40
  // Create a mock template JSON configuration
25
41
  const mockTemplate = {
@@ -44,7 +60,6 @@ test('direct JSON template initialization via --file', async () => {
44
60
  fs.writeFileSync(jsonFilePath, JSON.stringify(mockTemplate, null, 2));
45
61
 
46
62
  // Run the init command with the --file option
47
- // targetName (1st arg) is omitted/undefined, destPath (2nd arg) is our projectDest, file option is provided
48
63
  await init(undefined, projectDest, {
49
64
  file: jsonFilePath,
50
65
  yes: true,
@@ -65,13 +80,412 @@ test('direct JSON template initialization via --file', async () => {
65
80
  assert.ok(infoContent.includes('mock template'), 'Should contain description');
66
81
 
67
82
  // Clean up
68
- if (fs.existsSync(jsonFilePath)) {
69
- fs.unlinkSync(jsonFilePath);
83
+ cleanup(projectDest, testHome);
84
+ });
85
+
86
+ test('init with --file and typeName as destPath shortcut', async () => {
87
+ const jsonFilePath = path.join(process.cwd(), 'test-file-shortcut.json');
88
+ const projectDest = path.join(process.cwd(), 'test-shortcut-project');
89
+
90
+ cleanup(projectDest);
91
+
92
+ const mockTemplate = {
93
+ name: 'shortcut-test',
94
+ description: 'Testing typeName-as-dest shortcut',
95
+ folders: [
96
+ { name: 'lib', info: 'library code' }
97
+ ]
98
+ };
99
+
100
+ fs.writeFileSync(jsonFilePath, JSON.stringify(mockTemplate, null, 2));
101
+
102
+ // When --file is provided and only typeName is given (no destPath),
103
+ // typeName becomes the destination path
104
+ await init(projectDest, undefined, {
105
+ file: jsonFilePath,
106
+ yes: true,
107
+ skipPostConfig: true
108
+ });
109
+
110
+ assert.ok(fs.existsSync(projectDest), 'Project should be created at typeName path');
111
+ assert.ok(fs.existsSync(path.join(projectDest, 'lib')), 'lib directory should exist');
112
+
113
+ // Clean up
114
+ fs.unlinkSync(jsonFilePath);
115
+ cleanup(projectDest, testHome);
116
+ });
117
+
118
+ test('init creates nested folder structure with .info.md files', async () => {
119
+ const projectDest = path.join(process.cwd(), 'test-nested-structure');
120
+ cleanup(projectDest);
121
+
122
+ const jsonFilePath = path.join(process.cwd(), 'test-nested-template.json');
123
+ const mockTemplate = {
124
+ name: 'nested-test',
125
+ description: 'Testing nested structure creation',
126
+ folders: [
127
+ {
128
+ name: 'src',
129
+ info: 'Source code directory',
130
+ children: [
131
+ {
132
+ name: 'models',
133
+ info: 'Data models',
134
+ children: [
135
+ { name: 'base', info: 'Base model classes' }
136
+ ]
137
+ },
138
+ { name: 'views', info: 'View templates' }
139
+ ]
140
+ },
141
+ {
142
+ name: 'tests',
143
+ info: 'Test directory'
144
+ }
145
+ ]
146
+ };
147
+
148
+ fs.writeFileSync(jsonFilePath, JSON.stringify(mockTemplate, null, 2));
149
+
150
+ await init(undefined, projectDest, {
151
+ file: jsonFilePath,
152
+ yes: true,
153
+ skipPostConfig: true
154
+ });
155
+
156
+ // Verify deeply nested structure
157
+ assert.ok(fs.existsSync(path.join(projectDest, 'src')), 'src should exist');
158
+ assert.ok(fs.existsSync(path.join(projectDest, 'src/models')), 'src/models should exist');
159
+ assert.ok(fs.existsSync(path.join(projectDest, 'src/models/base')), 'src/models/base should exist');
160
+ assert.ok(fs.existsSync(path.join(projectDest, 'src/views')), 'src/views should exist');
161
+ assert.ok(fs.existsSync(path.join(projectDest, 'tests')), 'tests should exist');
162
+
163
+ // Verify .info.md files were created for folders with info
164
+ const srcInfo = fs.readFileSync(path.join(projectDest, 'src/.info.md'), 'utf-8');
165
+ assert.ok(srcInfo.includes('Source code directory'), 'src .info.md should have correct content');
166
+
167
+ const modelsInfo = fs.readFileSync(path.join(projectDest, 'src/models/.info.md'), 'utf-8');
168
+ assert.ok(modelsInfo.includes('Data models'), 'models .info.md should have correct content');
169
+
170
+ const baseInfo = fs.readFileSync(path.join(projectDest, 'src/models/base/.info.md'), 'utf-8');
171
+ assert.ok(baseInfo.includes('Base model classes'), 'base .info.md should have correct content');
172
+
173
+ // Clean up
174
+ fs.unlinkSync(jsonFilePath);
175
+ cleanup(projectDest, testHome);
176
+ });
177
+
178
+ test('init dry run does not create files', async () => {
179
+ const projectDest = path.join(process.cwd(), 'test-dry-run-project');
180
+ cleanup(projectDest);
181
+
182
+ const jsonFilePath = path.join(process.cwd(), 'test-dryrun-template.json');
183
+ const mockTemplate = {
184
+ name: 'dryrun-test',
185
+ description: 'Testing dry run mode',
186
+ folders: [
187
+ { name: 'src', info: 'source' },
188
+ { name: 'docs', info: 'documentation' }
189
+ ]
190
+ };
191
+
192
+ fs.writeFileSync(jsonFilePath, JSON.stringify(mockTemplate, null, 2));
193
+
194
+ await init(undefined, projectDest, {
195
+ file: jsonFilePath,
196
+ yes: true,
197
+ skipPostConfig: true,
198
+ dryRun: true
199
+ });
200
+
201
+ // Dry run should NOT create the project directory
202
+ assert.ok(!fs.existsSync(projectDest), 'Project directory should NOT exist in dry run');
203
+
204
+ // Clean up
205
+ fs.unlinkSync(jsonFilePath);
206
+ cleanup(testHome);
207
+ });
208
+
209
+ test('init with saved template from config', async () => {
210
+ const projectDest = path.join(process.cwd(), 'test-saved-template-project');
211
+ cleanup(projectDest);
212
+
213
+ // Set up a config with a template
214
+ setupTestConfig('test-saved-tpl', {
215
+ description: 'A saved template for testing',
216
+ folders: [
217
+ { name: 'app', info: 'application code' },
218
+ { name: 'config', info: 'configuration files' }
219
+ ]
220
+ });
221
+
222
+ await init('test-saved-tpl', projectDest, {
223
+ yes: true,
224
+ skipPostConfig: true
225
+ });
226
+
227
+ assert.ok(fs.existsSync(projectDest), 'Project should be created');
228
+ assert.ok(fs.existsSync(path.join(projectDest, 'app')), 'app directory should exist');
229
+ assert.ok(fs.existsSync(path.join(projectDest, 'config')), 'config directory should exist');
230
+
231
+ // Verify .info.md at project root
232
+ const infoContent = fs.readFileSync(path.join(projectDest, '.info.md'), 'utf-8');
233
+ assert.ok(infoContent.includes('test-saved-tpl'), 'Root .info.md should have template name');
234
+ assert.ok(infoContent.includes('A saved template for testing'), 'Root .info.md should have description');
235
+
236
+ // Clean up
237
+ cleanup(projectDest, testHome);
238
+ });
239
+
240
+ test('init with variables via --vars option', async () => {
241
+ const projectDest = path.join(process.cwd(), 'test-vars-project');
242
+ const templateRoot = path.join(process.cwd(), 'test-vars-template-root');
243
+ cleanup(projectDest, templateRoot);
244
+
245
+ // Create template root with a file containing variables
246
+ fs.mkdirSync(templateRoot, { recursive: true });
247
+ fs.writeFileSync(
248
+ path.join(templateRoot, 'README.md'),
249
+ '# {{ project_name }}\n\nBy {{ author }}\n'
250
+ );
251
+
252
+ // Set up config with template that has variables and copy_files
253
+ setupTestConfig('vars-tpl', {
254
+ description: 'Template with variables',
255
+ templateRoot: templateRoot,
256
+ folders: [],
257
+ variables: [
258
+ { name: 'project_name', prompt: 'Project name:', required: true },
259
+ { name: 'author', prompt: 'Author:', default: 'Unknown' }
260
+ ],
261
+ copy_files: [
262
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
263
+ ]
264
+ });
265
+
266
+ await init('vars-tpl', projectDest, {
267
+ yes: true,
268
+ skipPostConfig: true,
269
+ vars: 'project_name=MyProject,author=TestAuthor'
270
+ });
271
+
272
+ assert.ok(fs.existsSync(projectDest), 'Project should be created');
273
+
274
+ // Verify variable substitution in copied file
275
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
276
+ assert.ok(readme.includes('MyProject'), 'README should contain substituted project_name');
277
+ assert.ok(readme.includes('TestAuthor'), 'README should contain substituted author');
278
+ assert.ok(!readme.includes('{{ project_name }}'), 'README should NOT contain variable placeholder');
279
+
280
+ // Clean up
281
+ cleanup(projectDest, templateRoot, testHome);
282
+ });
283
+
284
+ test('init with variables uses defaults in --yes mode', async () => {
285
+ const projectDest = path.join(process.cwd(), 'test-vars-default-project');
286
+ const templateRoot = path.join(process.cwd(), 'test-vars-default-tpl-root');
287
+ cleanup(projectDest, templateRoot);
288
+
289
+ fs.mkdirSync(templateRoot, { recursive: true });
290
+ fs.writeFileSync(
291
+ path.join(templateRoot, 'config.txt'),
292
+ 'env={{ environment }}\n'
293
+ );
294
+
295
+ setupTestConfig('vars-default-tpl', {
296
+ description: 'Template with default variables',
297
+ templateRoot: templateRoot,
298
+ folders: [],
299
+ variables: [
300
+ { name: 'environment', prompt: 'Environment:', default: 'development', required: false }
301
+ ],
302
+ copy_files: [
303
+ { src: 'config.txt', dest: 'config.txt', substitute_variables: true }
304
+ ]
305
+ });
306
+
307
+ // Run without providing --vars; in --yes mode, defaults should be used
308
+ await init('vars-default-tpl', projectDest, {
309
+ yes: true,
310
+ skipPostConfig: true
311
+ });
312
+
313
+ const configContent = fs.readFileSync(path.join(projectDest, 'config.txt'), 'utf-8');
314
+ assert.ok(configContent.includes('env=development'), 'Should use default variable value');
315
+
316
+ cleanup(projectDest, templateRoot, testHome);
317
+ });
318
+
319
+ test('init with post_copy handles executables', async () => {
320
+ const projectDest = path.join(process.cwd(), 'test-postcopy-project');
321
+ const templateRoot = path.join(process.cwd(), 'test-postcopy-tpl-root');
322
+ cleanup(projectDest, templateRoot);
323
+
324
+ fs.mkdirSync(templateRoot, { recursive: true });
325
+ fs.writeFileSync(path.join(templateRoot, 'setup.sh'), '#!/bin/bash\necho "setup"\n');
326
+
327
+ setupTestConfig('postcopy-tpl', {
328
+ description: 'Template with post_copy',
329
+ templateRoot: templateRoot,
330
+ folders: [
331
+ { name: 'bin', info: 'executables' }
332
+ ],
333
+ post_copy: [
334
+ { src: 'setup.sh', dest: 'setup.sh' }
335
+ ]
336
+ });
337
+
338
+ await init('postcopy-tpl', projectDest, {
339
+ yes: true,
340
+ skipPostConfig: true
341
+ });
342
+
343
+ assert.ok(fs.existsSync(projectDest), 'Project should be created');
344
+ assert.ok(fs.existsSync(path.join(projectDest, 'setup.sh')), 'setup.sh should be copied');
345
+
346
+ // Verify content was copied
347
+ const content = fs.readFileSync(path.join(projectDest, 'setup.sh'), 'utf-8');
348
+ assert.ok(content.includes('echo "setup"'), 'setup.sh should have correct content');
349
+
350
+ // Verify it was made executable (on Linux/macOS)
351
+ if (process.platform !== 'win32') {
352
+ const stat = fs.statSync(path.join(projectDest, 'setup.sh'));
353
+ assert.ok(stat.mode & 0o111, 'setup.sh should be executable');
70
354
  }
71
- if (fs.existsSync(projectDest)) {
72
- fs.rmSync(projectDest, { recursive: true, force: true });
355
+
356
+ cleanup(projectDest, templateRoot, testHome);
357
+ });
358
+
359
+ test('init with missing templateRoot warns but creates structure', async () => {
360
+ const projectDest = path.join(process.cwd(), 'test-missing-root-project');
361
+ cleanup(projectDest);
362
+
363
+ setupTestConfig('missing-root-tpl', {
364
+ description: 'Template with missing root',
365
+ templateRoot: '/tmp/nonexistent-pt-test-dir-' + Date.now(),
366
+ folders: [
367
+ { name: 'src', info: 'source code' },
368
+ { name: 'lib', info: 'library code' }
369
+ ],
370
+ copy_files: [
371
+ { src: 'README.md', dest: 'README.md' }
372
+ ]
373
+ });
374
+
375
+ // Should still create the folder structure even though templateRoot is missing
376
+ await init('missing-root-tpl', projectDest, {
377
+ yes: true,
378
+ skipPostConfig: true
379
+ });
380
+
381
+ assert.ok(fs.existsSync(projectDest), 'Project should still be created');
382
+ assert.ok(fs.existsSync(path.join(projectDest, 'src')), 'src directory should exist');
383
+ assert.ok(fs.existsSync(path.join(projectDest, 'lib')), 'lib directory should exist');
384
+ // But README.md should NOT exist since templateRoot doesn't exist
385
+ assert.ok(!fs.existsSync(path.join(projectDest, 'README.md')), 'README.md should NOT exist');
386
+
387
+ cleanup(projectDest, testHome);
388
+ });
389
+
390
+ test('init fails for non-existent template name', async () => {
391
+ const projectDest = path.join(process.cwd(), 'test-nonexistent-tpl');
392
+ cleanup(projectDest);
393
+
394
+ // Set up empty config
395
+ setupTestConfig('existing-tpl', {
396
+ description: 'Some template',
397
+ folders: []
398
+ });
399
+
400
+ // Mock process.exit so the test doesn't die
401
+ let exitCalled = false;
402
+ let exitCode: number | undefined;
403
+ const originalExit = process.exit;
404
+ process.exit = ((code?: number) => {
405
+ exitCalled = true;
406
+ exitCode = code;
407
+ throw new Error('process.exit called');
408
+ }) as any;
409
+
410
+ try {
411
+ await init('nonexistent-template', projectDest, {
412
+ yes: true,
413
+ skipPostConfig: true
414
+ });
415
+ assert.fail('Should have called process.exit');
416
+ } catch (e) {
417
+ assert.ok(exitCalled, 'process.exit should have been called');
418
+ assert.strictEqual(exitCode, 1, 'Should exit with code 1');
419
+ } finally {
420
+ process.exit = originalExit;
73
421
  }
74
- if (fs.existsSync(testHome)) {
75
- fs.rmSync(testHome, { recursive: true, force: true });
422
+
423
+ cleanup(projectDest, testHome);
424
+ });
425
+
426
+ test('init fails when destination already exists', async () => {
427
+ const projectDest = path.join(process.cwd(), 'test-existing-dest');
428
+
429
+ // Create the destination first
430
+ fs.mkdirSync(projectDest, { recursive: true });
431
+
432
+ setupTestConfig('exists-tpl', {
433
+ description: 'A template',
434
+ folders: [{ name: 'src', info: '' }]
435
+ });
436
+
437
+ let exitCalled = false;
438
+ const originalExit = process.exit;
439
+ process.exit = ((code?: number) => {
440
+ exitCalled = true;
441
+ throw new Error('process.exit called');
442
+ }) as any;
443
+
444
+ try {
445
+ await init('exists-tpl', projectDest, {
446
+ yes: true,
447
+ skipPostConfig: true
448
+ });
449
+ assert.fail('Should have called process.exit');
450
+ } catch (e) {
451
+ assert.ok(exitCalled, 'process.exit should have been called for existing destination');
452
+ } finally {
453
+ process.exit = originalExit;
76
454
  }
455
+
456
+ cleanup(projectDest, testHome);
457
+ });
458
+
459
+ test('init with copy_files copies directory recursively', async () => {
460
+ const projectDest = path.join(process.cwd(), 'test-recursive-copy-project');
461
+ const templateRoot = path.join(process.cwd(), 'test-recursive-copy-tpl-root');
462
+ cleanup(projectDest, templateRoot);
463
+
464
+ // Create template root with a nested directory structure
465
+ fs.mkdirSync(path.join(templateRoot, 'scripts', 'helpers'), { recursive: true });
466
+ fs.writeFileSync(path.join(templateRoot, 'scripts', 'build.sh'), '#!/bin/bash\necho "building"');
467
+ fs.writeFileSync(path.join(templateRoot, 'scripts', 'helpers', 'utils.sh'), '#!/bin/bash\necho "utils"');
468
+
469
+ setupTestConfig('recursive-tpl', {
470
+ description: 'Template with recursive copy',
471
+ templateRoot: templateRoot,
472
+ folders: [],
473
+ copy_files: [
474
+ { src: 'scripts', dest: 'scripts', substitute_variables: false }
475
+ ]
476
+ });
477
+
478
+ await init('recursive-tpl', projectDest, {
479
+ yes: true,
480
+ skipPostConfig: true
481
+ });
482
+
483
+ assert.ok(fs.existsSync(path.join(projectDest, 'scripts')), 'scripts dir should exist');
484
+ assert.ok(fs.existsSync(path.join(projectDest, 'scripts', 'build.sh')), 'build.sh should exist');
485
+ assert.ok(fs.existsSync(path.join(projectDest, 'scripts', 'helpers', 'utils.sh')), 'nested utils.sh should exist');
486
+
487
+ const content = fs.readFileSync(path.join(projectDest, 'scripts', 'build.sh'), 'utf-8');
488
+ assert.ok(content.includes('echo "building"'), 'build.sh should have correct content');
489
+
490
+ cleanup(projectDest, templateRoot, testHome);
77
491
  });