@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,417 @@
1
+ import { test, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ // Force a temporary home directory for testing
7
+ const testHome = path.join(process.cwd(), '.test-home-update');
8
+ process.env.HOME = testHome;
9
+
10
+ import { update } from '../src/commands/updateCommand.js';
11
+ import { loadConfig, saveConfig, PtConfig, getConfigPath } 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 create a test source directory with a given structure
23
+ function createSourceDir(name: string): string {
24
+ const dir = path.join(process.cwd(), name);
25
+ cleanup(dir);
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ return dir;
28
+ }
29
+
30
+ test('update: basic additive update adds new folders', async () => {
31
+ const srcDir = createSourceDir('.test-update-folders');
32
+ cleanup(testHome);
33
+
34
+ try {
35
+ // Seed initial config with existing template
36
+ const initialConfig: PtConfig = {
37
+ version: '3.0',
38
+ templates: {
39
+ 'update-test': {
40
+ description: 'Original template',
41
+ templateRoot: '/old/path',
42
+ folders: [{ name: 'old-folder', info: 'old info', children: [] }],
43
+ copy_files: [],
44
+ variables: [{ name: 'old_var', prompt: 'Old:', required: true }]
45
+ }
46
+ }
47
+ };
48
+ saveConfig(initialConfig);
49
+
50
+ // Create new source structure with new folder
51
+ fs.mkdirSync(path.join(srcDir, 'old-folder'), { recursive: true });
52
+ fs.mkdirSync(path.join(srcDir, 'new-folder'), { recursive: true });
53
+ fs.writeFileSync(path.join(srcDir, 'README.md'), '# Test');
54
+
55
+ await update(srcDir, 'update-test', { yes: true });
56
+
57
+ const config = loadConfig();
58
+ const tpl = config.templates['update-test'];
59
+
60
+ assert.ok(tpl, 'Template should exist');
61
+ const folderNames = tpl.folders.map(f => f.name);
62
+ assert.ok(folderNames.includes('old-folder'), 'Should preserve old folder');
63
+ assert.ok(folderNames.includes('new-folder'), 'Should add new folder');
64
+ } finally {
65
+ cleanup(srcDir, testHome);
66
+ }
67
+ });
68
+
69
+ test('update: basic additive update adds new files', async () => {
70
+ const srcDir = createSourceDir('.test-update-files');
71
+ cleanup(testHome);
72
+
73
+ try {
74
+ const initialConfig: PtConfig = {
75
+ version: '3.0',
76
+ templates: {
77
+ 'update-test-files': {
78
+ description: 'Original template',
79
+ templateRoot: '/old/path',
80
+ folders: [],
81
+ copy_files: [{ src: 'existing.txt', dest: 'existing.txt', substitute_variables: true }],
82
+ variables: []
83
+ }
84
+ }
85
+ };
86
+ saveConfig(initialConfig);
87
+
88
+ fs.writeFileSync(path.join(srcDir, 'existing.txt'), 'old content');
89
+ fs.writeFileSync(path.join(srcDir, 'new-file.txt'), 'new content');
90
+ fs.writeFileSync(path.join(srcDir, 'another-new.md'), '# new');
91
+
92
+ await update(srcDir, 'update-test-files', { yes: true });
93
+
94
+ const config = loadConfig();
95
+ const tpl = config.templates['update-test-files'];
96
+
97
+ const copySrcs = tpl.copy_files!.map(cf => cf.src);
98
+ assert.ok(copySrcs.includes('existing.txt'), 'Should preserve existing file');
99
+ assert.ok(copySrcs.includes('new-file.txt'), 'Should add new file');
100
+ assert.ok(copySrcs.includes('another-new.md'), 'Should add another new file');
101
+ } finally {
102
+ cleanup(srcDir, testHome);
103
+ }
104
+ });
105
+
106
+ test('update: basic additive update adds new variables', async () => {
107
+ const srcDir = createSourceDir('.test-update-vars');
108
+ cleanup(testHome);
109
+
110
+ try {
111
+ const initialConfig: PtConfig = {
112
+ version: '3.0',
113
+ templates: {
114
+ 'update-test-vars': {
115
+ description: 'Original template',
116
+ templateRoot: '/old/path',
117
+ folders: [],
118
+ copy_files: [],
119
+ variables: [{ name: 'existing_var', prompt: 'Existing:', required: true }]
120
+ }
121
+ }
122
+ };
123
+ saveConfig(initialConfig);
124
+
125
+ fs.writeFileSync(path.join(srcDir, 'template.txt'), 'Hello {{ new_var }} and {{ existing_var }}');
126
+
127
+ await update(srcDir, 'update-test-vars', { yes: true });
128
+
129
+ const config = loadConfig();
130
+ const tpl = config.templates['update-test-vars'];
131
+
132
+ const varNames = tpl.variables!.map(v => v.name);
133
+ assert.ok(varNames.includes('existing_var'), 'Should preserve existing variable');
134
+ assert.ok(varNames.includes('new_var'), 'Should add new detected variable');
135
+ } finally {
136
+ cleanup(srcDir, testHome);
137
+ }
138
+ });
139
+
140
+ test('update: --no-diff full mode replaces template completely', async () => {
141
+ const srcDir = createSourceDir('.test-update-full');
142
+ cleanup(testHome);
143
+
144
+ try {
145
+ const initialConfig: PtConfig = {
146
+ version: '3.0',
147
+ templates: {
148
+ 'update-test-full': {
149
+ description: 'Old description',
150
+ templateRoot: '/old/path',
151
+ folders: [{ name: 'old-folder', info: 'old', children: [] }],
152
+ copy_files: [{ src: 'old.txt', dest: 'old.txt', substitute_variables: true }],
153
+ variables: [{ name: 'old_var', prompt: 'Old:', required: true }]
154
+ }
155
+ }
156
+ };
157
+ saveConfig(initialConfig);
158
+
159
+ fs.mkdirSync(path.join(srcDir, 'new-folder'), { recursive: true });
160
+ fs.writeFileSync(path.join(srcDir, 'new.txt'), 'new content');
161
+ fs.writeFileSync(path.join(srcDir, 'README.md'), '# New {{ var }}');
162
+
163
+ await update(srcDir, 'update-test-full', { yes: true, noDiff: true });
164
+
165
+ const config = loadConfig();
166
+ const tpl = config.templates['update-test-full'];
167
+
168
+ // Full mode should replace everything
169
+ const folderNames = tpl.folders.map(f => f.name);
170
+ assert.ok(folderNames.includes('new-folder'), 'Should have new folder');
171
+ assert.ok(!folderNames.includes('old-folder'), 'Should NOT have old folder');
172
+
173
+ const copySrcs = tpl.copy_files!.map(cf => cf.src);
174
+ assert.ok(copySrcs.includes('new.txt'), 'Should have new file');
175
+ assert.ok(!copySrcs.includes('old.txt'), 'Should NOT have old file');
176
+
177
+ const varNames = tpl.variables!.map(v => v.name);
178
+ assert.ok(varNames.includes('var'), 'Should have new variable');
179
+ assert.ok(!varNames.includes('old_var'), 'Should NOT have old variable');
180
+ } finally {
181
+ cleanup(srcDir, testHome);
182
+ }
183
+ });
184
+
185
+ test('update: preserves copy_files settings (substitute_variables, chmod)', async () => {
186
+ const srcDir = createSourceDir('.test-update-copyfiles');
187
+ cleanup(testHome);
188
+
189
+ try {
190
+ const initialConfig: PtConfig = {
191
+ version: '3.0',
192
+ templates: {
193
+ 'update-test-copy': {
194
+ description: 'Test',
195
+ templateRoot: '/old/path',
196
+ folders: [],
197
+ copy_files: [
198
+ { src: 'config.yaml', dest: 'config.yaml', substitute_variables: false, chmod: '644' },
199
+ { src: 'script.sh', dest: 'script.sh', substitute_variables: true, chmod: '755' }
200
+ ],
201
+ variables: []
202
+ }
203
+ }
204
+ };
205
+ saveConfig(initialConfig);
206
+
207
+ fs.writeFileSync(path.join(srcDir, 'config.yaml'), 'key: value');
208
+ fs.writeFileSync(path.join(srcDir, 'script.sh'), '#!/bin/bash\necho hello');
209
+ fs.writeFileSync(path.join(srcDir, 'new.txt'), 'new');
210
+
211
+ await update(srcDir, 'update-test-copy', { yes: true });
212
+
213
+ const config = loadConfig();
214
+ const tpl = config.templates['update-test-copy'];
215
+
216
+ // Find the preserved entries
217
+ const configEntry = tpl.copy_files!.find(cf => cf.src === 'config.yaml');
218
+ const scriptEntry = tpl.copy_files!.find(cf => cf.src === 'script.sh');
219
+ const newEntry = tpl.copy_files!.find(cf => cf.src === 'new.txt');
220
+
221
+ assert.ok(configEntry, 'Should preserve config.yaml entry');
222
+ assert.strictEqual(configEntry.substitute_variables, false, 'Should preserve substitute_variables: false');
223
+ assert.strictEqual(configEntry.chmod, '644', 'Should preserve chmod');
224
+
225
+ assert.ok(scriptEntry, 'Should preserve script.sh entry');
226
+ assert.strictEqual(scriptEntry.substitute_variables, true, 'Should preserve substitute_variables: true');
227
+ assert.strictEqual(scriptEntry.chmod, '755', 'Should preserve chmod');
228
+
229
+ assert.ok(newEntry, 'Should add new file');
230
+ assert.strictEqual(newEntry.substitute_variables, true, 'New files default to substitute_variables: true');
231
+ } finally {
232
+ cleanup(srcDir, testHome);
233
+ }
234
+ });
235
+
236
+ test('update: interactive mode can select which new items to add', async () => {
237
+ const srcDir = createSourceDir('.test-update-interactive');
238
+ cleanup(testHome);
239
+
240
+ try {
241
+ const initialConfig: PtConfig = {
242
+ version: '3.0',
243
+ templates: {
244
+ 'update-test-interactive': {
245
+ description: 'Original',
246
+ templateRoot: '/old/path',
247
+ folders: [{ name: 'existing', info: '', children: [] }],
248
+ copy_files: [{ src: 'existing.txt', dest: 'existing.txt', substitute_variables: true }],
249
+ variables: [{ name: 'existing_var', prompt: 'Existing:', required: true }]
250
+ }
251
+ }
252
+ };
253
+ saveConfig(initialConfig);
254
+
255
+ fs.mkdirSync(path.join(srcDir, 'existing'), { recursive: true });
256
+ fs.mkdirSync(path.join(srcDir, 'new-folder-a'), { recursive: true });
257
+ fs.mkdirSync(path.join(srcDir, 'new-folder-b'), { recursive: true });
258
+ fs.writeFileSync(path.join(srcDir, 'existing.txt'), 'old');
259
+ fs.writeFileSync(path.join(srcDir, 'new-a.txt'), 'a');
260
+ fs.writeFileSync(path.join(srcDir, 'new-b.txt'), 'b');
261
+ fs.writeFileSync(path.join(srcDir, 'template.txt'), '{{ new_var_a }} and {{ new_var_b }}');
262
+
263
+ // Test with --yes (auto-select all)
264
+ await update(srcDir, 'update-test-interactive', { yes: true });
265
+
266
+ const config = loadConfig();
267
+ const tpl = config.templates['update-test-interactive'];
268
+
269
+ // Should have all new folders added
270
+ const folderNames = tpl.folders.map(f => f.name);
271
+ assert.ok(folderNames.includes('existing'), 'Preserves existing');
272
+ assert.ok(folderNames.includes('new-folder-a'), 'Adds new-folder-a');
273
+ assert.ok(folderNames.includes('new-folder-b'), 'Adds new-folder-b');
274
+
275
+ // Should have all new files added
276
+ const copySrcs = tpl.copy_files!.map(cf => cf.src);
277
+ assert.ok(copySrcs.includes('existing.txt'), 'Preserves existing file');
278
+ assert.ok(copySrcs.includes('new-a.txt'), 'Adds new-a.txt');
279
+ assert.ok(copySrcs.includes('new-b.txt'), 'Adds new-b.txt');
280
+
281
+ // Should have all new variables
282
+ const varNames = tpl.variables!.map(v => v.name);
283
+ assert.ok(varNames.includes('existing_var'), 'Preserves existing var');
284
+ assert.ok(varNames.includes('new_var_a'), 'Adds new_var_a');
285
+ assert.ok(varNames.includes('new_var_b'), 'Adds new_var_b');
286
+ } finally {
287
+ cleanup(srcDir, testHome);
288
+ }
289
+ });
290
+
291
+ test('update: handles post_config scripts additively', async () => {
292
+ const srcDir = createSourceDir('.test-update-postconfig');
293
+ cleanup(testHome);
294
+
295
+ try {
296
+ const initialConfig: PtConfig = {
297
+ version: '3.0',
298
+ templates: {
299
+ 'update-test-post': {
300
+ description: 'Test',
301
+ templateRoot: '/old/path',
302
+ folders: [],
303
+ copy_files: [],
304
+ variables: [],
305
+ post_config: [{ command: 'npm install', description: 'Install deps' }]
306
+ }
307
+ }
308
+ };
309
+ saveConfig(initialConfig);
310
+
311
+ fs.writeFileSync(path.join(srcDir, 'post_config.sh'), '#!/bin/bash\necho "Running: Build"\nnpm run build\n');
312
+
313
+ await update(srcDir, 'update-test-post', { yes: true });
314
+
315
+ const config = loadConfig();
316
+ const tpl = config.templates['update-test-post'];
317
+
318
+ const commands = tpl.post_config!.map(t => t.command);
319
+ assert.ok(commands.includes('npm install'), 'Preserves existing post_config');
320
+ assert.ok(commands.includes('npm run build'), 'Adds new post_config from script');
321
+ } finally {
322
+ cleanup(srcDir, testHome);
323
+ }
324
+ });
325
+
326
+ test('update: handles post_copy executables additively', async () => {
327
+ const srcDir = createSourceDir('.test-update-postcopy');
328
+ cleanup(testHome);
329
+
330
+ try {
331
+ const initialConfig: PtConfig = {
332
+ version: '3.0',
333
+ templates: {
334
+ 'update-test-pc': {
335
+ description: 'Test',
336
+ templateRoot: '/old/path',
337
+ folders: [],
338
+ copy_files: [],
339
+ variables: [],
340
+ post_copy: [{ src: 'old.sh', dest: 'old.sh' }]
341
+ }
342
+ }
343
+ };
344
+ saveConfig(initialConfig);
345
+
346
+ fs.writeFileSync(path.join(srcDir, 'old.sh'), '#!/bin/bash\necho old');
347
+ fs.writeFileSync(path.join(srcDir, 'new.sh'), '#!/bin/bash\necho new');
348
+ fs.chmodSync(path.join(srcDir, 'old.sh'), 0o755);
349
+ fs.chmodSync(path.join(srcDir, 'new.sh'), 0o755);
350
+
351
+ await update(srcDir, 'update-test-pc', { yes: true });
352
+
353
+ const config = loadConfig();
354
+ const tpl = config.templates['update-test-pc'];
355
+
356
+ const postCopySrcs = tpl.post_copy!.map(pc => pc.src);
357
+ assert.ok(postCopySrcs.includes('old.sh'), 'Preserves existing post_copy');
358
+ assert.ok(postCopySrcs.includes('new.sh'), 'Adds new executable');
359
+
360
+ // copy_files should not include post_copy files
361
+ const copySrcs = tpl.copy_files!.map(cf => cf.src);
362
+ assert.ok(!copySrcs.includes('old.sh'), 'Does not duplicate in copy_files');
363
+ assert.ok(!copySrcs.includes('new.sh'), 'Does not duplicate new in copy_files');
364
+ } finally {
365
+ cleanup(srcDir, testHome);
366
+ }
367
+ });
368
+
369
+ test('update: JSON mode outputs template config', async () => {
370
+ const srcDir = createSourceDir('.test-update-json');
371
+ cleanup(testHome);
372
+
373
+ try {
374
+ const initialConfig: PtConfig = {
375
+ version: '3.0',
376
+ templates: {
377
+ 'update-test-json': {
378
+ description: 'Original',
379
+ templateRoot: '/old/path',
380
+ folders: [],
381
+ copy_files: [],
382
+ variables: []
383
+ }
384
+ }
385
+ };
386
+ saveConfig(initialConfig);
387
+
388
+ fs.mkdirSync(path.join(srcDir, 'src'), { recursive: true });
389
+ fs.writeFileSync(path.join(srcDir, 'README.md'), '# Test');
390
+ fs.writeFileSync(path.join(srcDir, 'template.txt'), '{{ var }}');
391
+
392
+ // Capture stdout using a proper approach
393
+ const output: string[] = [];
394
+ const originalLog = console.log;
395
+ console.log = (...args: any[]) => {
396
+ output.push(args.join(' '));
397
+ };
398
+
399
+ try {
400
+ await update(srcDir, 'update-test-json', { yes: true, json: true });
401
+ } finally {
402
+ console.log = originalLog;
403
+ }
404
+
405
+ // Find the JSON line (last line that starts with {)
406
+ const jsonLine = output.reverse().find(line => line.trim().startsWith('{'));
407
+ const parsed = JSON.parse(jsonLine!);
408
+ assert.strictEqual(parsed.name, 'update-test-json');
409
+ assert.ok(parsed.folders);
410
+ assert.ok(parsed.copy_files);
411
+ assert.ok(parsed.variables);
412
+ } finally {
413
+ cleanup(srcDir, testHome);
414
+ }
415
+ });
416
+
417
+ cleanup(testHome);