timonel 2.8.0 → 2.8.3

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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli.d.ts +0 -1
  3. package/dist/cli.js +319 -191
  4. package/dist/index.d.ts +0 -5
  5. package/dist/index.js +0 -6
  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 +0 -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 +0 -324
  17. package/dist/lib/rutter.js +8 -352
  18. package/dist/lib/security.d.ts +0 -119
  19. package/dist/lib/security.js +4 -160
  20. package/dist/lib/umbrella.d.ts +0 -24
  21. package/dist/lib/umbrella.js +0 -24
  22. package/dist/lib/umbrellaRutter.d.ts +0 -71
  23. package/dist/lib/umbrellaRutter.js +2 -82
  24. package/dist/lib/utils/helmHelpers.d.ts +0 -39
  25. package/dist/lib/utils/helmHelpers.js +0 -32
  26. package/package.json +1 -1
  27. package/dist/cli.d.ts.map +0 -1
  28. package/dist/cli.js.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js.map +0 -1
  31. package/dist/lib/helm.d.ts.map +0 -1
  32. package/dist/lib/helm.js.map +0 -1
  33. package/dist/lib/helmChartWriter.d.ts.map +0 -1
  34. package/dist/lib/helmChartWriter.js.map +0 -1
  35. package/dist/lib/resources/baseResourceProvider.d.ts.map +0 -1
  36. package/dist/lib/resources/baseResourceProvider.js.map +0 -1
  37. package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +0 -1
  38. package/dist/lib/resources/cloud/aws/awsResources.js.map +0 -1
  39. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts.map +0 -1
  40. package/dist/lib/resources/cloud/aws/karpenterResources.js.map +0 -1
  41. package/dist/lib/rutter.d.ts.map +0 -1
  42. package/dist/lib/rutter.js.map +0 -1
  43. package/dist/lib/security.d.ts.map +0 -1
  44. package/dist/lib/security.js.map +0 -1
  45. package/dist/lib/umbrella.d.ts.map +0 -1
  46. package/dist/lib/umbrella.js.map +0 -1
  47. package/dist/lib/umbrellaRutter.d.ts.map +0 -1
  48. package/dist/lib/umbrellaRutter.js.map +0 -1
  49. package/dist/lib/utils/helmHelpers.d.ts.map +0 -1
  50. package/dist/lib/utils/helmHelpers.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import * as cp from 'child_process';
2
+ import { spawnSync, execSync } from 'child_process';
3
+ import { mkdirSync, existsSync, writeFileSync, rmSync, unlinkSync } from 'fs';
4
+ import { join, dirname, isAbsolute, resolve } from 'path';
5
+ import { fileURLToPath } from 'url';
3
6
  import * as fs from 'fs';
4
7
  import * as path from 'path';
5
- import { fileURLToPath } from 'url';
6
8
  import { SecurityUtils } from './lib/security.js';
7
- // ES modules equivalent of __dirname
8
9
  const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
+ const __dirname = dirname(__filename);
10
11
  const HELM_INSTALL_URL = 'https://helm.sh/docs/intro/install/';
11
12
  const HELM_ENV_VAR_MSG = 'Or set HELM_BIN environment variable to helm binary path.';
12
13
  function usageAndExit(msg) {
@@ -50,42 +51,36 @@ function usageAndExit(msg) {
50
51
  async function cmdInit(name, silent = false) {
51
52
  if (!name)
52
53
  usageAndExit('Missing <chart-name>');
53
- // Validate chart name for security using centralized validation
54
- if (!SecurityUtils.isValidChartName(name)) {
54
+ const validName = name;
55
+ if (!SecurityUtils.isValidChartName(validName)) {
55
56
  usageAndExit('Invalid chart name: must be RFC 1123 compliant (lowercase alphanumeric with hyphens)');
56
57
  }
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
- 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)) {
58
+ const sanitizedName = validName;
59
+ const base = SecurityUtils.validatePath(join(process.cwd(), 'charts', sanitizedName), process.cwd());
60
+ const file = join(base, 'chart.ts');
61
+ mkdirSync(base, { recursive: true });
62
+ if (existsSync(file)) {
64
63
  console.error(`File already exists: ${file}`);
65
64
  process.exit(1);
66
65
  }
67
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
68
- fs.writeFileSync(file, exampleChartTs(name));
66
+ writeFileSync(file, exampleChartTs(validName));
69
67
  log(SecurityUtils.sanitizeLogMessage(`Scaffold created at ${file}`), silent);
70
68
  }
71
69
  async function cmdValidate(projectDir, silent = false) {
72
70
  if (!projectDir)
73
71
  usageAndExit('Missing <projectDir>');
74
72
  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)) {
73
+ const basePath = isAbsolute(proj) ? dirname(proj) : process.cwd();
74
+ const chartTs = isAbsolute(proj)
75
+ ? SecurityUtils.validatePath(join(proj, 'chart.ts'), basePath)
76
+ : SecurityUtils.validatePath(join(process.cwd(), proj, 'chart.ts'), process.cwd());
77
+ if (!existsSync(chartTs)) {
82
78
  console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
83
79
  process.exit(1);
84
80
  }
85
- // Check if helm is available
86
81
  const helm = process.env['HELM_BIN'] || 'helm';
87
82
  try {
88
- cp.execSync(`${helm} version --short`, { stdio: 'ignore' });
83
+ execSync(`${helm} version --short`, { stdio: 'ignore' });
89
84
  }
90
85
  catch {
91
86
  console.error('✗ Helm not found. Install Helm to enable chart validation.');
@@ -93,12 +88,10 @@ async function cmdValidate(projectDir, silent = false) {
93
88
  console.error(` ${HELM_ENV_VAR_MSG}`);
94
89
  process.exit(1);
95
90
  }
96
- const tempDir = path.join(process.cwd(), '.timonel-validate');
91
+ const tempDir = join(process.cwd(), '.timonel-validate');
97
92
  try {
98
- // Generate chart to temp directory for validation
99
93
  await cmdSynth(projectDir, tempDir, { dryRun: false, silent: true, set: {} });
100
- // Run helm lint on generated chart
101
- const lintResult = cp.spawnSync(helm, ['lint', tempDir], {
94
+ const lintResult = spawnSync(helm, ['lint', tempDir], {
102
95
  stdio: silent ? 'pipe' : 'inherit',
103
96
  encoding: 'utf8',
104
97
  });
@@ -117,19 +110,11 @@ async function cmdValidate(projectDir, silent = false) {
117
110
  process.exit(1);
118
111
  }
119
112
  finally {
120
- // Cleanup temp directory
121
- if (fs.existsSync(tempDir)) {
122
- fs.rmSync(tempDir, { recursive: true, force: true });
113
+ if (existsSync(tempDir)) {
114
+ rmSync(tempDir, { recursive: true, force: true });
123
115
  }
124
116
  }
125
117
  }
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
118
  async function executeTypeScriptChart(resolvedPath, outDir, flags) {
134
119
  const wrapperScript = `
135
120
  import { pathToFileURL } from 'url';
@@ -156,10 +141,10 @@ fs.mkdirSync(outDir, { recursive: true });
156
141
  await Promise.resolve(runner(outDir));
157
142
  console.log('Chart written to ' + outDir);
158
143
  `;
159
- const wrapperFile = path.join(process.cwd(), '.timonel-wrapper.mjs');
144
+ const wrapperFile = join(process.cwd(), '.timonel-wrapper.mjs');
160
145
  try {
161
- fs.writeFileSync(wrapperFile, wrapperScript);
162
- const result = cp.spawnSync('npx', ['tsx', wrapperFile], {
146
+ writeFileSync(wrapperFile, wrapperScript);
147
+ const result = spawnSync('npx', ['tsx', wrapperFile], {
163
148
  stdio: flags?.silent ? 'pipe' : 'inherit',
164
149
  encoding: 'utf8',
165
150
  });
@@ -171,9 +156,8 @@ console.log('Chart written to ' + outDir);
171
156
  }
172
157
  }
173
158
  finally {
174
- // Cleanup wrapper file
175
- if (fs.existsSync(wrapperFile)) {
176
- fs.unlinkSync(wrapperFile);
159
+ if (existsSync(wrapperFile)) {
160
+ unlinkSync(wrapperFile);
177
161
  }
178
162
  }
179
163
  }
@@ -181,24 +165,20 @@ async function cmdSynth(projectDir, out, flags) {
181
165
  if (!projectDir)
182
166
  usageAndExit('Missing <projectDir>');
183
167
  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)) {
168
+ const basePath = isAbsolute(proj) ? dirname(proj) : process.cwd();
169
+ const chartTs = isAbsolute(proj)
170
+ ? SecurityUtils.validatePath(join(proj, 'chart.ts'), basePath)
171
+ : SecurityUtils.validatePath(join(process.cwd(), proj, 'chart.ts'), process.cwd());
172
+ if (!existsSync(chartTs)) {
191
173
  console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
192
174
  process.exit(1);
193
175
  }
194
- // Validate TypeScript file extension
195
176
  if (!SecurityUtils.isValidTypeScriptFile(chartTs)) {
196
177
  console.error('Security: Only TypeScript files (.ts) are allowed');
197
178
  process.exit(1);
198
179
  }
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');
180
+ const resolvedPath = resolve(chartTs);
181
+ const outDir = out || join(dirname(chartTs), 'dist');
202
182
  if (flags?.dryRun) {
203
183
  log(`[DRY RUN] Would write chart to ${outDir}`, flags.silent);
204
184
  return;
@@ -215,15 +195,17 @@ async function cmdSynth(projectDir, out, flags) {
215
195
  async function cmdDiff(projectDir, chartDir, silent = false) {
216
196
  if (!projectDir || !chartDir)
217
197
  usageAndExit('Missing <projectDir> or <chartDir>');
218
- const tempDir = path.join(process.cwd(), '.timonel-temp');
198
+ const validProjectDir = projectDir;
199
+ const validChartDir = chartDir;
200
+ const tempDir = join(process.cwd(), '.timonel-temp');
219
201
  try {
220
- // Generate chart to temp directory
221
- await cmdSynth(projectDir, tempDir);
222
- // Compare with existing chart
202
+ await cmdSynth(validProjectDir, tempDir);
223
203
  const diffCmd = process.platform === 'win32' ? 'fc' : 'diff';
224
- const args = process.platform === 'win32' ? ['/N', chartDir, tempDir] : ['-r', chartDir, tempDir];
204
+ const args = process.platform === 'win32'
205
+ ? ['/N', validChartDir, tempDir]
206
+ : ['-r', validChartDir, tempDir];
225
207
  log(`> ${diffCmd} ${args.join(' ')}`, silent);
226
- const res = cp.spawnSync(diffCmd, args, { stdio: 'inherit' });
208
+ const res = spawnSync(diffCmd, args, { stdio: 'inherit' });
227
209
  if (res.status === 0) {
228
210
  log('✓ No differences found', silent);
229
211
  }
@@ -233,19 +215,19 @@ async function cmdDiff(projectDir, chartDir, silent = false) {
233
215
  }
234
216
  }
235
217
  finally {
236
- // Cleanup temp directory
237
- if (fs.existsSync(tempDir)) {
238
- fs.rmSync(tempDir, { recursive: true, force: true });
218
+ if (existsSync(tempDir)) {
219
+ rmSync(tempDir, { recursive: true, force: true });
239
220
  }
240
221
  }
241
222
  }
242
223
  async function cmdDeploy(projectDir, releaseName, flags) {
243
224
  if (!projectDir || !releaseName)
244
225
  usageAndExit('Missing <projectDir> or <release>');
245
- // Check if helm is available
226
+ const validProjectDir = projectDir;
227
+ const validReleaseName = releaseName;
246
228
  const helm = process.env['HELM_BIN'] || 'helm';
247
229
  try {
248
- cp.execSync(`${helm} version --short`, { stdio: 'ignore' });
230
+ execSync(`${helm} version --short`, { stdio: 'ignore' });
249
231
  }
250
232
  catch {
251
233
  console.error('✗ Helm not found. Install Helm to enable deployment.');
@@ -253,21 +235,16 @@ async function cmdDeploy(projectDir, releaseName, flags) {
253
235
  console.error(` ${HELM_ENV_VAR_MSG}`);
254
236
  process.exit(1);
255
237
  }
256
- const tempDir = path.join(process.cwd(), '.timonel-deploy');
238
+ const tempDir = join(process.cwd(), '.timonel-deploy');
257
239
  try {
258
- // Generate chart
259
- await cmdSynth(projectDir, tempDir, flags);
260
- // Check if release exists
240
+ await cmdSynth(validProjectDir, tempDir, flags);
261
241
  const checkCmd = `${helm} status ${releaseName}`;
262
- const releaseExists = cp.spawnSync('sh', ['-c', checkCmd], { stdio: 'ignore' }).status === 0;
263
- // Build helm command
242
+ const releaseExists = spawnSync('sh', ['-c', checkCmd], { stdio: 'ignore' }).status === 0;
264
243
  const action = releaseExists ? 'upgrade' : 'install';
265
- const args = [action, releaseName, tempDir];
266
- // Add environment-specific values if specified
244
+ const args = [action, validReleaseName, tempDir];
267
245
  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)) {
246
+ const valuesFile = join(tempDir, `values-${flags.env}.yaml`);
247
+ if (existsSync(valuesFile)) {
271
248
  args.push('-f', valuesFile);
272
249
  }
273
250
  }
@@ -275,16 +252,15 @@ async function cmdDeploy(projectDir, releaseName, flags) {
275
252
  args.push('--dry-run');
276
253
  }
277
254
  log(`> ${helm} ${args.join(' ')}`, flags?.silent);
278
- const res = cp.spawnSync(helm, args, { stdio: 'inherit' });
255
+ const res = spawnSync(helm, args, { stdio: 'inherit' });
279
256
  if (res.status !== 0) {
280
257
  process.exit(res.status ?? 1);
281
258
  }
282
- log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${releaseName}`, flags?.silent);
259
+ log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${validReleaseName}`, flags?.silent);
283
260
  }
284
261
  finally {
285
- // Cleanup temp directory
286
- if (fs.existsSync(tempDir)) {
287
- fs.rmSync(tempDir, { recursive: true, force: true });
262
+ if (existsSync(tempDir)) {
263
+ rmSync(tempDir, { recursive: true, force: true });
288
264
  }
289
265
  }
290
266
  }
@@ -307,65 +283,99 @@ async function cmdUmbrella(subcommand, args, flags) {
307
283
  }
308
284
  }
309
285
  const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
286
+ const UMBRELLA_FILE_NAME = 'umbrella.ts';
310
287
  async function cmdUmbrellaInit(name, silent = false) {
311
288
  const MISSING_NAME_MSG = 'Missing umbrella chart name';
312
289
  if (!name)
313
290
  usageAndExit(MISSING_NAME_MSG);
314
- const base = path.join(process.cwd(), name);
315
- const umbrellaFile = path.join(base, 'umbrella.ts');
291
+ const validName = name;
292
+ const base = path.join(process.cwd(), validName);
293
+ const umbrellaFile = path.join(base, UMBRELLA_FILE_NAME);
316
294
  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
295
  fs.mkdirSync(base, { recursive: true });
319
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
320
296
  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
297
+ fs.writeFileSync(umbrellaFile, exampleUmbrellaTs(validName));
325
298
  const config = {
326
- name,
299
+ name: validName,
327
300
  version: '0.1.0',
328
301
  description: `${name} umbrella chart`,
329
302
  subcharts: [],
330
303
  };
331
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
332
304
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
333
305
  log(`Umbrella chart structure created at ${base}`, silent);
334
306
  log(`Add subcharts with: tl umbrella add <subchart-name>`, silent);
335
307
  }
336
- async function cmdUmbrellaAdd(subchartName, silent = false) {
337
- const MISSING_SUBCHART_MSG = 'Missing subchart name';
338
- if (!subchartName)
308
+ function updateUmbrellaTs(subchartPath, subchartName) {
309
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
310
+ if (!fs.existsSync(umbrellaFile)) {
311
+ console.error('umbrella.ts not found.');
312
+ return;
313
+ }
314
+ let content = fs.readFileSync(umbrellaFile, 'utf8');
315
+ const chartName = subchartName || path.basename(subchartPath);
316
+ const camelCaseName = chartName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
317
+ const importStatement = `import { rutter as ${camelCaseName} } from './charts/${subchartPath}/chart';`;
318
+ const importRegex = /import\s+.*?from\s+['"].*?['"];?/g;
319
+ const imports = content.match(importRegex) || [];
320
+ if (imports.length > 0) {
321
+ const lastImport = imports[imports.length - 1];
322
+ if (!lastImport) {
323
+ throw new Error('No valid import statements found');
324
+ }
325
+ const lastImportIndex = content.lastIndexOf(lastImport);
326
+ const insertIndex = lastImportIndex + lastImport.length;
327
+ if (!content.includes(importStatement)) {
328
+ content = content.slice(0, insertIndex) + '\n' + importStatement + content.slice(insertIndex);
329
+ }
330
+ }
331
+ const subchartsRegex = /subcharts:\s*\[([\s\S]*?)\]/;
332
+ const match = content.match(subchartsRegex);
333
+ if (match) {
334
+ const subchartsContent = match[1];
335
+ if (subchartsContent === undefined) {
336
+ throw new Error('Unable to parse subcharts content');
337
+ }
338
+ if (!subchartsContent.includes(`name: '${chartName}'`)) {
339
+ let newSubchartsContent;
340
+ const subchartEntry = `{ name: '${chartName}', rutter: ${camelCaseName} }`;
341
+ const hasExistingEntries = /\{\s*name:\s*['"]/.test(subchartsContent);
342
+ if (!hasExistingEntries) {
343
+ newSubchartsContent = `\n // Add your subcharts here:\n ${subchartEntry},\n `;
344
+ }
345
+ else {
346
+ const trimmedContent = subchartsContent.trimEnd();
347
+ newSubchartsContent = trimmedContent + `,\n ${subchartEntry}`;
348
+ }
349
+ content = content.replace(subchartsRegex, `subcharts: [${newSubchartsContent}\n ]`);
350
+ }
351
+ }
352
+ fs.writeFileSync(umbrellaFile, content);
353
+ }
354
+ async function cmdUmbrellaAdd(subchartPath, silent = false) {
355
+ const MISSING_SUBCHART_MSG = 'Missing subchart name or path';
356
+ if (!subchartPath)
339
357
  usageAndExit(MISSING_SUBCHART_MSG);
358
+ const validSubchartPath = subchartPath;
340
359
  const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
341
360
  if (!fs.existsSync(configFile)) {
342
361
  console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
343
362
  process.exit(1);
344
363
  }
345
364
  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
365
+ const subchartName = path.basename(validSubchartPath);
366
+ const subchartDir = path.join(process.cwd(), 'charts', validSubchartPath);
349
367
  fs.mkdirSync(subchartDir, { recursive: true });
350
368
  const chartFile = path.join(subchartDir, 'chart.ts');
351
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
352
369
  fs.writeFileSync(chartFile, exampleSubchartTs(subchartName));
353
- // Update config
354
370
  config.subcharts.push({
355
371
  name: subchartName,
356
372
  version: '0.1.0',
357
- path: `./charts/${subchartName}/chart.ts`,
373
+ path: `./charts/${validSubchartPath}/chart.ts`,
358
374
  });
359
375
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
360
- log(`Subchart ${subchartName} added to umbrella`, silent);
376
+ updateUmbrellaTs(validSubchartPath, subchartName);
377
+ log(`Subchart ${subchartName} added to umbrella at path '${validSubchartPath}'`, silent);
361
378
  }
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
379
  async function executeTypeScriptUmbrella(resolvedPath, outDir, flags) {
370
380
  const wrapperScript = `
371
381
  import { pathToFileURL } from 'url';
@@ -386,7 +396,7 @@ console.log('Umbrella chart written to ' + output);
386
396
  const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
387
397
  try {
388
398
  fs.writeFileSync(wrapperFile, wrapperScript);
389
- const result = cp.spawnSync('npx', ['tsx', wrapperFile], {
399
+ const result = spawnSync('npx', ['tsx', wrapperFile], {
390
400
  stdio: flags?.silent ? 'pipe' : 'inherit',
391
401
  encoding: 'utf8',
392
402
  });
@@ -398,7 +408,6 @@ console.log('Umbrella chart written to ' + output);
398
408
  }
399
409
  }
400
410
  finally {
401
- // Cleanup wrapper file
402
411
  if (fs.existsSync(wrapperFile)) {
403
412
  fs.unlinkSync(wrapperFile);
404
413
  }
@@ -410,17 +419,15 @@ async function cmdUmbrellaSynth(outDir, flags) {
410
419
  console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
411
420
  process.exit(1);
412
421
  }
413
- const umbrellaFile = path.join(process.cwd(), 'umbrella.ts');
422
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
414
423
  if (!fs.existsSync(umbrellaFile)) {
415
424
  console.error('umbrella.ts not found.');
416
425
  process.exit(1);
417
426
  }
418
- // Validate TypeScript file extension
419
427
  if (!SecurityUtils.isValidTypeScriptFile(umbrellaFile)) {
420
428
  console.error('Security: Only TypeScript files (.ts) are allowed');
421
429
  process.exit(1);
422
430
  }
423
- // Validate path to prevent traversal attacks
424
431
  const validatedPath = SecurityUtils.validatePath(umbrellaFile, process.cwd());
425
432
  const resolvedPath = path.resolve(validatedPath);
426
433
  const output = outDir || path.join(process.cwd(), 'dist');
@@ -440,20 +447,21 @@ async function cmdUmbrellaSynth(outDir, flags) {
440
447
  async function cmdPackage(chartDir, out, silent = false) {
441
448
  if (!chartDir)
442
449
  usageAndExit('Missing <chartDir>');
443
- const src = path.isAbsolute(chartDir) ? chartDir : path.join(process.cwd(), chartDir);
450
+ const validChartDir = chartDir;
451
+ const src = path.isAbsolute(validChartDir)
452
+ ? validChartDir
453
+ : path.join(process.cwd(), validChartDir);
444
454
  const chartYaml = path.join(src, 'Chart.yaml');
445
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
446
455
  if (!fs.existsSync(chartYaml)) {
447
456
  console.error(`Chart.yaml not found in ${src}`);
448
457
  process.exit(1);
449
458
  }
450
459
  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
460
  fs.mkdirSync(outDir, { recursive: true });
453
461
  const helm = process.env['HELM_BIN'] || 'helm';
454
462
  const args = ['package', src, '-d', outDir];
455
463
  log(`> ${helm} ${args.join(' ')}`, silent);
456
- const res = cp.spawnSync(helm, args, { stdio: 'inherit' });
464
+ const res = spawnSync(helm, args, { stdio: 'inherit' });
457
465
  if (res.error) {
458
466
  console.error(`✗ Failed to execute Helm: ${res.error.message}`);
459
467
  console.error(` Install: ${HELM_INSTALL_URL}`);
@@ -505,18 +513,11 @@ export default function run(outDir: string) {
505
513
  }
506
514
  `;
507
515
  }
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
516
  function exampleSubchartTs(name) {
516
517
  return `import { Rutter } from 'timonel';
517
- import { valuesRef } from 'timonel/lib/helm';
518
+ import { valuesRef, helm } from 'timonel/lib/helm';
518
519
 
519
- // Define subchart
520
+ // Create a new Rutter instance
520
521
  const rutter = new Rutter({
521
522
  meta: {
522
523
  name: '${name}',
@@ -531,19 +532,73 @@ const rutter = new Rutter({
531
532
  },
532
533
  });
533
534
 
534
- // Add resources
535
- rutter.addDeployment({
536
- name: '${name}-deployment',
537
- image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
538
- replicas: 1,
539
- containerPort: 80,
540
- });
535
+ // Add Deployment using addManifest with native Kubernetes object
536
+ rutter.addManifest({
537
+ apiVersion: 'apps/v1',
538
+ kind: 'Deployment',
539
+ metadata: {
540
+ name: '${name}',
541
+ labels: {
542
+ app: '${name}'
543
+ }
544
+ },
545
+ spec: {
546
+ replicas: valuesRef('replicas'),
547
+ selector: {
548
+ matchLabels: {
549
+ app: '${name}'
550
+ }
551
+ },
552
+ template: {
553
+ metadata: {
554
+ labels: {
555
+ app: '${name}'
556
+ }
557
+ },
558
+ spec: {
559
+ containers: [
560
+ {
561
+ name: '${name}',
562
+ image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
563
+ ports: [
564
+ {
565
+ name: 'http',
566
+ containerPort: 80,
567
+ protocol: 'TCP'
568
+ }
569
+ ]
570
+ }
571
+ ]
572
+ }
573
+ }
574
+ }
575
+ }, '${name}-deployment');
541
576
 
542
- rutter.addService({
543
- name: '${name}-service',
544
- ports: [{ port: 80, targetPort: 80 }],
545
- selector: { app: '${name}' },
546
- });
577
+ // Add Service using addManifest with native Kubernetes object
578
+ rutter.addManifest({
579
+ apiVersion: 'v1',
580
+ kind: 'Service',
581
+ metadata: {
582
+ name: '${name}',
583
+ labels: {
584
+ app: '${name}'
585
+ }
586
+ },
587
+ spec: {
588
+ type: 'ClusterIP',
589
+ ports: [
590
+ {
591
+ port: valuesRef('service.port'),
592
+ targetPort: 'http',
593
+ protocol: 'TCP',
594
+ name: 'http'
595
+ }
596
+ ],
597
+ selector: {
598
+ app: '${name}'
599
+ }
600
+ }
601
+ }, '${name}-service');
547
602
 
548
603
  export default function run(outDir: string) {
549
604
  rutter.write(outDir);
@@ -553,13 +608,6 @@ export default function run(outDir: string) {
553
608
  export { rutter };
554
609
  `;
555
610
  }
556
- /**
557
- * Generate TypeScript code for a main chart with correct package imports.
558
- * Uses published package imports instead of relative development paths.
559
- * @param name - The name of the chart
560
- * @returns TypeScript code string for the chart
561
- * @since 2.7.3
562
- */
563
611
  function exampleChartTs(name) {
564
612
  return `import { Rutter } from 'timonel';
565
613
  import { valuesRef, helm } from 'timonel/lib/helm';
@@ -577,6 +625,7 @@ const rutter = new Rutter({
577
625
  replicas: 1,
578
626
  service: { port: 80 },
579
627
  ingress: { enabled: false, host: 'example.com', className: 'nginx' },
628
+ namespace: { create: false, name: '${name}' },
580
629
  },
581
630
  envValues: {
582
631
  dev: { replicas: 1 },
@@ -584,54 +633,140 @@ const rutter = new Rutter({
584
633
  },
585
634
  });
586
635
 
587
- // Use Helm placeholders in strings passed to cdk8s constructs
588
- rutter.addDeployment({
589
- name: '${name}-deployment',
590
- image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
591
- replicas: valuesRef('replicas'),
592
- containerPort: 80,
593
- env: {
594
- APP_NAME: '${name}',
595
- RELEASE: helm.releaseName,
636
+ // Add Deployment using addManifest with native Kubernetes object
637
+ rutter.addManifest({
638
+ apiVersion: 'apps/v1',
639
+ kind: 'Deployment',
640
+ metadata: {
641
+ name: '${name}',
642
+ labels: {
643
+ app: '${name}'
644
+ }
596
645
  },
597
- });
646
+ spec: {
647
+ replicas: valuesRef('replicas'),
648
+ selector: {
649
+ matchLabels: {
650
+ app: '${name}'
651
+ }
652
+ },
653
+ template: {
654
+ metadata: {
655
+ labels: {
656
+ app: '${name}'
657
+ }
658
+ },
659
+ spec: {
660
+ containers: [
661
+ {
662
+ name: '${name}',
663
+ image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
664
+ ports: [
665
+ {
666
+ name: 'http',
667
+ containerPort: 80,
668
+ protocol: 'TCP'
669
+ }
670
+ ],
671
+ env: [
672
+ {
673
+ name: 'APP_NAME',
674
+ value: '${name}'
675
+ },
676
+ {
677
+ name: 'RELEASE',
678
+ value: helm.releaseName
679
+ }
680
+ ]
681
+ }
682
+ ]
683
+ }
684
+ }
685
+ }
686
+ }, '${name}-deployment');
598
687
 
599
- rutter.addService({
600
- name: '${name}-service',
601
- ports: [{ port: valuesRef('service.port'), targetPort: 80 }],
602
- type: 'ClusterIP',
603
- selector: { app: '${name}' },
604
- });
688
+ // Add Service using addManifest with native Kubernetes object
689
+ rutter.addManifest({
690
+ apiVersion: 'v1',
691
+ kind: 'Service',
692
+ metadata: {
693
+ name: '${name}',
694
+ labels: {
695
+ app: '${name}'
696
+ }
697
+ },
698
+ spec: {
699
+ type: 'ClusterIP',
700
+ ports: [
701
+ {
702
+ port: valuesRef('service.port'),
703
+ targetPort: 'http',
704
+ protocol: 'TCP',
705
+ name: 'http'
706
+ }
707
+ ],
708
+ selector: {
709
+ app: '${name}'
710
+ }
711
+ }
712
+ }, '${name}-service');
605
713
 
606
- // Optional ingress with advanced path routing and TLS support
607
- // Helm conditionals can be embedded as comments; template users can wrap templates with if blocks
608
- // Here we just generate an ingress; for real conditional generation you could split assets yourself.
609
- rutter.addIngress({
610
- name: '${name}-ingress',
611
- ingressClassName: String(valuesRef('ingress.className')),
612
- rules: [{
613
- host: String(valuesRef('ingress.host')),
614
- paths: [{
615
- path: '/',
616
- pathType: 'Prefix',
617
- backend: {
618
- service: {
619
- name: '${name}',
620
- port: { number: valuesRef('service.port') },
621
- },
622
- },
623
- }],
624
- }],
625
- });
714
+ // Add Ingress using addConditionalManifest with native Kubernetes object
715
+ rutter.addConditionalManifest({
716
+ apiVersion: 'networking.k8s.io/v1',
717
+ kind: 'Ingress',
718
+ metadata: {
719
+ name: '${name}',
720
+ labels: {
721
+ app: '${name}'
722
+ }
723
+ },
724
+ spec: {
725
+ ingressClassName: String(valuesRef('ingress.className')),
726
+ rules: [
727
+ {
728
+ host: String(valuesRef('ingress.host')),
729
+ http: {
730
+ paths: [
731
+ {
732
+ path: '/',
733
+ pathType: 'Prefix',
734
+ backend: {
735
+ service: {
736
+ name: '${name}',
737
+ port: {
738
+ number: valuesRef('service.port')
739
+ }
740
+ }
741
+ }
742
+ }
743
+ ]
744
+ }
745
+ }
746
+ ]
747
+ }
748
+ }, valuesRef('ingress.enabled'), '${name}-ingress');
749
+
750
+ // Add Namespace using addConditionalManifest
751
+ rutter.addConditionalManifest({
752
+ apiVersion: 'v1',
753
+ kind: 'Namespace',
754
+ metadata: {
755
+ name: String(valuesRef('namespace.name')),
756
+ labels: {
757
+ app: '${name}',
758
+ 'app.kubernetes.io/name': '${name}',
759
+ 'app.kubernetes.io/instance': helm.releaseName
760
+ }
761
+ }
762
+ }, valuesRef('namespace.create'), '${name}-namespace');
626
763
 
627
764
  export default function run(outDir: string) {
628
765
  rutter.write(outDir);
629
766
  }
630
767
  `;
631
768
  }
632
- // eslint-disable-next-line sonarjs/cognitive-complexity -- CLI argument parsing requires multiple conditions
633
769
  function parseArgs() {
634
- /* eslint-disable security/detect-object-injection */
635
770
  const argv = process.argv.slice(2);
636
771
  const flags = { dryRun: false, silent: false, set: {} };
637
772
  const args = [];
@@ -679,14 +814,8 @@ function parseArgs() {
679
814
  }
680
815
  }
681
816
  }
682
- /* eslint-enable security/detect-object-injection */
683
817
  return { cmd, args, flags };
684
818
  }
685
- /**
686
- * Display the current version of Timonel CLI.
687
- * Reads version from package.json using ES modules compatible path resolution.
688
- * @since 2.7.3
689
- */
690
819
  function showVersion() {
691
820
  const packagePath = path.join(__dirname, '..', 'package.json');
692
821
  try {
@@ -734,4 +863,3 @@ main().catch((err) => {
734
863
  console.error(err);
735
864
  process.exit(1);
736
865
  });
737
- //# sourceMappingURL=cli.js.map