@garyr/pt-cli 0.30.0 → 0.31.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.
@@ -26,6 +26,12 @@ test('normalizeVariable key ordering', () => {
26
26
  });
27
27
 
28
28
  test('saveConfig and loadConfig roundtrip', () => {
29
+ // Save the current config to restore later
30
+ let savedConfig: PtConfig | null = null;
31
+ if (fs.existsSync(CONFIG_PATH)) {
32
+ savedConfig = loadConfig();
33
+ }
34
+
29
35
  // Clean up any existing test files
30
36
  if (fs.existsSync(CONFIG_PATH)) {
31
37
  fs.unlinkSync(CONFIG_PATH);
@@ -92,7 +98,666 @@ test('saveConfig and loadConfig roundtrip', () => {
92
98
  if (fs.existsSync(backupPath)) {
93
99
  fs.unlinkSync(backupPath);
94
100
  }
101
+
102
+ // Restore the original config if it existed
103
+ if (savedConfig) {
104
+ saveConfig(savedConfig);
105
+ }
106
+
107
+ if (fs.existsSync(testHome)) {
108
+ fs.rmdirSync(testHome);
109
+ }
110
+ });
111
+
112
+ test('normalizeVariable with minimal input', () => {
113
+ const variableInput = {
114
+ name: 'minimalVar'
115
+ };
116
+
117
+ const normalized = normalizeVariable(variableInput);
118
+
119
+ // Verify that only name is present
120
+ const keys = Object.keys(normalized);
121
+ assert.deepStrictEqual(keys, ['name']);
122
+ assert.strictEqual(normalized.name, 'minimalVar');
123
+ });
124
+
125
+ test('normalizeVariable with all fields', () => {
126
+ const variableInput = {
127
+ required: false,
128
+ default: 'default-value',
129
+ prompt: 'Custom prompt:',
130
+ name: 'allFields'
131
+ };
132
+
133
+ const normalized = normalizeVariable(variableInput);
134
+
135
+ // Verify all fields are present and in correct order
136
+ const keys = Object.keys(normalized);
137
+ assert.deepStrictEqual(keys, ['name', 'prompt', 'default', 'required']);
138
+ assert.strictEqual(normalized.name, 'allFields');
139
+ assert.strictEqual(normalized.prompt, 'Custom prompt:');
140
+ assert.strictEqual(normalized.default, 'default-value');
141
+ assert.strictEqual(normalized.required, false);
142
+ });
143
+
144
+ test('saveConfig creates backup on overwrite', () => {
145
+ // Save the current config to restore later
146
+ let savedConfig: PtConfig | null = null;
147
+ if (fs.existsSync(CONFIG_PATH)) {
148
+ savedConfig = loadConfig();
149
+ }
150
+
151
+ // Clean up any existing test files
152
+ if (fs.existsSync(CONFIG_PATH)) {
153
+ fs.unlinkSync(CONFIG_PATH);
154
+ }
155
+ const backupPath = CONFIG_PATH + '.bak';
156
+ if (fs.existsSync(backupPath)) {
157
+ fs.unlinkSync(backupPath);
158
+ }
159
+
160
+ const config1: PtConfig = {
161
+ version: '1.0.0',
162
+ templates: {}
163
+ };
164
+ const config2: PtConfig = {
165
+ version: '2.0.0',
166
+ templates: {}
167
+ };
168
+
169
+ // Save first config
170
+ saveConfig(config1);
171
+ assert.ok(fs.existsSync(CONFIG_PATH), 'Config file should be created');
172
+
173
+ // Save second config (should create backup)
174
+ saveConfig(config2);
175
+ assert.ok(fs.existsSync(backupPath), 'Backup file should exist');
176
+
177
+ // Verify backup contains first config
178
+ const backupContent = fs.readFileSync(backupPath, 'utf-8');
179
+ assert.ok(backupContent.includes("version: 1.0.0") || backupContent.includes("version: '1.0.0'"), 'Backup should contain previous version');
180
+
181
+ // Clean up
182
+ if (fs.existsSync(CONFIG_PATH)) {
183
+ fs.unlinkSync(CONFIG_PATH);
184
+ }
185
+ if (fs.existsSync(backupPath)) {
186
+ fs.unlinkSync(backupPath);
187
+ }
188
+
189
+ // Restore the original config if it existed
190
+ if (savedConfig) {
191
+ saveConfig(savedConfig);
192
+ }
193
+
194
+ if (fs.existsSync(testHome)) {
195
+ fs.rmdirSync(testHome);
196
+ }
197
+ });
198
+
199
+ test('saveConfig atomic write behavior', () => {
200
+ // Save the current config to restore later
201
+ let savedConfig: PtConfig | null = null;
202
+ if (fs.existsSync(CONFIG_PATH)) {
203
+ savedConfig = loadConfig();
204
+ }
205
+
206
+ // Clean up any existing test files
207
+ if (fs.existsSync(CONFIG_PATH)) {
208
+ fs.unlinkSync(CONFIG_PATH);
209
+ }
210
+ const backupPath = CONFIG_PATH + '.bak';
211
+ if (fs.existsSync(backupPath)) {
212
+ fs.unlinkSync(backupPath);
213
+ }
214
+
215
+ const config: PtConfig = {
216
+ version: '1.0.0',
217
+ templates: {}
218
+ };
219
+
220
+ // Save config - should use atomic write
221
+ saveConfig(config);
222
+
223
+ // Verify config exists
224
+ assert.ok(fs.existsSync(CONFIG_PATH), 'Config file should be created');
225
+
226
+ // Verify no temp file remains
227
+ const tempPath = CONFIG_PATH + '.tmp';
228
+ assert.ok(!fs.existsSync(tempPath), 'Temp file should not remain');
229
+
230
+ // Clean up
231
+ if (fs.existsSync(CONFIG_PATH)) {
232
+ fs.unlinkSync(CONFIG_PATH);
233
+ }
234
+ if (fs.existsSync(backupPath)) {
235
+ fs.unlinkSync(backupPath);
236
+ }
237
+
238
+ // Restore the original config if it existed
239
+ if (savedConfig) {
240
+ saveConfig(savedConfig);
241
+ }
242
+
243
+ if (fs.existsSync(testHome)) {
244
+ fs.rmdirSync(testHome);
245
+ }
246
+ });
247
+
248
+ test('loadConfig returns default config when file missing', () => {
249
+ // Save the current config to restore later
250
+ let savedConfig: PtConfig | null = null;
251
+ if (fs.existsSync(CONFIG_PATH)) {
252
+ savedConfig = loadConfig();
253
+ }
254
+
255
+ // Clean up any existing test files
256
+ if (fs.existsSync(CONFIG_PATH)) {
257
+ fs.unlinkSync(CONFIG_PATH);
258
+ }
259
+ const backupPath = CONFIG_PATH + '.bak';
260
+ if (fs.existsSync(backupPath)) {
261
+ fs.unlinkSync(backupPath);
262
+ }
263
+
264
+ const config = loadConfig();
265
+
266
+ // Verify default config structure
267
+ assert.strictEqual(config.version, '3.0');
268
+ assert.deepStrictEqual(config.templates, {});
269
+ assert.deepStrictEqual(config.default_post_config, []);
270
+ assert.deepStrictEqual(config.variables, []);
271
+
272
+ // Clean up
273
+ if (fs.existsSync(testHome)) {
274
+ fs.rmdirSync(testHome);
275
+ }
276
+
277
+ // Restore the original config if it existed
278
+ if (savedConfig) {
279
+ saveConfig(savedConfig);
280
+ }
281
+ });
282
+
283
+ test('saveConfig with variables normalizes key order', () => {
284
+ // Save the current config to restore later
285
+ let savedConfig: PtConfig | null = null;
286
+ if (fs.existsSync(CONFIG_PATH)) {
287
+ savedConfig = loadConfig();
288
+ }
289
+
290
+ // Clean up any existing test files
291
+ if (fs.existsSync(CONFIG_PATH)) {
292
+ fs.unlinkSync(CONFIG_PATH);
293
+ }
294
+ const backupPath = CONFIG_PATH + '.bak';
295
+ if (fs.existsSync(backupPath)) {
296
+ fs.unlinkSync(backupPath);
297
+ }
298
+
299
+ const config: PtConfig = {
300
+ version: '1.0.0',
301
+ templates: {
302
+ 'test-template': {
303
+ description: 'Test',
304
+ folders: [],
305
+ variables: [
306
+ {
307
+ required: true,
308
+ default: 'value1',
309
+ prompt: 'Prompt1:',
310
+ name: 'var1'
311
+ },
312
+ {
313
+ required: false,
314
+ default: 'value2',
315
+ prompt: 'Prompt2:',
316
+ name: 'var2'
317
+ }
318
+ ]
319
+ }
320
+ }
321
+ };
322
+
323
+ // Save config
324
+ saveConfig(config);
325
+
326
+ // Load and verify
327
+ const loaded = loadConfig();
328
+ const vars = loaded.templates['test-template'].variables;
329
+
330
+ assert.strictEqual(vars.length, 2, 'Should have 2 variables');
331
+ assert.strictEqual(vars[0].name, 'var1', 'First variable name should be var1');
332
+ assert.strictEqual(vars[1].name, 'var2', 'Second variable name should be var2');
333
+
334
+ // Clean up
335
+ if (fs.existsSync(CONFIG_PATH)) {
336
+ fs.unlinkSync(CONFIG_PATH);
337
+ }
338
+ if (fs.existsSync(backupPath)) {
339
+ fs.unlinkSync(backupPath);
340
+ }
341
+
342
+ // Restore the original config if it existed
343
+ if (savedConfig) {
344
+ saveConfig(savedConfig);
345
+ }
346
+
347
+ if (fs.existsSync(testHome)) {
348
+ fs.rmdirSync(testHome);
349
+ }
350
+ });
351
+
352
+ test('saveConfig with empty config file', () => {
353
+ // Save the current config to restore later
354
+ let savedConfig: PtConfig | null = null;
355
+ if (fs.existsSync(CONFIG_PATH)) {
356
+ savedConfig = loadConfig();
357
+ }
358
+
359
+ // Clean up any existing test files
360
+ if (fs.existsSync(CONFIG_PATH)) {
361
+ fs.unlinkSync(CONFIG_PATH);
362
+ }
363
+ const backupPath = CONFIG_PATH + '.bak';
364
+ if (fs.existsSync(backupPath)) {
365
+ fs.unlinkSync(backupPath);
366
+ }
367
+
368
+ // Create empty config file
369
+ fs.writeFileSync(CONFIG_PATH, '');
370
+
371
+ // loadConfig calls process.exit(1) on error, so we need to mock it
372
+ // to prevent the test runner from dying
373
+ let exitCalled = false;
374
+ let exitCode: number | undefined;
375
+ const originalExit = process.exit;
376
+ process.exit = ((code?: number) => {
377
+ exitCalled = true;
378
+ exitCode = code;
379
+ throw new Error('process.exit called');
380
+ }) as any;
381
+
382
+ try {
383
+ loadConfig();
384
+ assert.fail('loadConfig should have called process.exit');
385
+ } catch (e) {
386
+ assert.ok(exitCalled, 'process.exit should have been called');
387
+ assert.strictEqual(exitCode, 1, 'Should exit with code 1');
388
+ } finally {
389
+ process.exit = originalExit;
390
+ }
391
+
392
+ // Clean up
393
+ if (fs.existsSync(CONFIG_PATH)) {
394
+ fs.unlinkSync(CONFIG_PATH);
395
+ }
396
+ if (fs.existsSync(backupPath)) {
397
+ fs.unlinkSync(backupPath);
398
+ }
399
+
400
+ // Restore the original config if it existed
401
+ if (savedConfig) {
402
+ saveConfig(savedConfig);
403
+ }
404
+
405
+ if (fs.existsSync(testHome)) {
406
+ fs.rmdirSync(testHome);
407
+ }
408
+ });
409
+
410
+ test('normalizeVariable preserves field order', () => {
411
+ const variableInput = {
412
+ required: true,
413
+ default: 'my-default',
414
+ prompt: 'Enter variable:',
415
+ name: 'varName'
416
+ };
417
+
418
+ const normalized = normalizeVariable(variableInput);
419
+
420
+ // Verify field order is preserved
421
+ const keys = Object.keys(normalized);
422
+ assert.deepStrictEqual(keys, ['name', 'prompt', 'default', 'required']);
423
+ });
424
+
425
+ test('saveConfig handles multiple templates', () => {
426
+ // Save the current config to restore later
427
+ let savedConfig: PtConfig | null = null;
428
+ if (fs.existsSync(CONFIG_PATH)) {
429
+ savedConfig = loadConfig();
430
+ }
431
+
432
+ // Clean up any existing test files
433
+ if (fs.existsSync(CONFIG_PATH)) {
434
+ fs.unlinkSync(CONFIG_PATH);
435
+ }
436
+ const backupPath = CONFIG_PATH + '.bak';
437
+ if (fs.existsSync(backupPath)) {
438
+ fs.unlinkSync(backupPath);
439
+ }
440
+
441
+ const config: PtConfig = {
442
+ version: '1.0.0',
443
+ templates: {
444
+ 'template1': {
445
+ description: 'Template 1',
446
+ folders: []
447
+ },
448
+ 'template2': {
449
+ description: 'Template 2',
450
+ folders: []
451
+ }
452
+ }
453
+ };
454
+
455
+ // Save config
456
+ saveConfig(config);
457
+
458
+ // Load and verify
459
+ const loaded = loadConfig();
460
+ assert.strictEqual(Object.keys(loaded.templates).length, 2, 'Should have 2 templates');
461
+ assert.strictEqual(loaded.templates['template1'].description, 'Template 1');
462
+ assert.strictEqual(loaded.templates['template2'].description, 'Template 2');
463
+
464
+ // Clean up
465
+ if (fs.existsSync(CONFIG_PATH)) {
466
+ fs.unlinkSync(CONFIG_PATH);
467
+ }
468
+ if (fs.existsSync(backupPath)) {
469
+ fs.unlinkSync(backupPath);
470
+ }
471
+
472
+ // Restore the original config if it existed
473
+ if (savedConfig) {
474
+ saveConfig(savedConfig);
475
+ }
476
+
95
477
  if (fs.existsSync(testHome)) {
96
478
  fs.rmdirSync(testHome);
97
479
  }
98
480
  });
481
+
482
+ test('saveConfig handles templates with variables', () => {
483
+ // Save the current config to restore later
484
+ let savedConfig: PtConfig | null = null;
485
+ if (fs.existsSync(CONFIG_PATH)) {
486
+ savedConfig = loadConfig();
487
+ }
488
+
489
+ // Clean up any existing test files
490
+ if (fs.existsSync(CONFIG_PATH)) {
491
+ fs.unlinkSync(CONFIG_PATH);
492
+ }
493
+ const backupPath = CONFIG_PATH + '.bak';
494
+ if (fs.existsSync(backupPath)) {
495
+ fs.unlinkSync(backupPath);
496
+ }
497
+
498
+ const config: PtConfig = {
499
+ version: '1.0.0',
500
+ templates: {
501
+ 'template-with-vars': {
502
+ description: 'Template with variables',
503
+ folders: [],
504
+ variables: [
505
+ {
506
+ required: true,
507
+ default: 'default-value',
508
+ prompt: 'Enter value:',
509
+ name: 'myVar'
510
+ }
511
+ ]
512
+ }
513
+ }
514
+ };
515
+
516
+ // Save config
517
+ saveConfig(config);
518
+
519
+ // Load and verify
520
+ const loaded = loadConfig();
521
+ const template = loaded.templates['template-with-vars'];
522
+
523
+ assert.ok(template, 'Template should exist');
524
+ assert.strictEqual(template.variables?.length, 1, 'Should have 1 variable');
525
+ assert.strictEqual(template.variables?.[0].name, 'myVar', 'Variable name should be myVar');
526
+ assert.strictEqual(template.variables?.[0].prompt, 'Enter value:', 'Variable prompt should match');
527
+ assert.strictEqual(template.variables?.[0].default, 'default-value', 'Variable default should match');
528
+ assert.strictEqual(template.variables?.[0].required, true, 'Variable should be required');
529
+
530
+ // Clean up
531
+ if (fs.existsSync(CONFIG_PATH)) {
532
+ fs.unlinkSync(CONFIG_PATH);
533
+ }
534
+ if (fs.existsSync(backupPath)) {
535
+ fs.unlinkSync(backupPath);
536
+ }
537
+
538
+ // Restore the original config if it existed
539
+ if (savedConfig) {
540
+ saveConfig(savedConfig);
541
+ }
542
+
543
+ if (fs.existsSync(testHome)) {
544
+ fs.rmdirSync(testHome);
545
+ }
546
+ });
547
+
548
+ test('loadConfig handles migration from v2.0', () => {
549
+ // Save the current config to restore later
550
+ let savedConfig: PtConfig | null = null;
551
+ if (fs.existsSync(CONFIG_PATH)) {
552
+ savedConfig = loadConfig();
553
+ }
554
+
555
+ // Clean up any existing test files
556
+ if (fs.existsSync(CONFIG_PATH)) {
557
+ fs.unlinkSync(CONFIG_PATH);
558
+ }
559
+ const backupPath = CONFIG_PATH + '.bak';
560
+ if (fs.existsSync(backupPath)) {
561
+ fs.unlinkSync(backupPath);
562
+ }
563
+
564
+ // Create a v2.0 config
565
+ const v2Config = {
566
+ version: '2.0',
567
+ templates: {
568
+ 'test-template': {
569
+ name: 'Test Template',
570
+ type: 'web',
571
+ folders: []
572
+ }
573
+ }
574
+ };
575
+
576
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(v2Config));
577
+
578
+ // Load config - should migrate to v3.0
579
+ const config = loadConfig();
580
+
581
+ // Verify migration happened
582
+ assert.strictEqual(config.version, '3.0', 'Should be migrated to v3.0');
583
+ assert.strictEqual(config.templates['test-template'].description, 'Test Template', 'Name should be migrated to description');
584
+ assert.ok(!config.templates['test-template'].type, 'Type should be removed');
585
+
586
+ // Clean up
587
+ if (fs.existsSync(CONFIG_PATH)) {
588
+ fs.unlinkSync(CONFIG_PATH);
589
+ }
590
+ if (fs.existsSync(backupPath)) {
591
+ fs.unlinkSync(backupPath);
592
+ }
593
+
594
+ // Restore the original config if it existed
595
+ if (savedConfig) {
596
+ saveConfig(savedConfig);
597
+ }
598
+
599
+ if (fs.existsSync(testHome)) {
600
+ fs.rmdirSync(testHome);
601
+ }
602
+ });
603
+
604
+ test('loadConfig handles migration from v2.0 with global_post_config', () => {
605
+ // Save the current config to restore later
606
+ let savedConfig: PtConfig | null = null;
607
+ if (fs.existsSync(CONFIG_PATH)) {
608
+ savedConfig = loadConfig();
609
+ }
610
+
611
+ // Clean up any existing test files
612
+ if (fs.existsSync(CONFIG_PATH)) {
613
+ fs.unlinkSync(CONFIG_PATH);
614
+ }
615
+ const backupPath = CONFIG_PATH + '.bak';
616
+ if (fs.existsSync(backupPath)) {
617
+ fs.unlinkSync(backupPath);
618
+ }
619
+
620
+ // Create a v2.0 config with global_post_config
621
+ const v2Config = {
622
+ version: '2.0',
623
+ templates: {},
624
+ global_post_config: [
625
+ {
626
+ command: 'npm install',
627
+ description: 'Install dependencies'
628
+ }
629
+ ]
630
+ };
631
+
632
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(v2Config));
633
+
634
+ // Load config - should migrate to v3.0
635
+ const config = loadConfig();
636
+
637
+ // Verify migration happened
638
+ assert.strictEqual(config.version, '3.0', 'Should be migrated to v3.0');
639
+ assert.deepStrictEqual(config.default_post_config, [
640
+ {
641
+ command: 'npm install',
642
+ description: 'Install dependencies'
643
+ }
644
+ ], 'global_post_config should be migrated to default_post_config');
645
+ assert.ok(!config.global_post_config, 'global_post_config should be removed');
646
+
647
+ // Clean up
648
+ if (fs.existsSync(CONFIG_PATH)) {
649
+ fs.unlinkSync(CONFIG_PATH);
650
+ }
651
+ if (fs.existsSync(backupPath)) {
652
+ fs.unlinkSync(backupPath);
653
+ }
654
+
655
+ // Restore the original config if it existed
656
+ if (savedConfig) {
657
+ saveConfig(savedConfig);
658
+ }
659
+
660
+ if (fs.existsSync(testHome)) {
661
+ fs.rmdirSync(testHome);
662
+ }
663
+ });
664
+
665
+ test('loadConfig handles migration from v2.0 with variables', () => {
666
+ // Save the current config to restore later
667
+ let savedConfig: PtConfig | null = null;
668
+ if (fs.existsSync(CONFIG_PATH)) {
669
+ savedConfig = loadConfig();
670
+ }
671
+
672
+ // Clean up any existing test files
673
+ if (fs.existsSync(CONFIG_PATH)) {
674
+ fs.unlinkSync(CONFIG_PATH);
675
+ }
676
+ const backupPath = CONFIG_PATH + '.bak';
677
+ if (fs.existsSync(backupPath)) {
678
+ fs.unlinkSync(backupPath);
679
+ }
680
+
681
+ // Create a v2.0 config with variables as Record<string, string>
682
+ const v2Config = {
683
+ version: '2.0',
684
+ templates: {},
685
+ variables: {
686
+ 'client_name': 'My Client',
687
+ 'project_type': 'web'
688
+ }
689
+ };
690
+
691
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(v2Config));
692
+
693
+ // Load config - should migrate to v3.0
694
+ const config = loadConfig();
695
+
696
+ // Verify migration happened
697
+ assert.strictEqual(config.version, '3.0', 'Should be migrated to v3.0');
698
+ assert.strictEqual(config.variables.length, 2, 'Should have 2 variables');
699
+ assert.strictEqual(config.variables[0].name, 'client_name', 'First variable name should be client_name');
700
+ assert.strictEqual(config.variables[0].default, 'My Client', 'First variable default should be My Client');
701
+ assert.strictEqual(config.variables[1].name, 'project_type', 'Second variable name should be project_type');
702
+ assert.strictEqual(config.variables[1].default, 'web', 'Second variable default should be web');
703
+
704
+ // Clean up
705
+ if (fs.existsSync(CONFIG_PATH)) {
706
+ fs.unlinkSync(CONFIG_PATH);
707
+ }
708
+ if (fs.existsSync(backupPath)) {
709
+ fs.unlinkSync(backupPath);
710
+ }
711
+
712
+ // Restore the original config if it existed
713
+ if (savedConfig) {
714
+ saveConfig(savedConfig);
715
+ }
716
+
717
+ if (fs.existsSync(testHome)) {
718
+ fs.rmdirSync(testHome);
719
+ }
720
+ });
721
+
722
+ test('loadConfig handles existing config file', () => {
723
+ // Don't delete the existing config file - let it be restored by the user
724
+ // This test verifies that loadConfig works with an existing config
725
+ const config = loadConfig();
726
+
727
+ // Verify config is loaded correctly
728
+ assert.ok(config, 'Config should be loaded');
729
+ assert.ok(config.templates, 'Templates should exist');
730
+ assert.ok(config.version, 'Version should exist');
731
+ });
732
+
733
+ test('saveConfig preserves existing config when not deleting', () => {
734
+ // Save the current config to restore later
735
+ let savedConfig: PtConfig | null = null;
736
+ if (fs.existsSync(CONFIG_PATH)) {
737
+ savedConfig = loadConfig();
738
+ }
739
+
740
+ // Don't delete the config file - test saving without deletion
741
+ const config: PtConfig = {
742
+ version: '1.0.0',
743
+ templates: {
744
+ 'test-template': {
745
+ description: 'Test',
746
+ folders: []
747
+ }
748
+ }
749
+ };
750
+
751
+ // Save config
752
+ saveConfig(config);
753
+
754
+ // Load and verify
755
+ const loaded = loadConfig();
756
+ assert.strictEqual(loaded.version, '1.0.0');
757
+ assert.ok(loaded.templates['test-template']);
758
+
759
+ // Restore the original config if it existed
760
+ if (savedConfig) {
761
+ saveConfig(savedConfig);
762
+ }
763
+ });