@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,242 @@
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-nested');
8
+ process.env.HOME = testHome;
9
+
10
+ // Import the init command to test the nested variable expansion functionality
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('nested variable expansion: prefix variable with nested {{ project }} reference', async () => {
36
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-nested');
37
+ const projectDest = path.join(parentEnvDir, 'test-nested-project');
38
+ const templateRoot = path.join(process.cwd(), 'test-nested-tpl-root');
39
+ cleanup(projectDest, templateRoot, testHome);
40
+
41
+ // Create template root with a file containing the nested variable example
42
+ fs.mkdirSync(templateRoot, { recursive: true });
43
+ fs.writeFileSync(
44
+ path.join(templateRoot, 'README.md'),
45
+ '# {{ prefix }}\n\nThis is a test project.\n'
46
+ );
47
+
48
+ // Set up config with template that has the nested variable example
49
+ setupTestConfig('nested-tpl', {
50
+ description: 'Template with nested variable expansion',
51
+ templateRoot: templateRoot,
52
+ folders: [],
53
+ variables: [
54
+ { name: 'prefix', prompt: 'Project prefix:', required: true },
55
+ { name: 'project', prompt: 'Project name:', default: 'default' }
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 the exact example from the user's request
63
+ fs.mkdirSync(parentEnvDir, { recursive: true });
64
+ fs.writeFileSync(
65
+ path.join(parentEnvDir, '.env'),
66
+ `prefix='rst_{{ project }}'\nproject=MyProject\n`
67
+ );
68
+
69
+ // Run init from within the parent directory
70
+ await init('nested-tpl', projectDest, {
71
+ yes: true,
72
+ skipPostConfig: true
73
+ });
74
+
75
+ // Verify nested variable expansion worked correctly
76
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
77
+
78
+ // The prefix variable should be 'rst_MyProject' (after expanding {{ project }})
79
+ assert.ok(readme.includes('rst_MyProject'), 'README should contain expanded prefix value');
80
+ assert.ok(!readme.includes('{{ prefix }}'), 'README should NOT contain variable placeholder');
81
+ assert.ok(!readme.includes('{{ project }}'), 'README should NOT contain nested variable placeholder');
82
+ assert.ok(!readme.includes('rst_{{ project }}'), 'README should NOT contain unexpanded nested variable');
83
+
84
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
85
+ });
86
+
87
+ test('nested variable expansion: multiple levels of nesting', async () => {
88
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-deep-nested');
89
+ const projectDest = path.join(parentEnvDir, 'test-deep-nested-project');
90
+ const templateRoot = path.join(process.cwd(), 'test-deep-nested-tpl-root');
91
+ cleanup(projectDest, templateRoot, testHome);
92
+
93
+ // Create template root with a file containing deeply nested variables
94
+ fs.mkdirSync(templateRoot, { recursive: true });
95
+ fs.writeFileSync(
96
+ path.join(templateRoot, 'config.txt'),
97
+ 'name={{ prefix }}_{{ project }}_{{ version }}\n'
98
+ );
99
+
100
+ // Set up config with template that has deeply nested variables
101
+ setupTestConfig('deep-nested-tpl', {
102
+ description: 'Template with deeply nested variables',
103
+ templateRoot: templateRoot,
104
+ folders: [],
105
+ variables: [
106
+ { name: 'prefix', prompt: 'Prefix:', required: true },
107
+ { name: 'project', prompt: 'Project:', default: 'default' },
108
+ { name: 'version', prompt: 'Version:', default: '1.0' }
109
+ ],
110
+ copy_files: [
111
+ { src: 'config.txt', dest: 'config.txt', substitute_variables: true }
112
+ ]
113
+ });
114
+
115
+ // Create parent directory with .env file containing deeply nested variables
116
+ fs.mkdirSync(parentEnvDir, { recursive: true });
117
+ fs.writeFileSync(
118
+ path.join(parentEnvDir, '.env'),
119
+ `prefix='app_{{ env }}'\nenv=prod\nproject=MyApp\nversion=2.0\n`
120
+ );
121
+
122
+ // Run init from within the parent directory
123
+ await init('deep-nested-tpl', projectDest, {
124
+ yes: true,
125
+ skipPostConfig: true
126
+ });
127
+
128
+ // Verify deeply nested variable expansion worked correctly
129
+ const configContent = fs.readFileSync(path.join(projectDest, 'config.txt'), 'utf-8');
130
+
131
+ // The prefix variable should be 'app_prod' (after expanding {{ env }})
132
+ // The final result should be 'app_prod_MyApp_2.0'
133
+ assert.ok(configContent.includes('app_prod_MyApp_2.0'), 'config.txt should contain fully expanded variables');
134
+ assert.ok(!configContent.includes('{{ prefix }}'), 'config.txt should NOT contain variable placeholders');
135
+ assert.ok(!configContent.includes('{{ project }}'), 'config.txt should NOT contain variable placeholders');
136
+ assert.ok(!configContent.includes('{{ version }}'), 'config.txt should NOT contain variable placeholders');
137
+ assert.ok(!configContent.includes('{{ env }}'), 'config.txt should NOT contain nested variable placeholders');
138
+ assert.ok(!configContent.includes('app_{{ env }}'), 'config.txt should NOT contain unexpanded nested variable');
139
+
140
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
141
+ });
142
+
143
+ test('nested variable expansion: handles circular references gracefully', async () => {
144
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-circular');
145
+ const projectDest = path.join(parentEnvDir, 'test-circular-project');
146
+ const templateRoot = path.join(process.cwd(), 'test-circular-tpl-root');
147
+ cleanup(projectDest, templateRoot, testHome);
148
+
149
+ // Create template root with a file containing potentially circular variables
150
+ fs.mkdirSync(templateRoot, { recursive: true });
151
+ fs.writeFileSync(
152
+ path.join(templateRoot, 'data.txt'),
153
+ 'value={{ a }}\n'
154
+ );
155
+
156
+ // Set up config with template that has variables that could cause circular references
157
+ setupTestConfig('circular-tpl', {
158
+ description: 'Template with potential circular references',
159
+ templateRoot: templateRoot,
160
+ folders: [],
161
+ variables: [
162
+ { name: 'a', prompt: 'Value a:', required: true }
163
+ ],
164
+ copy_files: [
165
+ { src: 'data.txt', dest: 'data.txt', substitute_variables: true }
166
+ ]
167
+ });
168
+
169
+ // Create parent directory with .env file that could cause circular references
170
+ fs.mkdirSync(parentEnvDir, { recursive: true });
171
+ fs.writeFileSync(
172
+ path.join(parentEnvDir, '.env'),
173
+ `a={{ a }}\n`
174
+ );
175
+
176
+ // Run init from within the parent directory - should not hang or crash
177
+ await init('circular-tpl', projectDest, {
178
+ yes: true,
179
+ skipPostConfig: true
180
+ });
181
+
182
+ // Verify the feature handles circular references without hanging
183
+ const dataContent = fs.readFileSync(path.join(projectDest, 'data.txt'), 'utf-8');
184
+
185
+ // The circular reference should be handled gracefully (either left as-is or replaced with empty)
186
+ assert.ok(dataContent.includes('{{ a }}') || dataContent.includes(''),
187
+ 'circular reference should be handled without hanging');
188
+ assert.ok(dataContent.includes('value='), 'data.txt should still contain the original structure');
189
+
190
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
191
+ });
192
+
193
+ test('nested variable expansion: handles missing nested variables', async () => {
194
+ const parentEnvDir = path.join(process.cwd(), 'test-parent-missing-nested');
195
+ const projectDest = path.join(parentEnvDir, 'test-missing-nested-project');
196
+ const templateRoot = path.join(process.cwd(), 'test-missing-nested-tpl-root');
197
+ cleanup(projectDest, templateRoot, testHome);
198
+
199
+ // Create template root with a file containing nested variables
200
+ fs.mkdirSync(templateRoot, { recursive: true });
201
+ fs.writeFileSync(
202
+ path.join(templateRoot, 'README.md'),
203
+ '# {{ prefix }}\n\nThis is a test project.\n'
204
+ );
205
+
206
+ // Set up config with template that has nested variables
207
+ setupTestConfig('missing-nested-tpl', {
208
+ description: 'Template with missing nested variables',
209
+ templateRoot: templateRoot,
210
+ folders: [],
211
+ variables: [
212
+ { name: 'prefix', prompt: 'Prefix:', required: true },
213
+ { name: 'project', prompt: 'Project:', default: 'default' }
214
+ ],
215
+ copy_files: [
216
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
217
+ ]
218
+ });
219
+
220
+ // Create parent directory with .env file that has nested variable but missing the referenced variable
221
+ fs.mkdirSync(parentEnvDir, { recursive: true });
222
+ fs.writeFileSync(
223
+ path.join(parentEnvDir, '.env'),
224
+ `prefix='rst_{{ missing_var }}'\n`
225
+ );
226
+
227
+ // Run init from within the parent directory
228
+ await init('missing-nested-tpl', projectDest, {
229
+ yes: true,
230
+ skipPostConfig: true
231
+ });
232
+
233
+ // Verify that missing nested variables are handled gracefully
234
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
235
+
236
+ // The missing variable should remain as placeholder or be replaced with empty
237
+ assert.ok(readme.includes('# ') || readme.includes('rst_'), 'README should contain the prefix value');
238
+ assert.ok(!readme.includes('{{ prefix }}'), 'README should NOT contain the original prefix placeholder');
239
+ // The missing variable might remain as placeholder or be replaced with empty - both are acceptable
240
+
241
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
242
+ });
@@ -0,0 +1,208 @@
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-rst-example');
8
+ process.env.HOME = testHome;
9
+
10
+ // Import the init command to test the nested variable expansion functionality
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('RST example: prefix variable with nested {{ project }} reference', async () => {
36
+ // Simulate the exact scenario from /mnt/production/CLIENT/RST/.env
37
+ const parentEnvDir = path.join(process.cwd(), 'test-rst-parent');
38
+ const projectDest = path.join(parentEnvDir, 'test-rst-project');
39
+ const templateRoot = path.join(process.cwd(), 'test-rst-tpl-root');
40
+ cleanup(projectDest, templateRoot, testHome);
41
+
42
+ // Create template root with a file containing the exact example from the user's request
43
+ fs.mkdirSync(templateRoot, { recursive: true });
44
+ fs.writeFileSync(
45
+ path.join(templateRoot, 'README.md'),
46
+ '# {{ prefix }}\n\nThis is a test project.\n'
47
+ );
48
+
49
+ // Set up config with template that has the nested variable example
50
+ setupTestConfig('rst-tpl', {
51
+ description: 'RST Template with nested variable expansion',
52
+ templateRoot: templateRoot,
53
+ folders: [],
54
+ variables: [
55
+ { name: 'prefix', prompt: 'Project prefix:', required: true },
56
+ { name: 'project', prompt: 'Project name:', default: 'default' }
57
+ ],
58
+ copy_files: [
59
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
60
+ ]
61
+ });
62
+
63
+ // Create parent directory with .env file containing the exact example from the user's request
64
+ // This simulates /mnt/production/CLIENT/RST/.env
65
+ fs.mkdirSync(parentEnvDir, { recursive: true });
66
+ fs.writeFileSync(
67
+ path.join(parentEnvDir, '.env'),
68
+ `prefix='rst_{{ project }}'\nproject=MyRSTProject\n`
69
+ );
70
+
71
+ // Run init from within the parent directory
72
+ await init('rst-tpl', projectDest, {
73
+ yes: true,
74
+ skipPostConfig: true
75
+ });
76
+
77
+ // Verify nested variable expansion worked correctly
78
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
79
+
80
+ // The prefix variable should be 'rst_MyRSTProject' (after expanding {{ project }})
81
+ assert.ok(readme.includes('rst_MyRSTProject'), 'README should contain expanded prefix value');
82
+ assert.ok(!readme.includes('{{ prefix }}'), 'README should NOT contain variable placeholder');
83
+ assert.ok(!readme.includes('{{ project }}'), 'README should NOT contain nested variable placeholder');
84
+ assert.ok(!readme.includes('rst_{{ project }}'), 'README should NOT contain unexpanded nested variable');
85
+
86
+ console.log('✓ RST example test passed: nested variable expansion works correctly');
87
+ console.log(` Input: prefix='rst_{{ project }}', project=MyRSTProject`);
88
+ console.log(` Output: prefix resolved to 'rst_MyRSTProject'`);
89
+
90
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
91
+ });
92
+
93
+ test('RST example: multiple nested variables in .env', async () => {
94
+ // Test multiple nested variables in a single .env file
95
+ const parentEnvDir = path.join(process.cwd(), 'test-rst-multi-parent');
96
+ const projectDest = path.join(parentEnvDir, 'test-rst-multi-project');
97
+ const templateRoot = path.join(process.cwd(), 'test-rst-multi-tpl-root');
98
+ cleanup(projectDest, templateRoot, testHome);
99
+
100
+ // Create template root with a file containing multiple nested variables
101
+ fs.mkdirSync(templateRoot, { recursive: true });
102
+ fs.writeFileSync(
103
+ path.join(templateRoot, 'config.txt'),
104
+ 'name={{ prefix }}_{{ project }}_{{ version }}\n'
105
+ );
106
+
107
+ // Set up config with template that has multiple nested variables
108
+ setupTestConfig('rst-multi-tpl', {
109
+ description: 'RST Template with multiple nested variables',
110
+ templateRoot: templateRoot,
111
+ folders: [],
112
+ variables: [
113
+ { name: 'prefix', prompt: 'Prefix:', required: true },
114
+ { name: 'project', prompt: 'Project:', default: 'default' },
115
+ { name: 'version', prompt: 'Version:', default: '1.0' }
116
+ ],
117
+ copy_files: [
118
+ { src: 'config.txt', dest: 'config.txt', substitute_variables: true }
119
+ ]
120
+ });
121
+
122
+ // Create parent directory with .env file containing multiple nested variables
123
+ fs.mkdirSync(parentEnvDir, { recursive: true });
124
+ fs.writeFileSync(
125
+ path.join(parentEnvDir, '.env'),
126
+ `prefix='app_{{ env }}'\nenv=prod\nproject=MyApp\nversion=2.0\n`
127
+ );
128
+
129
+ // Run init from within the parent directory
130
+ await init('rst-multi-tpl', projectDest, {
131
+ yes: true,
132
+ skipPostConfig: true
133
+ });
134
+
135
+ // Verify multiple nested variables were expanded correctly
136
+ const configContent = fs.readFileSync(path.join(projectDest, 'config.txt'), 'utf-8');
137
+
138
+ // The prefix variable should be 'app_prod' (after expanding {{ env }})
139
+ // The final result should be 'app_prod_MyApp_2.0'
140
+ assert.ok(configContent.includes('app_prod_MyApp_2.0'), 'config.txt should contain fully expanded variables');
141
+ assert.ok(!configContent.includes('{{ prefix }}'), 'config.txt should NOT contain variable placeholders');
142
+ assert.ok(!configContent.includes('{{ project }}'), 'config.txt should NOT contain variable placeholders');
143
+ assert.ok(!configContent.includes('{{ version }}'), 'config.txt should NOT contain variable placeholders');
144
+ assert.ok(!configContent.includes('{{ env }}'), 'config.txt should NOT contain nested variable placeholders');
145
+ assert.ok(!configContent.includes('app_{{ env }}'), 'config.txt should NOT contain unexpanded nested variable');
146
+
147
+ console.log('✓ RST multi-variable test passed: all nested variables expanded correctly');
148
+ console.log(` Input: prefix='app_{{ env }}', env=prod, project=MyApp, version=2.0`);
149
+ console.log(` Output: name resolved to 'app_prod_MyApp_2.0'`);
150
+
151
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
152
+ });
153
+
154
+ test('RST example: .env values are used as defaults in interactive mode', async () => {
155
+ // Test that .env values are used as defaults when user doesn't override
156
+ const parentEnvDir = path.join(process.cwd(), 'test-rst-defaults-parent');
157
+ const projectDest = path.join(parentEnvDir, 'test-rst-defaults-project');
158
+ const templateRoot = path.join(process.cwd(), 'test-rst-defaults-tpl-root');
159
+ cleanup(projectDest, templateRoot, testHome);
160
+
161
+ // Create template root with a file containing variables
162
+ fs.mkdirSync(templateRoot, { recursive: true });
163
+ fs.writeFileSync(
164
+ path.join(templateRoot, 'README.md'),
165
+ '# {{ prefix }}\n\nThis is a test project.\n'
166
+ );
167
+
168
+ // Set up config with template that has variables with defaults
169
+ setupTestConfig('rst-defaults-tpl', {
170
+ description: 'RST Template with defaults',
171
+ templateRoot: templateRoot,
172
+ folders: [],
173
+ variables: [
174
+ { name: 'prefix', prompt: 'Project prefix:', required: true, default: 'default_prefix' },
175
+ { name: 'project', prompt: 'Project name:', default: 'default_project' }
176
+ ],
177
+ copy_files: [
178
+ { src: 'README.md', dest: 'README.md', substitute_variables: true }
179
+ ]
180
+ });
181
+
182
+ // Create parent directory with .env file containing values that should be used as defaults
183
+ fs.mkdirSync(parentEnvDir, { recursive: true });
184
+ fs.writeFileSync(
185
+ path.join(parentEnvDir, '.env'),
186
+ `prefix='rst_{{ project }}'\nproject=EnvProject\n`
187
+ );
188
+
189
+ // Run init from within the parent directory
190
+ await init('rst-defaults-tpl', projectDest, {
191
+ yes: true,
192
+ skipPostConfig: true
193
+ });
194
+
195
+ // Verify .env values were used as defaults
196
+ const readme = fs.readFileSync(path.join(projectDest, 'README.md'), 'utf-8');
197
+
198
+ // The prefix variable should be 'rst_EnvProject' (after expanding {{ project }})
199
+ assert.ok(readme.includes('rst_EnvProject'), 'README should contain expanded prefix value from .env');
200
+ assert.ok(!readme.includes('{{ prefix }}'), 'README should NOT contain variable placeholder');
201
+ assert.ok(!readme.includes('default_prefix'), 'README should NOT contain default value');
202
+
203
+ console.log('✓ RST defaults test passed: .env values used as defaults');
204
+ console.log(` Input: prefix='rst_{{ project }}', project=EnvProject`);
205
+ console.log(` Output: prefix resolved to 'rst_EnvProject'`);
206
+
207
+ cleanup(projectDest, templateRoot, parentEnvDir, testHome);
208
+ });
@@ -58,14 +58,15 @@ test('substituteVariables: variable with no whitespace in braces', () => {
58
58
 
59
59
  test('substituteVariables: missing variable remains as normalized placeholder', () => {
60
60
  const result = substituteVariables('Hello {{ unknown }}!', {});
61
- // The regex replaces {{ unknown }} with {{unknown}} (no spaces) when not found
62
- assert.strictEqual(result, 'Hello {{unknown}}!');
61
+ // The regex preserves original spacing when variable is not found
62
+ assert.strictEqual(result, 'Hello {{ unknown }}!');
63
63
  });
64
64
 
65
65
  test('substituteVariables: empty variables object leaves all placeholders', () => {
66
66
  const content = '{{ foo }} and {{ bar }}';
67
67
  const result = substituteVariables(content, {});
68
- assert.strictEqual(result, '{{foo}} and {{bar}}');
68
+ // The regex preserves original spacing when variables are not found
69
+ assert.strictEqual(result, '{{ foo }} and {{ bar }}');
69
70
  });
70
71
 
71
72
  test('substituteVariables: no variables in content returns content unchanged', () => {
@@ -79,7 +80,8 @@ test('substituteVariables: mixed - some found, some not', () => {
79
80
  '{{ found }} and {{ missing }}',
80
81
  { found: 'YES' }
81
82
  );
82
- assert.strictEqual(result, 'YES and {{missing}}');
83
+ // The regex preserves original spacing when variable is not found
84
+ assert.strictEqual(result, 'YES and {{ missing }}');
83
85
  });
84
86
 
85
87
  test('substituteVariables: repeated variable is substituted in all occurrences', () => {
@@ -95,8 +97,14 @@ test('substituteVariables: empty string content', () => {
95
97
  assert.strictEqual(result, '');
96
98
  });
97
99
 
98
- test('substituteVariables: value containing braces is not re-processed', () => {
99
- // Substituted values should be inserted literally, no recursive substitution
100
+ test('substituteVariables: value containing braces is recursively expanded', () => {
101
+ // With recursive expansion, {{ other }} in the value will be expanded if 'other' exists
102
+ const result = substituteVariables('{{ name }}', { name: '{{ other }}', other: 'expanded' });
103
+ assert.strictEqual(result, 'expanded');
104
+ });
105
+
106
+ test('substituteVariables: value containing braces remains as-is if nested variable not found', () => {
107
+ // With recursive expansion, {{ other }} in the value will remain as-is if 'other' doesn't exist
100
108
  const result = substituteVariables('{{ name }}', { name: '{{ other }}' });
101
109
  assert.strictEqual(result, '{{ other }}');
102
110
  });