timonel 2.8.1 → 2.9.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli.d.ts +0 -1
  3. package/dist/cli.js +274 -634
  4. package/dist/index.d.ts +4 -6
  5. package/dist/index.js +4 -7
  6. package/dist/lib/helm.d.ts +0 -472
  7. package/dist/lib/helm.js +0 -481
  8. package/dist/lib/helmChartWriter.d.ts +0 -178
  9. package/dist/lib/helmChartWriter.js +27 -180
  10. package/dist/lib/resources/baseResourceProvider.d.ts +0 -46
  11. package/dist/lib/resources/baseResourceProvider.js +1 -47
  12. package/dist/lib/resources/cloud/aws/awsResources.d.ts +0 -144
  13. package/dist/lib/resources/cloud/aws/awsResources.js +1 -163
  14. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +0 -132
  15. package/dist/lib/resources/cloud/aws/karpenterResources.js +0 -75
  16. package/dist/lib/rutter.d.ts +28 -340
  17. package/dist/lib/rutter.js +291 -371
  18. package/dist/lib/security.d.ts +0 -119
  19. package/dist/lib/security.js +4 -160
  20. package/dist/lib/templates/basic-chart.d.ts +19 -0
  21. package/dist/lib/templates/basic-chart.js +189 -0
  22. package/dist/lib/templates/subchart.d.ts +27 -0
  23. package/dist/lib/templates/subchart.js +224 -0
  24. package/dist/lib/templates/umbrella-chart.d.ts +13 -0
  25. package/dist/lib/templates/umbrella-chart.js +222 -0
  26. package/dist/lib/types.d.ts +20 -0
  27. package/dist/lib/types.js +1 -0
  28. package/dist/lib/umbrella.d.ts +0 -24
  29. package/dist/lib/umbrella.js +0 -24
  30. package/dist/lib/umbrellaRutter.d.ts +0 -71
  31. package/dist/lib/umbrellaRutter.js +2 -82
  32. package/dist/lib/utils/helmHelpers.d.ts +18 -41
  33. package/dist/lib/utils/helmHelpers.js +246 -35
  34. package/package.json +9 -7
  35. package/README.md +0 -74
  36. package/dist/cli.d.ts.map +0 -1
  37. package/dist/cli.js.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/index.js.map +0 -1
  40. package/dist/lib/helm.d.ts.map +0 -1
  41. package/dist/lib/helm.js.map +0 -1
  42. package/dist/lib/helmChartWriter.d.ts.map +0 -1
  43. package/dist/lib/helmChartWriter.js.map +0 -1
  44. package/dist/lib/resources/baseResourceProvider.d.ts.map +0 -1
  45. package/dist/lib/resources/baseResourceProvider.js.map +0 -1
  46. package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +0 -1
  47. package/dist/lib/resources/cloud/aws/awsResources.js.map +0 -1
  48. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts.map +0 -1
  49. package/dist/lib/resources/cloud/aws/karpenterResources.js.map +0 -1
  50. package/dist/lib/rutter.d.ts.map +0 -1
  51. package/dist/lib/rutter.js.map +0 -1
  52. package/dist/lib/security.d.ts.map +0 -1
  53. package/dist/lib/security.js.map +0 -1
  54. package/dist/lib/umbrella.d.ts.map +0 -1
  55. package/dist/lib/umbrella.js.map +0 -1
  56. package/dist/lib/umbrellaRutter.d.ts.map +0 -1
  57. package/dist/lib/umbrellaRutter.js.map +0 -1
  58. package/dist/lib/utils/helmHelpers.d.ts.map +0 -1
  59. package/dist/lib/utils/helmHelpers.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import * as cp from 'child_process';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
2
+ import { spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { SecurityUtils } from './lib/security.js';
7
- // ES modules equivalent of __dirname
8
7
  const __filename = fileURLToPath(import.meta.url);
9
8
  const __dirname = path.dirname(__filename);
10
- const HELM_INSTALL_URL = 'https://helm.sh/docs/intro/install/';
11
- const HELM_ENV_VAR_MSG = 'Or set HELM_BIN environment variable to helm binary path.';
9
+ const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
10
+ const UMBRELLA_FILE_NAME = 'umbrella.ts';
11
+ function log(msg, silent = false) {
12
+ if (!silent) {
13
+ console.log(msg);
14
+ }
15
+ }
12
16
  function usageAndExit(msg) {
13
- if (msg)
14
- console.error(SecurityUtils.sanitizeLogMessage(msg));
17
+ if (msg) {
18
+ console.error(`Error: ${msg}`);
19
+ }
15
20
  console.log([
16
- 'timonel (tl) - programmatic Helm chart generator',
21
+ 'Usage: tl <command> [options]',
17
22
  '',
18
- 'Usage:',
19
- ' tl init <chart-name> Scaffold example at charts/<name>/chart.ts',
20
- ' tl synth <projectDir> [outDir] Run charts/<...>/chart.ts and write chart',
21
- ' tl validate <projectDir> Validate chart.ts without generating files',
22
- ' tl diff <projectDir> <chartDir> Compare generated chart with existing',
23
- ' tl deploy <projectDir> <release> Synth and deploy with helm install/upgrade',
24
- ' tl package <chartDir> [outDir] Run `helm package` into outDir (requires Helm)',
23
+ 'Commands:',
24
+ ' tl init <chart-name> Create new chart',
25
+ ' tl synth [outDir] Generate Helm chart',
26
+ ' tl validate Validate chart',
27
+ ' tl deploy <release> [namespace] Deploy chart',
28
+ ' tl templates List available templates',
25
29
  '',
26
30
  'Umbrella Charts:',
27
- ' tl umbrella init <name> Create umbrella chart structure',
31
+ ' tl umbrella init <n> Create umbrella chart structure',
28
32
  ' tl umbrella add <subchart> Add subchart to umbrella',
29
33
  ' tl umbrella synth [outDir] Generate umbrella chart',
30
34
  '',
@@ -38,130 +42,64 @@ function usageAndExit(msg) {
38
42
  '',
39
43
  'Examples:',
40
44
  ' tl init my-app',
41
- ' tl synth charts/my-app charts/my-app/dist',
42
- ' tl validate charts/my-app',
43
- ' tl deploy charts/my-app my-release --env prod',
44
- ' tl synth charts/my-app --dry-run --silent',
45
- ' tl synth charts/my-app --set replicas=5 --set image.tag=v2.0.0',
46
- ' tl deploy charts/my-app my-release --set service.port=8080',
45
+ ' tl synth my-app my-app/dist',
46
+ ' tl validate my-app',
47
+ ' tl deploy my-app my-release --env prod',
48
+ ' tl synth my-app --dry-run --silent',
49
+ ' tl synth my-app --set replicas=5 --set image.tag=v2.0.0',
50
+ ' tl deploy my-app my-release --set service.port=8080',
47
51
  ].join('\n'));
48
52
  process.exit(msg ? 1 : 0);
49
53
  }
50
54
  async function cmdInit(name, silent = false) {
51
55
  if (!name)
52
56
  usageAndExit('Missing <chart-name>');
53
- // Validate chart name for security using centralized validation
54
- if (!SecurityUtils.isValidChartName(name)) {
55
- usageAndExit('Invalid chart name: must be RFC 1123 compliant (lowercase alphanumeric with hyphens)');
57
+ const validName = name;
58
+ if (!SecurityUtils.isValidChartName(validName)) {
59
+ usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
56
60
  }
57
- const sanitizedName = name; // Already validated by SecurityUtils
58
- const base = SecurityUtils.validatePath(path.join(process.cwd(), 'charts', sanitizedName), process.cwd());
59
- const file = path.join(base, 'chart.ts');
60
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
61
+ const base = path.join(process.cwd(), validName);
62
+ const chartFile = path.join(base, 'chart.ts');
61
63
  fs.mkdirSync(base, { recursive: true });
62
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
63
- if (fs.existsSync(file)) {
64
- console.error(`File already exists: ${file}`);
65
- process.exit(1);
66
- }
67
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
68
- fs.writeFileSync(file, exampleChartTs(name));
69
- log(SecurityUtils.sanitizeLogMessage(`Scaffold created at ${file}`), silent);
64
+ const { generateBasicChart } = await import('./lib/templates/basic-chart.js');
65
+ fs.writeFileSync(chartFile, generateBasicChart(validName));
66
+ log(`Chart created at ${base}`, silent);
67
+ log(`Generated chart.ts file`, silent);
68
+ log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
70
69
  }
71
- async function cmdValidate(projectDir, silent = false) {
72
- if (!projectDir)
73
- usageAndExit('Missing <projectDir>');
74
- const proj = projectDir;
75
- // Validate and secure the project directory path
76
- const basePath = path.isAbsolute(proj) ? path.dirname(proj) : process.cwd();
77
- const chartTs = path.isAbsolute(proj)
78
- ? SecurityUtils.validatePath(path.join(proj, 'chart.ts'), basePath)
79
- : SecurityUtils.validatePath(path.join(process.cwd(), proj, 'chart.ts'), process.cwd());
80
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
81
- if (!fs.existsSync(chartTs)) {
82
- console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
83
- process.exit(1);
84
- }
85
- // Check if helm is available
86
- const helm = process.env['HELM_BIN'] || 'helm';
87
- try {
88
- cp.execSync(`${helm} version --short`, { stdio: 'ignore' });
89
- }
90
- catch {
91
- console.error('✗ Helm not found. Install Helm to enable chart validation.');
92
- console.error(` Install: ${HELM_INSTALL_URL}`);
93
- console.error(` ${HELM_ENV_VAR_MSG}`);
94
- process.exit(1);
95
- }
96
- const tempDir = path.join(process.cwd(), '.timonel-validate');
97
- try {
98
- // Generate chart to temp directory for validation
99
- await cmdSynth(projectDir, tempDir, { dryRun: false, silent: true, set: {} });
100
- // Run helm lint on generated chart
101
- const lintResult = cp.spawnSync(helm, ['lint', tempDir], {
102
- stdio: silent ? 'pipe' : 'inherit',
103
- encoding: 'utf8',
104
- });
105
- if (lintResult.status !== 0) {
106
- if (silent && lintResult.stderr) {
107
- console.error(SecurityUtils.sanitizeLogMessage(lintResult.stderr));
108
- }
109
- console.error('✗ Chart validation failed: Helm lint errors found');
110
- process.exit(1);
111
- }
112
- log('✓ Chart validation passed', silent);
113
- }
114
- catch (error) {
115
- const errorMsg = error instanceof Error ? error.message : String(error);
116
- console.error('✗ Chart validation failed:', SecurityUtils.sanitizeLogMessage(errorMsg));
70
+ async function cmdSynth(chartDirOrOutDir, flags) {
71
+ let chartDir = process.cwd();
72
+ let outDir;
73
+ if (chartDirOrOutDir && fs.existsSync(path.join(chartDirOrOutDir, 'chart.ts'))) {
74
+ chartDir = path.resolve(chartDirOrOutDir);
75
+ }
76
+ else {
77
+ outDir = chartDirOrOutDir;
78
+ }
79
+ const chartFile = path.join(chartDir, 'chart.ts');
80
+ const defaultOutDir = path.join(chartDir, 'dist');
81
+ if (!fs.existsSync(chartFile)) {
82
+ console.error('chart.ts not found. Run `tl init` first.');
117
83
  process.exit(1);
118
84
  }
119
- finally {
120
- // Cleanup temp directory
121
- if (fs.existsSync(tempDir)) {
122
- fs.rmSync(tempDir, { recursive: true, force: true });
123
- }
124
- }
125
- }
126
- /**
127
- * Execute TypeScript chart using tsx with proper error handling.
128
- * @param resolvedPath - Resolved path to the TypeScript file
129
- * @param outDir - Output directory for the chart
130
- * @param flags - CLI flags
131
- * @since 2.7.3
132
- */
133
- async function executeTypeScriptChart(resolvedPath, outDir, flags) {
85
+ const resolvedOutDir = outDir ? path.resolve(outDir) : defaultOutDir;
86
+ const originalContent = fs.readFileSync(chartFile, 'utf8');
87
+ const modifiedContent = originalContent.replace(/chart\.writeHelmChart\(['"][^'"]*['"]\)/, `chart.writeHelmChart('${resolvedOutDir}')`);
88
+ const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
89
+ fs.writeFileSync(tempChartFile, modifiedContent);
134
90
  const wrapperScript = `
135
91
  import { pathToFileURL } from 'url';
136
92
 
137
- const mod = await import(pathToFileURL('${resolvedPath}').href);
138
- const runner = mod.default || mod.run || mod.synth;
139
- if (typeof runner !== 'function') {
140
- console.error('chart.ts must export a default/run/synth function');
141
- process.exit(1);
142
- }
143
-
144
- ${flags?.set && Object.keys(flags.set).length > 0
145
- ? `
146
- // Apply --set overrides if provided
147
- if (mod.rutter && typeof mod.rutter.setValues === 'function') {
148
- mod.rutter.setValues(${JSON.stringify(flags.set)});
149
- }
150
- `
151
- : ''}
152
-
153
- const outDir = '${outDir}';
154
- const fs = await import('fs');
155
- fs.mkdirSync(outDir, { recursive: true });
156
- await Promise.resolve(runner(outDir));
157
- console.log('Chart written to ' + outDir);
93
+ // Import the modified chart file which should execute the synthesis directly
94
+ await import(pathToFileURL('${tempChartFile}').href);
158
95
  `;
159
- const wrapperFile = path.join(process.cwd(), '.timonel-wrapper.mjs');
96
+ const wrapperFile = path.join(chartDir, '.timonel-wrapper.mjs');
160
97
  try {
161
98
  fs.writeFileSync(wrapperFile, wrapperScript);
162
- const result = cp.spawnSync('npx', ['tsx', wrapperFile], {
99
+ const result = spawnSync('npx', ['tsx', wrapperFile].filter(Boolean), {
163
100
  stdio: flags?.silent ? 'pipe' : 'inherit',
164
101
  encoding: 'utf8',
102
+ cwd: chartDir,
165
103
  });
166
104
  if (result.status !== 0) {
167
105
  if (flags?.silent && result.stderr) {
@@ -171,127 +109,77 @@ console.log('Chart written to ' + outDir);
171
109
  }
172
110
  }
173
111
  finally {
174
- // Cleanup wrapper file
175
112
  if (fs.existsSync(wrapperFile)) {
176
113
  fs.unlinkSync(wrapperFile);
177
114
  }
178
- }
179
- }
180
- async function cmdSynth(projectDir, out, flags) {
181
- if (!projectDir)
182
- usageAndExit('Missing <projectDir>');
183
- const proj = projectDir;
184
- // Validate and secure the project directory path
185
- const basePath = path.isAbsolute(proj) ? path.dirname(proj) : process.cwd();
186
- const chartTs = path.isAbsolute(proj)
187
- ? SecurityUtils.validatePath(path.join(proj, 'chart.ts'), basePath)
188
- : SecurityUtils.validatePath(path.join(process.cwd(), proj, 'chart.ts'), process.cwd());
189
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
190
- if (!fs.existsSync(chartTs)) {
191
- console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
192
- process.exit(1);
193
- }
194
- // Validate TypeScript file extension
195
- if (!SecurityUtils.isValidTypeScriptFile(chartTs)) {
196
- console.error('Security: Only TypeScript files (.ts) are allowed');
197
- process.exit(1);
198
- }
199
- // Additional path validation is already done by SecurityUtils.validatePath above
200
- const resolvedPath = path.resolve(chartTs);
201
- const outDir = out || path.join(path.dirname(chartTs), 'dist');
202
- if (flags?.dryRun) {
203
- log(`[DRY RUN] Would write chart to ${outDir}`, flags.silent);
204
- return;
205
- }
206
- try {
207
- await executeTypeScriptChart(resolvedPath, outDir, flags);
208
- }
209
- catch (error) {
210
- const errorMsg = error instanceof Error ? error.message : String(error);
211
- console.error(SecurityUtils.sanitizeLogMessage(`Failed to execute chart: ${errorMsg}`));
212
- process.exit(1);
213
- }
214
- }
215
- async function cmdDiff(projectDir, chartDir, silent = false) {
216
- if (!projectDir || !chartDir)
217
- usageAndExit('Missing <projectDir> or <chartDir>');
218
- const tempDir = path.join(process.cwd(), '.timonel-temp');
219
- try {
220
- // Generate chart to temp directory
221
- await cmdSynth(projectDir, tempDir);
222
- // Compare with existing chart
223
- const diffCmd = process.platform === 'win32' ? 'fc' : 'diff';
224
- const args = process.platform === 'win32' ? ['/N', chartDir, tempDir] : ['-r', chartDir, tempDir];
225
- log(`> ${diffCmd} ${args.join(' ')}`, silent);
226
- const res = cp.spawnSync(diffCmd, args, { stdio: 'inherit' });
227
- if (res.status === 0) {
228
- log('✓ No differences found', silent);
229
- }
230
- else if (res.status === 1) {
231
- log('⚠ Differences found', silent);
232
- process.exit(1);
115
+ if (fs.existsSync(tempChartFile)) {
116
+ fs.unlinkSync(tempChartFile);
233
117
  }
234
118
  }
235
- finally {
236
- // Cleanup temp directory
237
- if (fs.existsSync(tempDir)) {
238
- fs.rmSync(tempDir, { recursive: true, force: true });
119
+ }
120
+ async function cmdValidate(flags) {
121
+ const result = spawnSync('helm', ['lint', '.'], {
122
+ stdio: flags?.silent ? 'pipe' : 'inherit',
123
+ encoding: 'utf8',
124
+ });
125
+ if (result.status !== 0) {
126
+ if (flags?.silent && result.stderr) {
127
+ console.error(SecurityUtils.sanitizeLogMessage(result.stderr));
239
128
  }
129
+ process.exit(result.status ?? 1);
240
130
  }
241
131
  }
242
- async function cmdDeploy(projectDir, releaseName, flags) {
243
- if (!projectDir || !releaseName)
244
- usageAndExit('Missing <projectDir> or <release>');
245
- // Check if helm is available
246
- const helm = process.env['HELM_BIN'] || 'helm';
247
- try {
248
- cp.execSync(`${helm} version --short`, { stdio: 'ignore' });
249
- }
250
- catch {
251
- console.error('✗ Helm not found. Install Helm to enable deployment.');
252
- console.error(` Install: ${HELM_INSTALL_URL}`);
253
- console.error(` ${HELM_ENV_VAR_MSG}`);
254
- process.exit(1);
255
- }
256
- const tempDir = path.join(process.cwd(), '.timonel-deploy');
257
- try {
258
- // Generate chart
259
- await cmdSynth(projectDir, tempDir, flags);
260
- // Check if release exists
261
- const checkCmd = `${helm} status ${releaseName}`;
262
- const releaseExists = cp.spawnSync('sh', ['-c', checkCmd], { stdio: 'ignore' }).status === 0;
263
- // Build helm command
264
- const action = releaseExists ? 'upgrade' : 'install';
265
- const args = [action, releaseName, tempDir];
266
- // Add environment-specific values if specified
267
- if (flags?.env) {
268
- const valuesFile = path.join(tempDir, `values-${flags.env}.yaml`);
269
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
270
- if (fs.existsSync(valuesFile)) {
271
- args.push('-f', valuesFile);
272
- }
273
- }
274
- if (flags?.dryRun) {
275
- args.push('--dry-run');
276
- }
277
- log(`> ${helm} ${args.join(' ')}`, flags?.silent);
278
- const res = cp.spawnSync(helm, args, { stdio: 'inherit' });
279
- if (res.status !== 0) {
280
- process.exit(res.status ?? 1);
132
+ async function cmdDeploy(release, namespace, flags) {
133
+ if (!release)
134
+ usageAndExit('Missing <release>');
135
+ const args = ['upgrade', '--install', release, '.'];
136
+ if (namespace) {
137
+ args.push('--namespace', namespace);
138
+ }
139
+ const result = spawnSync('helm', args.filter((arg) => Boolean(arg)), {
140
+ stdio: flags?.silent ? 'pipe' : 'inherit',
141
+ encoding: 'utf8',
142
+ });
143
+ if (result.status !== 0) {
144
+ if (flags?.silent && result.stderr) {
145
+ console.error(SecurityUtils.sanitizeLogMessage(result.stderr));
281
146
  }
282
- log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${releaseName}`, flags?.silent);
147
+ process.exit(result.status ?? 1);
283
148
  }
284
- finally {
285
- // Cleanup temp directory
286
- if (fs.existsSync(tempDir)) {
287
- fs.rmSync(tempDir, { recursive: true, force: true });
288
- }
149
+ }
150
+ async function cmdTemplates(flags) {
151
+ const templates = [
152
+ {
153
+ name: 'basic-chart',
154
+ description: 'Basic chart with deployment and service',
155
+ usage: 'tl init my-app',
156
+ },
157
+ {
158
+ name: 'umbrella-chart',
159
+ description: 'Umbrella chart for managing multiple subcharts',
160
+ usage: 'tl umbrella init my-app',
161
+ },
162
+ {
163
+ name: 'subchart',
164
+ description: 'Subchart for use with umbrella charts',
165
+ usage: 'tl umbrella add my-subchart',
166
+ },
167
+ ];
168
+ if (flags?.silent) {
169
+ console.log(JSON.stringify(templates));
170
+ }
171
+ else {
172
+ console.log('Available templates:');
173
+ templates.forEach((t) => {
174
+ console.log(`\n${t.name}`);
175
+ console.log(` Description: ${t.description}`);
176
+ console.log(` Usage: ${t.usage}`);
177
+ });
289
178
  }
290
179
  }
291
180
  async function cmdUmbrella(subcommand, args, flags) {
292
- const UMBRELLA_USAGE_MSG = 'Missing umbrella subcommand (init|add|synth)';
293
181
  if (!subcommand)
294
- usageAndExit(UMBRELLA_USAGE_MSG);
182
+ usageAndExit('Missing umbrella subcommand');
295
183
  switch (subcommand) {
296
184
  case 'init':
297
185
  await cmdUmbrellaInit(args?.[0], flags?.silent);
@@ -306,66 +194,137 @@ async function cmdUmbrella(subcommand, args, flags) {
306
194
  usageAndExit(`Unknown umbrella subcommand: ${subcommand}`);
307
195
  }
308
196
  }
309
- const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
310
197
  async function cmdUmbrellaInit(name, silent = false) {
311
198
  const MISSING_NAME_MSG = 'Missing umbrella chart name';
312
199
  if (!name)
313
200
  usageAndExit(MISSING_NAME_MSG);
314
- const base = path.join(process.cwd(), name);
315
- const umbrellaFile = path.join(base, 'umbrella.ts');
201
+ const validName = name;
202
+ const base = path.join(process.cwd(), validName);
203
+ const umbrellaFile = path.join(base, UMBRELLA_FILE_NAME);
316
204
  const configFile = path.join(base, UMBRELLA_CONFIG_FILE);
317
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
318
205
  fs.mkdirSync(base, { recursive: true });
319
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
320
- fs.mkdirSync(path.join(base, 'charts'), { recursive: true });
321
- // Create umbrella.ts
322
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
323
- fs.writeFileSync(umbrellaFile, exampleUmbrellaTs(name));
324
- // Create config file
206
+ const { generateUmbrellaChart } = await import('./lib/templates/umbrella-chart.js');
207
+ fs.writeFileSync(umbrellaFile, generateUmbrellaChart(validName));
325
208
  const config = {
326
- name,
209
+ name: validName,
327
210
  version: '0.1.0',
328
- description: `${name} umbrella chart`,
211
+ description: `${validName} umbrella chart`,
329
212
  subcharts: [],
330
213
  };
331
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
332
214
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
215
+ const chartsDir = path.join(base, 'charts');
216
+ fs.mkdirSync(chartsDir, { recursive: true });
333
217
  log(`Umbrella chart structure created at ${base}`, silent);
334
218
  log(`Add subcharts with: tl umbrella add <subchart-name>`, silent);
335
219
  }
336
- async function cmdUmbrellaAdd(subchartName, silent = false) {
337
- const MISSING_SUBCHART_MSG = 'Missing subchart name';
338
- if (!subchartName)
220
+ function toCamelCase(str) {
221
+ return str.replace(/-([a-z])/g, (g) => g[1]?.toUpperCase() || '');
222
+ }
223
+ function findLastImportIndex(lines) {
224
+ let lastImportIndex = -1;
225
+ for (let i = 0; i < lines.length; i++) {
226
+ if (lines[i]?.trim().startsWith('import ')) {
227
+ lastImportIndex = i;
228
+ }
229
+ else if (lines[i]?.trim() && !lines[i]?.trim().startsWith('import ')) {
230
+ break;
231
+ }
232
+ }
233
+ return lastImportIndex;
234
+ }
235
+ function addImportStatement(content, importStatement) {
236
+ if (content.includes(importStatement)) {
237
+ return content;
238
+ }
239
+ const lines = content.split('\n');
240
+ const lastImportIndex = findLastImportIndex(lines);
241
+ if (lastImportIndex >= 0) {
242
+ lines.splice(lastImportIndex + 1, 0, importStatement);
243
+ }
244
+ else {
245
+ lines.splice(0, 0, importStatement);
246
+ }
247
+ return lines.join('\n');
248
+ }
249
+ function buildSubchartsContent(subchartsContent, subchartEntry) {
250
+ const hasExistingEntries = /\{\s*name:\s*['"]/.test(subchartsContent || '');
251
+ if (!hasExistingEntries) {
252
+ return `\n // Add your subcharts here:\n ${subchartEntry},\n `;
253
+ }
254
+ const trimmedContent = subchartsContent?.trimEnd() ?? '';
255
+ const needsComma = !trimmedContent.endsWith(',');
256
+ const comma = needsComma ? ',' : '';
257
+ return trimmedContent + `${comma}\n ${subchartEntry}`;
258
+ }
259
+ function addSubchartToArray(content, chartName, camelCaseName) {
260
+ const subchartsRegex = /subcharts:\s*\[([\s\S]*?)\]/;
261
+ const match = content.match(subchartsRegex);
262
+ if (!match) {
263
+ return content;
264
+ }
265
+ const subchartsContent = match[1];
266
+ if (subchartsContent?.includes(`name: '${chartName}'`)) {
267
+ return content;
268
+ }
269
+ const subchartEntry = `{ name: '${chartName}', chart: ${camelCaseName} }`;
270
+ const newSubchartsContent = buildSubchartsContent(subchartsContent, subchartEntry);
271
+ return content.replace(subchartsRegex, `subcharts: [${newSubchartsContent}\n ]`);
272
+ }
273
+ function updateUmbrellaTs(subchartPath, chartName) {
274
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
275
+ const content = processUmbrellaFile(umbrellaFile, subchartPath, chartName);
276
+ fs.writeFileSync(umbrellaFile, content);
277
+ }
278
+ function processUmbrellaFile(umbrellaFile, subchartPath, chartName) {
279
+ let content = fs.readFileSync(umbrellaFile, 'utf8');
280
+ const importData = createImportData(subchartPath, chartName);
281
+ content = addImportStatement(content, importData.importStatement);
282
+ content = addSubchartToArray(content, chartName, importData.camelCaseName);
283
+ return content;
284
+ }
285
+ function createImportData(subchartPath, chartName) {
286
+ const importPath = `./charts/${subchartPath}/chart`;
287
+ const camelCaseName = toCamelCase(chartName);
288
+ const importStatement = `import ${camelCaseName} from '${importPath}';`;
289
+ return { importPath, camelCaseName, importStatement };
290
+ }
291
+ async function cmdUmbrellaAdd(subchartPath, silent = false) {
292
+ const MISSING_SUBCHART_MSG = 'Missing subchart name or path';
293
+ if (!subchartPath)
339
294
  usageAndExit(MISSING_SUBCHART_MSG);
295
+ const validSubchartPath = subchartPath;
340
296
  const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
341
297
  if (!fs.existsSync(configFile)) {
342
298
  console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
343
299
  process.exit(1);
344
300
  }
345
301
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
346
- // Create subchart directory and scaffold
347
- const subchartDir = path.join(process.cwd(), 'charts', subchartName);
348
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
302
+ const subchartName = path.basename(validSubchartPath);
303
+ const subchartDir = path.join(process.cwd(), 'charts', validSubchartPath);
349
304
  fs.mkdirSync(subchartDir, { recursive: true });
350
305
  const chartFile = path.join(subchartDir, 'chart.ts');
351
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
352
- fs.writeFileSync(chartFile, exampleSubchartTs(subchartName));
353
- // Update config
306
+ const { generateSubchartTemplate } = await import('./lib/templates/subchart.js');
307
+ const subchartContent = generateSubchartTemplate(subchartName);
308
+ fs.writeFileSync(chartFile, subchartContent);
354
309
  config.subcharts.push({
355
310
  name: subchartName,
356
311
  version: '0.1.0',
357
- path: `./charts/${subchartName}/chart.ts`,
312
+ path: `./charts/${validSubchartPath}/chart.ts`,
358
313
  });
359
314
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
360
- log(`Subchart ${subchartName} added to umbrella`, silent);
315
+ updateUmbrellaTs(validSubchartPath, subchartName);
316
+ log(`Subchart ${subchartName} added to umbrella at path '${validSubchartPath}'`, silent);
317
+ }
318
+ async function cmdUmbrellaSynth(outDir, flags) {
319
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
320
+ const defaultOutDir = path.join(process.cwd(), 'dist');
321
+ if (!fs.existsSync(umbrellaFile)) {
322
+ console.error('umbrella.ts not found. Run `tl umbrella init` first.');
323
+ process.exit(1);
324
+ }
325
+ const resolvedOutDir = outDir ? path.resolve(outDir) : defaultOutDir;
326
+ await executeTypeScriptUmbrella(umbrellaFile, resolvedOutDir, flags);
361
327
  }
362
- /**
363
- * Execute TypeScript umbrella chart using tsx with proper error handling.
364
- * @param resolvedPath - Resolved path to the TypeScript file
365
- * @param outDir - Output directory for the chart
366
- * @param flags - CLI flags
367
- * @since 2.7.3
368
- */
369
328
  async function executeTypeScriptUmbrella(resolvedPath, outDir, flags) {
370
329
  const wrapperScript = `
371
330
  import { pathToFileURL } from 'url';
@@ -386,7 +345,7 @@ console.log('Umbrella chart written to ' + output);
386
345
  const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
387
346
  try {
388
347
  fs.writeFileSync(wrapperFile, wrapperScript);
389
- const result = cp.spawnSync('npx', ['tsx', wrapperFile], {
348
+ const result = spawnSync('npx', ['tsx', wrapperFile].filter(Boolean), {
390
349
  stdio: flags?.silent ? 'pipe' : 'inherit',
391
350
  encoding: 'utf8',
392
351
  });
@@ -398,404 +357,85 @@ console.log('Umbrella chart written to ' + output);
398
357
  }
399
358
  }
400
359
  finally {
401
- // Cleanup wrapper file
402
360
  if (fs.existsSync(wrapperFile)) {
403
361
  fs.unlinkSync(wrapperFile);
404
362
  }
405
363
  }
406
364
  }
407
- async function cmdUmbrellaSynth(outDir, flags) {
408
- const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
409
- if (!fs.existsSync(configFile)) {
410
- console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
411
- process.exit(1);
412
- }
413
- const umbrellaFile = path.join(process.cwd(), 'umbrella.ts');
414
- if (!fs.existsSync(umbrellaFile)) {
415
- console.error('umbrella.ts not found.');
416
- process.exit(1);
417
- }
418
- // Validate TypeScript file extension
419
- if (!SecurityUtils.isValidTypeScriptFile(umbrellaFile)) {
420
- console.error('Security: Only TypeScript files (.ts) are allowed');
421
- process.exit(1);
422
- }
423
- // Validate path to prevent traversal attacks
424
- const validatedPath = SecurityUtils.validatePath(umbrellaFile, process.cwd());
425
- const resolvedPath = path.resolve(validatedPath);
426
- const output = outDir || path.join(process.cwd(), 'dist');
427
- if (flags?.dryRun) {
428
- log(`[DRY RUN] Would write umbrella chart to ${output}`, flags.silent);
429
- return;
430
- }
431
- try {
432
- await executeTypeScriptUmbrella(resolvedPath, output, flags);
433
- }
434
- catch (error) {
435
- const errorMsg = error instanceof Error ? error.message : String(error);
436
- console.error(SecurityUtils.sanitizeLogMessage(`Failed to execute umbrella: ${errorMsg}`));
437
- process.exit(1);
438
- }
439
- }
440
- async function cmdPackage(chartDir, out, silent = false) {
441
- if (!chartDir)
442
- usageAndExit('Missing <chartDir>');
443
- const src = path.isAbsolute(chartDir) ? chartDir : path.join(process.cwd(), chartDir);
444
- const chartYaml = path.join(src, 'Chart.yaml');
445
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
446
- if (!fs.existsSync(chartYaml)) {
447
- console.error(`Chart.yaml not found in ${src}`);
448
- process.exit(1);
449
- }
450
- const outDir = out ? (path.isAbsolute(out) ? out : path.join(process.cwd(), out)) : src;
451
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
452
- fs.mkdirSync(outDir, { recursive: true });
453
- const helm = process.env['HELM_BIN'] || 'helm';
454
- const args = ['package', src, '-d', outDir];
455
- log(`> ${helm} ${args.join(' ')}`, silent);
456
- const res = cp.spawnSync(helm, args, { stdio: 'inherit' });
457
- if (res.error) {
458
- console.error(`✗ Failed to execute Helm: ${res.error.message}`);
459
- console.error(` Install: ${HELM_INSTALL_URL}`);
460
- console.error(` ${HELM_ENV_VAR_MSG}`);
461
- process.exit(1);
462
- }
463
- if (res.status !== 0) {
464
- process.exit(res.status ?? 1);
465
- }
466
- }
467
- function exampleUmbrellaTs(name) {
468
- return `import { createUmbrella } from 'timonel';
469
- import { Rutter } from 'timonel';
470
-
471
- // Import subcharts (these will be created with 'tl umbrella add')
472
- // import { rutter as fluentBit } from './charts/fluent-bit/chart';
473
- // import { rutter as openObserve } from './charts/openobserve/chart';
474
-
475
- // Create umbrella chart
476
- const umbrella = createUmbrella({
477
- meta: {
478
- name: '${name}',
479
- version: '0.1.0',
480
- description: '${name} umbrella chart',
481
- appVersion: '1.0.0',
482
- },
483
- subcharts: [
484
- // Add your subcharts here:
485
- // { name: 'fluent-bit', rutter: fluentBit },
486
- // { name: 'openobserve', rutter: openObserve },
487
- ],
488
- defaultValues: {
489
- global: {
490
- // Global values shared across subcharts
491
- },
492
- },
493
- envValues: {
494
- dev: {
495
- // Development environment overrides
496
- },
497
- prod: {
498
- // Production environment overrides
499
- },
500
- },
501
- });
502
-
503
- export default function run(outDir: string) {
504
- umbrella.write(outDir);
505
- }
506
- `;
507
- }
508
- /**
509
- * Generate TypeScript code for a subchart with correct package imports.
510
- * Uses published package imports instead of relative development paths.
511
- * @param name - The name of the subchart
512
- * @returns TypeScript code string for the subchart
513
- * @since 2.7.3
514
- */
515
- function exampleSubchartTs(name) {
516
- return `import { Rutter } from 'timonel';
517
- import { valuesRef } from './lib/helm.js';
518
-
519
- // Define subchart
520
- const rutter = new Rutter({
521
- meta: {
522
- name: '${name}',
523
- version: '0.1.0',
524
- description: '${name} subchart',
525
- appVersion: '1.0.0',
526
- },
527
- defaultValues: {
528
- image: { repository: 'nginx', tag: '1.27' },
529
- replicas: 1,
530
- service: { port: 80 },
531
- },
532
- });
533
-
534
- // Add Deployment manifest
535
- rutter.addManifest({
536
- apiVersion: 'apps/v1',
537
- kind: 'Deployment',
538
- metadata: {
539
- name: '${name}-deployment',
540
- labels: { app: '${name}' },
541
- },
542
- spec: {
543
- replicas: valuesRef('replicas'),
544
- selector: {
545
- matchLabels: { app: '${name}' },
546
- },
547
- template: {
548
- metadata: {
549
- labels: { app: '${name}' },
550
- },
551
- spec: {
552
- containers: [{
553
- name: '${name}',
554
- image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
555
- ports: [{ containerPort: 80 }],
556
- }],
557
- },
558
- },
559
- },
560
- }, 'deployment');
561
-
562
- // Add Service manifest
563
- rutter.addManifest({
564
- apiVersion: 'v1',
565
- kind: 'Service',
566
- metadata: {
567
- name: '${name}-service',
568
- labels: { app: '${name}' },
569
- },
570
- spec: {
571
- type: 'ClusterIP',
572
- ports: [{ port: valuesRef('service.port'), targetPort: 80 }],
573
- selector: { app: '${name}' },
574
- },
575
- }, 'service');
576
-
577
- export default function run(outDir: string) {
578
- rutter.write(outDir);
579
- }
580
-
581
- // Export rutter for umbrella chart
582
- export { rutter };
583
- `;
584
- }
585
- /**
586
- * Generate TypeScript code for a main chart with correct package imports.
587
- * Uses published package imports instead of relative development paths.
588
- * @param name - The name of the chart
589
- * @returns TypeScript code string for the chart
590
- * @since 2.7.3
591
- */
592
- function exampleChartTs(name) {
593
- return `import { Rutter } from 'timonel';
594
- import { valuesRef, helm } from 'timonel/lib/helm';
595
-
596
- // Define chart metadata and default/env values
597
- const rutter = new Rutter({
598
- meta: {
599
- name: '${name}',
600
- version: '0.1.0',
601
- description: 'Example Helm chart generated with timonel + cdk8s',
602
- appVersion: '1.0.0',
603
- },
604
- defaultValues: {
605
- image: { repository: 'nginx', tag: '1.27' },
606
- replicas: 1,
607
- service: { port: 80 },
608
- ingress: { enabled: false, host: 'example.com', className: 'nginx' },
609
- },
610
- envValues: {
611
- dev: { replicas: 1 },
612
- prod: { replicas: 3 },
613
- },
614
- });
615
-
616
- // Add Deployment manifest
617
- rutter.addManifest({
618
- apiVersion: 'apps/v1',
619
- kind: 'Deployment',
620
- metadata: {
621
- name: '${name}-deployment',
622
- labels: { app: '${name}' },
623
- },
624
- spec: {
625
- replicas: valuesRef('replicas'),
626
- selector: {
627
- matchLabels: { app: '${name}' },
628
- },
629
- template: {
630
- metadata: {
631
- labels: { app: '${name}' },
632
- },
633
- spec: {
634
- containers: [{
635
- name: '${name}',
636
- image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
637
- ports: [{ containerPort: 80 }],
638
- env: [
639
- { name: 'APP_NAME', value: '${name}' },
640
- { name: 'RELEASE', value: helm.releaseName },
641
- ],
642
- }],
643
- },
644
- },
645
- },
646
- }, 'deployment');
647
-
648
- // Add Service manifest
649
- rutter.addManifest({
650
- apiVersion: 'v1',
651
- kind: 'Service',
652
- metadata: {
653
- name: '${name}-service',
654
- labels: { app: '${name}' },
655
- },
656
- spec: {
657
- type: 'ClusterIP',
658
- ports: [{ port: valuesRef('service.port'), targetPort: 80 }],
659
- selector: { app: '${name}' },
660
- },
661
- }, 'service');
662
-
663
- // Add Ingress manifest
664
- rutter.addManifest({
665
- apiVersion: 'networking.k8s.io/v1',
666
- kind: 'Ingress',
667
- metadata: {
668
- name: '${name}-ingress',
669
- labels: { app: '${name}' },
670
- },
671
- spec: {
672
- ingressClassName: String(valuesRef('ingress.className')),
673
- rules: [{
674
- host: String(valuesRef('ingress.host')),
675
- http: {
676
- paths: [{
677
- path: '/',
678
- pathType: 'Prefix',
679
- backend: {
680
- service: {
681
- name: '${name}-service',
682
- port: { number: valuesRef('service.port') },
683
- },
684
- },
685
- }],
686
- },
687
- }],
688
- },
689
- }, 'ingress');
690
-
691
- export default function run(outDir: string) {
692
- rutter.write(outDir);
693
- }
694
- `;
695
- }
696
- // eslint-disable-next-line sonarjs/cognitive-complexity -- CLI argument parsing requires multiple conditions
697
- function parseArgs() {
698
- /* eslint-disable security/detect-object-injection */
699
- const argv = process.argv.slice(2);
700
- const flags = { dryRun: false, silent: false, set: {} };
701
- const args = [];
702
- let cmd = '';
703
- for (let i = 0; i < argv.length; i++) {
704
- const arg = argv[i];
705
- if (arg === '--dry-run') {
706
- flags.dryRun = true;
707
- }
708
- else if (arg === '--silent') {
709
- flags.silent = true;
710
- }
711
- else if (arg === '--env' && i + 1 < argv.length) {
712
- const nextArg = argv[++i];
713
- if (nextArg)
714
- flags.env = nextArg;
715
- }
716
- else if (arg === '--version-bump' && i + 1 < argv.length) {
717
- const nextArg = argv[++i];
718
- if (nextArg)
719
- flags.versionBump = nextArg;
720
- }
721
- else if (arg === '--set' && i + 1 < argv.length) {
722
- const setValue = argv[++i];
723
- if (setValue) {
724
- const [key, value] = setValue.split('=', 2);
725
- if (key && value !== undefined) {
726
- flags.set[key] = value;
727
- }
365
+ function parseFlags(args) {
366
+ const flags = {};
367
+ while (args.length > 0 && args[0]?.startsWith('-')) {
368
+ const flag = args.shift();
369
+ switch (flag) {
370
+ case '--dry-run':
371
+ flags.dryRun = true;
372
+ break;
373
+ case '--silent':
374
+ flags.silent = true;
375
+ break;
376
+ case '--env': {
377
+ const envValue = args.shift();
378
+ if (envValue)
379
+ flags.env = envValue;
380
+ break;
728
381
  }
729
- }
730
- else if (arg === '--version' || arg === '-v') {
731
- showVersion();
732
- process.exit(0);
733
- }
734
- else if (arg === '--help' || arg === '-h') {
735
- usageAndExit();
736
- }
737
- else if (arg && !arg.startsWith('--')) {
738
- if (!cmd) {
739
- cmd = arg;
740
- }
741
- else {
742
- args.push(arg);
382
+ case '--set': {
383
+ const setValue = args.shift();
384
+ if (setValue) {
385
+ flags.set = flags.set || [];
386
+ flags.set.push(setValue);
387
+ }
388
+ break;
743
389
  }
390
+ case '--version':
391
+ case '-v':
392
+ console.log('Timonel v0.1.0');
393
+ process.exit(0);
394
+ break;
395
+ case '--help':
396
+ case '-h':
397
+ usageAndExit();
398
+ break;
399
+ default:
400
+ usageAndExit(`Unknown flag: ${flag}`);
744
401
  }
745
402
  }
746
- /* eslint-enable security/detect-object-injection */
747
- return { cmd, args, flags };
403
+ return flags;
748
404
  }
749
- /**
750
- * Display the current version of Timonel CLI.
751
- * Reads version from package.json using ES modules compatible path resolution.
752
- * @since 2.7.3
753
- */
754
- function showVersion() {
755
- const packagePath = path.join(__dirname, '..', 'package.json');
756
- try {
757
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
758
- console.log(`timonel v${packageJson.version}`);
759
- }
760
- catch {
761
- console.log('timonel (version unknown)');
762
- }
763
- }
764
- function log(message, silent = false) {
765
- if (!silent) {
766
- console.log(message);
767
- }
768
- }
769
- async function main() {
770
- const { cmd, args, flags } = parseArgs();
771
- switch (cmd) {
405
+ async function executeCommand(command, args, flags) {
406
+ switch (command) {
772
407
  case 'init':
773
408
  await cmdInit(args[0], flags.silent);
774
409
  break;
775
- case 'validate':
776
- await cmdValidate(args[0], flags.silent);
777
- break;
778
410
  case 'synth':
779
- await cmdSynth(args[0], args[1], flags);
411
+ await cmdSynth(args[0], flags);
780
412
  break;
781
- case 'diff':
782
- await cmdDiff(args[0], args[1], flags.silent);
413
+ case 'validate':
414
+ await cmdValidate(flags);
783
415
  break;
784
416
  case 'deploy':
785
417
  await cmdDeploy(args[0], args[1], flags);
786
418
  break;
787
- case 'package':
788
- await cmdPackage(args[0], args[1], flags.silent);
419
+ case 'templates':
420
+ await cmdTemplates(flags);
789
421
  break;
790
422
  case 'umbrella':
791
423
  await cmdUmbrella(args[0], args.slice(1), flags);
792
424
  break;
425
+ case undefined:
426
+ usageAndExit('Missing command');
427
+ break;
793
428
  default:
794
- usageAndExit();
429
+ usageAndExit(`Unknown command: ${command}`);
795
430
  }
796
431
  }
797
- main().catch((err) => {
798
- console.error(err);
432
+ async function main() {
433
+ const args = process.argv.slice(2);
434
+ const command = args.shift();
435
+ const flags = parseFlags(args);
436
+ await executeCommand(command, args, flags);
437
+ }
438
+ main().catch((error) => {
439
+ console.error('Error:', error.message);
799
440
  process.exit(1);
800
441
  });
801
- //# sourceMappingURL=cli.js.map