@garyr/pt-cli 0.36.4 โ†’ 0.39.1

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,330 @@
1
+ import { test } 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 before importing anything from the CLI
7
+ const testHome = path.join(process.cwd(), '.test-home-env');
8
+ process.env.HOME = testHome;
9
+
10
+ // Import the init command to test the env scanning functionality
11
+ import { init } from '../src/commands/initCommand.js';
12
+ import { loadConfig, saveConfig, PtConfig, getConfigPath } from '../src/config.js';
13
+
14
+ // Helper to clean up test directories
15
+ function cleanup(...paths: string[]) {
16
+ for (const p of paths) {
17
+ if (fs.existsSync(p)) {
18
+ fs.rmSync(p, { recursive: true, force: true });
19
+ }
20
+ }
21
+ }
22
+
23
+ // Helper to set up a config with a template for testing
24
+ function setupTestConfig(templateName: string, template: any): PtConfig {
25
+ const config: PtConfig = {
26
+ version: '3.0',
27
+ templates: {
28
+ [templateName]: template
29
+ }
30
+ };
31
+ saveConfig(config);
32
+ return config;
33
+ }
34
+
35
+ test('init scans parent directories for .env files and pre-fills variables', async () => {
36
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-env');
37
+ const projectDest = path.join(parentEnvDir, 'test-env-project');
38
+ const templateRoot = path.join(process.cwd(), 'test-env-tpl-root');
39
+ cleanup(projectDest, templateRoot, testHome);
40
+
41
+ // Create template root with a file containing variables
42
+ fs.mkdirSync(templateRoot, { recursive: true });
43
+ fs.writeFileSync(
44
+ path.join(templateRoot, 'README.md'),
45
+ '# {{ project_name }}\n\nBy {{ author }}\n'
46
+ );
47
+
48
+ // Set up config with template that has variables and copy_files
49
+ setupTestConfig('env-tpl', {
50
+ description: 'Template with variables',
51
+ templateRoot: templateRoot,
52
+ folders: [],
53
+ variables: [
54
+ { name: 'project_name', prompt: 'Project name:', required: true },
55
+ { name: 'author', prompt: 'Author:', default: 'Unknown' }
56
+ ],
57
+ copy_files: [
58
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
59
+ ]
60
+ });
61
+
62
+ // Create parent directory with .env file containing matching variables
63
+ fs.mkdirSync(parentEnvDir, { recursive: true });
64
+ fs.writeFileSync(
65
+ path.join(parentEnvDir, '.env'),
66
+ `project_name=EnvProject\nauthor=EnvAuthor\n`
67
+ );
68
+
69
+ // Run init from within the parent directory
70
+ await init('env-tpl', projectDest, {
71
+ yes: true,
72
+ skipPostConfig: true
73
+ });
74
+
75
+ // Verify .env variables were used
76
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
77
+ assert.ok(readme.includes('EnvProject'), 'README should contain project_name from .env');
78
+ assert.ok(readme.includes('EnvAuthor'), 'README should contain author from .env');
79
+ assert.ok(!readme.includes('{{ project_name }}'), 'README should NOT contain variable placeholder');
80
+
81
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
82
+ });
83
+
84
+ test('init --vars overrides .env variables', async () => {
85
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-override');
86
+ const projectDest = path.join(parentEnvDir, 'test-override-project');
87
+ const templateRoot = path.join(process.cwd(), 'test-override-tpl-root');
88
+ cleanup(projectDest, templateRoot, testHome);
89
+
90
+ // Create template root with a file containing variables
91
+ fs.mkdirSync(templateRoot, { recursive: true });
92
+ fs.writeFileSync(
93
+ path.join(templateRoot, 'config.txt'),
94
+ 'name={{ project_name }}\n'
95
+ );
96
+
97
+ // Set up config with template that has variables and copy_files
98
+ setupTestConfig('override-tpl', {
99
+ description: 'Template with variables',
100
+ templateRoot: templateRoot,
101
+ folders: [],
102
+ variables: [
103
+ { name: 'project_name', prompt: 'Project name:', required: true }
104
+ ],
105
+ copy_files: [
106
+ { src: 'config.txt', dest: 'config.txt', substitute_variables: true }
107
+ ]
108
+ });
109
+
110
+ // Create parent directory with .env file
111
+ fs.mkdirSync(parentEnvDir, { recursive: true });
112
+ fs.writeFileSync(
113
+ path.join(parentEnvDir, '.env'),
114
+ `project_name=EnvProject\n`
115
+ );
116
+
117
+ // Run init with --vars to override .env value
118
+ await init('override-tpl', projectDest, {
119
+ yes: true,
120
+ skipPostConfig: true,
121
+ vars: 'project_name=CLIProject'
122
+ });
123
+
124
+ // Verify --vars value was used instead of .env value
125
+ const configContent = fs.readFileSync(path.join(projectDest, 'config.txt'), 'utf-8');
126
+ assert.ok(configContent.includes('CLIProject'), 'config.txt should contain --vars value');
127
+ assert.ok(!configContent.includes('EnvProject'), 'config.txt should NOT contain .env value');
128
+
129
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
130
+ });
131
+
132
+ test('init uses .env from immediate parent directory', async () => {
133
+ const parentEnvDir = path.join(process.cwd(), 'test-immediate-parent');
134
+ const projectDest = path.join(parentEnvDir, 'test-immediate-parent-project');
135
+ const templateRoot = path.join(process.cwd(), 'test-immediate-parent-tpl-root');
136
+ cleanup(projectDest, templateRoot, testHome);
137
+
138
+ // Create template root with a file containing variables
139
+ fs.mkdirSync(templateRoot, { recursive: true });
140
+ fs.writeFileSync(
141
+ path.join(templateRoot, 'README.md'),
142
+ '# {{ project_name }}\n'
143
+ );
144
+
145
+ // Set up config with template that has variables and copy_files
146
+ setupTestConfig('immediate-parent-tpl', {
147
+ description: 'Template with variables',
148
+ templateRoot: templateRoot,
149
+ folders: [],
150
+ variables: [
151
+ { name: 'project_name', prompt: 'Project name:', required: true }
152
+ ],
153
+ copy_files: [
154
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
155
+ ]
156
+ });
157
+
158
+ // Create immediate parent directory with .env file
159
+ fs.mkdirSync(parentEnvDir, { recursive: true });
160
+ fs.writeFileSync(
161
+ path.join(parentEnvDir, '.env'),
162
+ `project_name=ImmediateParent\n`
163
+ );
164
+
165
+ // Run init from within the parent directory
166
+ await init('immediate-parent-tpl', projectDest, {
167
+ yes: true,
168
+ skipPostConfig: true
169
+ });
170
+
171
+ // Verify .env variable was used
172
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
173
+ assert.ok(readme.includes('ImmediateParent'), 'README should contain project_name from immediate parent .env');
174
+
175
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
176
+ });
177
+
178
+ test('init scans multiple parent directories for .env files', async () => {
179
+ const topEnvDir = path.join(process.cwd(), 'test-multi-parent');
180
+ const midEnvDir = path.join(topEnvDir, 'test-mid-env');
181
+ const projectDest = path.join(midEnvDir, 'test-multi-parent-project');
182
+ const templateRoot = path.join(process.cwd(), 'test-multi-parent-tpl-root');
183
+ cleanup(projectDest, templateRoot, testHome);
184
+
185
+ // Create template root with a file containing variables
186
+ fs.mkdirSync(templateRoot, { recursive: true });
187
+ fs.writeFileSync(
188
+ path.join(templateRoot, 'README.md'),
189
+ '# {{ project_name }}\n\nBy {{ author }}\n'
190
+ );
191
+
192
+ // Set up config with template that has variables and copy_files
193
+ setupTestConfig('multi-parent-tpl', {
194
+ description: 'Template with variables',
195
+ templateRoot: templateRoot,
196
+ folders: [],
197
+ variables: [
198
+ { name: 'project_name', prompt: 'Project name:', required: true },
199
+ { name: 'author', prompt: 'Author:', default: 'Unknown' }
200
+ ],
201
+ copy_files: [
202
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
203
+ ]
204
+ });
205
+
206
+ // Create nested directory structure with .env files at different levels
207
+ fs.mkdirSync(topEnvDir, { recursive: true });
208
+ fs.mkdirSync(midEnvDir, { recursive: true });
209
+
210
+ // .env file at the top level with project_name
211
+ fs.writeFileSync(
212
+ path.join(topEnvDir, '.env'),
213
+ `project_name=TopLevel\n`
214
+ );
215
+
216
+ // .env file at the middle level with author
217
+ fs.writeFileSync(
218
+ path.join(midEnvDir, '.env'),
219
+ `author=MidLevelAuthor\n`
220
+ );
221
+
222
+ // Run init from the deepest directory
223
+ await init('multi-parent-tpl', projectDest, {
224
+ yes: true,
225
+ skipPostConfig: true
226
+ });
227
+
228
+ // Verify .env variables from different levels were used
229
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
230
+ assert.ok(readme.includes('TopLevel'), 'README should contain project_name from top level .env');
231
+ assert.ok(readme.includes('MidLevelAuthor'), 'README should contain author from middle level .env');
232
+
233
+ cleanup(projectDest, templateRoot, topEnvDir, midEnvDir, testHome);
234
+ });
235
+
236
+ test('init uses .env values with quoted strings', async () => {
237
+ const parentEnvDir = path.join(process.cwd(), 'test-quoted-parent');
238
+ const projectDest = path.join(parentEnvDir, 'test-quoted-strings-project');
239
+ const templateRoot = path.join(process.cwd(), 'test-quoted-strings-tpl-root');
240
+ cleanup(projectDest, templateRoot, testHome);
241
+
242
+ // Create template root with a file containing variables
243
+ fs.mkdirSync(templateRoot, { recursive: true });
244
+ fs.writeFileSync(
245
+ path.join(templateRoot, 'README.md'),
246
+ '# {{ project_name }}\n\nBy {{ author }}\n'
247
+ );
248
+
249
+ // Set up config with template that has variables and copy_files
250
+ setupTestConfig('quoted-strings-tpl', {
251
+ description: 'Template with variables',
252
+ templateRoot: templateRoot,
253
+ folders: [],
254
+ variables: [
255
+ { name: 'project_name', prompt: 'Project name:', required: true },
256
+ { name: 'author', prompt: 'Author:', default: 'Unknown' }
257
+ ],
258
+ copy_files: [
259
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
260
+ ]
261
+ });
262
+
263
+ // Create parent directory with .env file containing quoted strings
264
+ fs.mkdirSync(parentEnvDir, { recursive: true });
265
+ fs.writeFileSync(
266
+ path.join(parentEnvDir, '.env'),
267
+ `project_name="My Quoted Project"\nauthor='Quoted Author'\n`
268
+ );
269
+
270
+ // Run init from within the parent directory
271
+ await init('quoted-strings-tpl', projectDest, {
272
+ yes: true,
273
+ skipPostConfig: true
274
+ });
275
+
276
+ // Verify quoted strings were parsed correctly
277
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
278
+ assert.ok(readme.includes('My Quoted Project'), 'README should contain project_name without quotes');
279
+ assert.ok(readme.includes('Quoted Author'), 'README should contain author without quotes');
280
+ assert.ok(!readme.includes('"My Quoted Project"'), 'README should NOT contain quotes around project_name');
281
+ assert.ok(!readme.includes("'Quoted Author'"), 'README should NOT contain quotes around author');
282
+
283
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
284
+ });
285
+
286
+ test('init uses .env values with comments and empty lines', async () => {
287
+ const parentEnvDir = path.join(process.cwd(), 'test-comments-parent');
288
+ const projectDest = path.join(parentEnvDir, 'test-comments-project');
289
+ const templateRoot = path.join(process.cwd(), 'test-comments-tpl-root');
290
+ cleanup(projectDest, templateRoot, testHome);
291
+
292
+ // Create template root with a file containing variables
293
+ fs.mkdirSync(templateRoot, { recursive: true });
294
+ fs.writeFileSync(
295
+ path.join(templateRoot, 'README.md'),
296
+ '# {{ project_name }}\n'
297
+ );
298
+
299
+ // Set up config with template that has variables and copy_files
300
+ setupTestConfig('comments-tpl', {
301
+ description: 'Template with variables',
302
+ templateRoot: templateRoot,
303
+ folders: [],
304
+ variables: [
305
+ { name: 'project_name', prompt: 'Project name:', required: true }
306
+ ],
307
+ copy_files: [
308
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
309
+ ]
310
+ });
311
+
312
+ // Create parent directory with .env file containing comments and empty lines
313
+ fs.mkdirSync(parentEnvDir, { recursive: true });
314
+ fs.writeFileSync(
315
+ path.join(parentEnvDir, '.env'),
316
+ `# This is a comment\n\nproject_name=CommentProject\n\n# Another comment\n`
317
+ );
318
+
319
+ // Run init from within the parent directory
320
+ await init('comments-tpl', projectDest, {
321
+ yes: true,
322
+ skipPostConfig: true
323
+ });
324
+
325
+ // Verify .env variable was used (comments and empty lines should be ignored)
326
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
327
+ assert.ok(readme.includes('CommentProject'), 'README should contain project_name from .env');
328
+
329
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
330
+ });
@@ -0,0 +1,173 @@
1
+ import { test } 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-final-verification');
8
+ process.env.HOME = testHome;
9
+
10
+ // Import the init command
11
+ import { init } from '../src/commands/initCommand.js';
12
+ import { loadConfig, saveConfig, PtConfig } from '../src/config.js';
13
+
14
+ // Helper to clean up test directories
15
+ function cleanup(...paths: string[]) {
16
+ for (const p of paths) {
17
+ if (fs.existsSync(p)) {
18
+ fs.rmSync(p, { recursive: true, force: true });
19
+ }
20
+ }
21
+ }
22
+
23
+ // Helper to set up a config with a template for testing
24
+ function setupTestConfig(templateName: string, template: any): PtConfig {
25
+ const config: PtConfig = {
26
+ version: '3.0',
27
+ templates: {
28
+ [templateName]: template
29
+ }
30
+ };
31
+ saveConfig(config);
32
+ return config;
33
+ }
34
+
35
+ test('FINAL VERIFICATION: Exact scenario from /mnt/production/CLIENT/RST/.env', async () => {
36
+ console.log('\n๐ŸŽฏ FINAL VERIFICATION TEST - Exact scenario from user request');
37
+ const prefixVar = 'rst_' + '{{ project }}';
38
+ console.log('๐Ÿ“ Testing: prefix=' + prefixVar + ' with project value');
39
+
40
+ // Set up the exact scenario from the user's request
41
+ const parentEnvDir = path.join(process.cwd(), 'test-final-rst');
42
+ const projectDest = path.join(parentEnvDir, 'final-rst-project');
43
+ const templateRoot = path.join(process.cwd(), 'test-final-tpl-root');
44
+ cleanup(projectDest, templateRoot, testHome);
45
+
46
+ // Create template with the exact structure from the user's request
47
+ fs.mkdirSync(templateRoot, { recursive: true });
48
+ fs.writeFileSync(
49
+ path.join(templateRoot, 'README.md'),
50
+ '# {{ prefix }}\n\nThis is a test project.\n'
51
+ );
52
+
53
+ // Set up config with template that has the nested variable example
54
+ setupTestConfig('final-rst-tpl', {
55
+ description: 'Final RST Template with nested variable expansion',
56
+ templateRoot: templateRoot,
57
+ folders: [],
58
+ variables: [
59
+ { name: 'prefix', prompt: 'Project prefix:', required: true },
60
+ { name: 'project', prompt: 'Project name:', default: 'default' }
61
+ ],
62
+ copy_files: [
63
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
64
+ ]
65
+ });
66
+
67
+ // Create parent directory with .env file containing the EXACT example from the user's request
68
+ fs.mkdirSync(parentEnvDir, { recursive: true });
69
+ fs.writeFileSync(
70
+ path.join(parentEnvDir, '.env'),
71
+ `prefix='rst_{{ project }}'\nproject=MyProject\n`
72
+ );
73
+
74
+ console.log('๐Ÿ” Running pt init with the exact scenario...');
75
+ // Run init from within the parent directory
76
+ await init('final-rst-tpl', projectDest, {
77
+ yes: true,
78
+ skipPostConfig: true
79
+ });
80
+
81
+ // Verify the nested variable expansion worked correctly
82
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
83
+
84
+ console.log('๐Ÿ“Š Test Results:');
85
+ console.log(' Input: prefix=rst_{{ project }}, project=MyProject');
86
+ console.log(' Expected: README should contain rst_MyProject');
87
+ console.log(' Actual: README contains ' + readme.trim());
88
+
89
+ // The prefix variable should be 'rst_MyProject' (after expanding {{ project }})
90
+ assert.ok(readme.includes('rst_MyProject'), 'โœ… README should contain expanded prefix value');
91
+ assert.ok(!readme.includes('{{ prefix }}'), 'โœ… README should NOT contain variable placeholder');
92
+ assert.ok(!readme.includes('{{ project }}'), 'โœ… README should NOT contain nested variable placeholder');
93
+ assert.ok(!readme.includes('rst_{{ project }}'), 'โœ… README should NOT contain unexpanded nested variable');
94
+
95
+ console.log('\n๐ŸŽ‰ SUCCESS: Nested variable expansion works exactly as requested!');
96
+ console.log(' - .env file was scanned in parent directory');
97
+ console.log(' - prefix variable was pre-filled from .env with value rst_{{ project }}');
98
+ console.log(' - Nested {{ project }} placeholder was expanded to MyProject');
99
+ console.log(' - Final result: prefix resolved to rst_MyProject');
100
+ console.log(' - No manual input needed - fully automated!');
101
+
102
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
103
+ });
104
+
105
+ test('FINAL VERIFICATION: Multiple nested variables with complex expansion', async () => {
106
+ console.log('\n๐Ÿงช BONUS TEST: Complex nested variable expansion scenario');
107
+
108
+ // Set up a more complex scenario with multiple levels of nesting
109
+ const parentEnvDir = path.join(process.cwd(), 'test-final-complex');
110
+ const projectDest = path.join(parentEnvDir, 'complex-project');
111
+ const templateRoot = path.join(process.cwd(), 'test-final-complex-tpl-root');
112
+ cleanup(projectDest, templateRoot, testHome);
113
+
114
+ // Create template with complex nested variables
115
+ fs.mkdirSync(templateRoot, { recursive: true });
116
+ fs.writeFileSync(
117
+ path.join(templateRoot, 'config.json'),
118
+ '{\n "name": "{{ prefix }}_{{ project }}",\n "version": "{{ version }}"\n}\n'
119
+ );
120
+
121
+ // Set up config with template that has multiple nested variables
122
+ setupTestConfig('complex-tpl', {
123
+ description: 'Complex nested variable test',
124
+ templateRoot: templateRoot,
125
+ folders: [],
126
+ variables: [
127
+ { name: 'prefix', prompt: 'Prefix:', required: true },
128
+ { name: 'project', prompt: 'Project:', default: 'default' },
129
+ { name: 'version', prompt: 'Version:', default: '1.0' }
130
+ ],
131
+ copy_files: [
132
+ { src: 'config.json', dest: 'config.json', substitute_variables: true }
133
+ ]
134
+ });
135
+
136
+ // Create parent directory with .env file containing complex nested variables
137
+ fs.mkdirSync(parentEnvDir, { recursive: true });
138
+ fs.writeFileSync(
139
+ path.join(parentEnvDir, '.env'),
140
+ `prefix='app_{{ env }}'\nenv=prod\nproject=MyApp\nversion=2.0\n`
141
+ );
142
+
143
+ console.log('๐Ÿ” Running complex nested variable scenario...');
144
+ // Run init from within the parent directory
145
+ await init('complex-tpl', projectDest, {
146
+ yes: true,
147
+ skipPostConfig: true
148
+ });
149
+
150
+ // Verify complex nested variables were expanded correctly
151
+ const configContent = fs.readFileSync(path.join(projectDest, 'config.json'), 'utf-8');
152
+
153
+ console.log('๐Ÿ“Š Complex Test Results:');
154
+ console.log(' Input: prefix=app_{{ env }}, env=prod, project=MyApp, version=2.0');
155
+ console.log(' Expected: name=app_prod_MyApp, version=2.0');
156
+ console.log(' Actual: ' + configContent.trim());
157
+
158
+ // The prefix variable should be 'app_prod' (after expanding {{ env }})
159
+ // The final name should be 'app_prod_MyApp'
160
+ assert.ok(configContent.includes('app_prod_MyApp'), 'โœ… config.json should contain fully expanded name');
161
+ assert.ok(configContent.includes('"2.0"'), 'โœ… config.json should contain version');
162
+ assert.ok(!configContent.includes('{{ prefix }}'), 'โœ… config.json should NOT contain variable placeholders');
163
+ assert.ok(!configContent.includes('{{ env }}'), 'โœ… config.json should NOT contain nested variable placeholders');
164
+ assert.ok(!configContent.includes('app_{{ env }}'), 'โœ… config.json should NOT contain unexpanded nested variable');
165
+
166
+ console.log('\n๐ŸŽ‰ SUCCESS: Complex nested variables work perfectly!');
167
+ console.log(' - Multiple levels of nesting were resolved');
168
+ console.log(' - prefix=app_{{ env }} expanded to app_prod');
169
+ console.log(' - Final name resolved to app_prod_MyApp');
170
+ console.log(' - All variables expanded correctly in one pass!');
171
+
172
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
173
+ });