timonel 3.1.1 → 3.1.2

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 (83) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +323 -695
  3. package/dist/cli.js +275 -15
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.js +12 -0
  6. package/dist/lib/helm.d.ts +471 -0
  7. package/dist/lib/helm.js +483 -0
  8. package/dist/lib/helmChartWriter.d.ts +157 -0
  9. package/dist/lib/helmChartWriter.js +171 -1
  10. package/dist/lib/policy/configurationLoader.d.ts +132 -0
  11. package/dist/lib/policy/configurationLoader.js +132 -0
  12. package/dist/lib/policy/errorContextGenerator.d.ts +89 -0
  13. package/dist/lib/policy/errorContextGenerator.js +99 -2
  14. package/dist/lib/policy/errors.d.ts +35 -0
  15. package/dist/lib/policy/errors.js +36 -0
  16. package/dist/lib/policy/index.d.ts +9 -0
  17. package/dist/lib/policy/index.js +16 -0
  18. package/dist/lib/policy/parallelExecutor.d.ts +90 -0
  19. package/dist/lib/policy/parallelExecutor.js +86 -3
  20. package/dist/lib/policy/pluginLoader.d.ts +92 -0
  21. package/dist/lib/policy/pluginLoader.js +92 -1
  22. package/dist/lib/policy/pluginRegistry.d.ts +54 -0
  23. package/dist/lib/policy/pluginRegistry.js +56 -0
  24. package/dist/lib/policy/policyEngine.d.ts +137 -0
  25. package/dist/lib/policy/policyEngine.js +191 -5
  26. package/dist/lib/policy/resultAggregator.d.ts +46 -0
  27. package/dist/lib/policy/resultAggregator.js +69 -1
  28. package/dist/lib/policy/resultFormatter.d.ts +88 -0
  29. package/dist/lib/policy/resultFormatter.js +101 -0
  30. package/dist/lib/policy/types.d.ts +136 -0
  31. package/dist/lib/policy/types.js +8 -0
  32. package/dist/lib/policy/validationCache.d.ts +146 -0
  33. package/dist/lib/policy/validationCache.js +142 -6
  34. package/dist/lib/resources/baseResourceProvider.d.ts +45 -0
  35. package/dist/lib/resources/baseResourceProvider.js +48 -1
  36. package/dist/lib/resources/cloud/aws/awsResources.d.ts +192 -0
  37. package/dist/lib/resources/cloud/aws/awsResources.js +163 -1
  38. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +131 -0
  39. package/dist/lib/resources/cloud/aws/karpenterResources.js +77 -0
  40. package/dist/lib/rutter.d.ts +381 -3
  41. package/dist/lib/rutter.js +439 -28
  42. package/dist/lib/security.d.ts +123 -0
  43. package/dist/lib/security.js +162 -4
  44. package/dist/lib/templates/flexible-subchart.d.ts +52 -0
  45. package/dist/lib/templates/flexible-subchart.js +70 -0
  46. package/dist/lib/templates/umbrella-chart.d.ts +27 -0
  47. package/dist/lib/templates/umbrella-chart.js +89 -0
  48. package/dist/lib/types.d.ts +26 -0
  49. package/dist/lib/umbrella.d.ts +23 -0
  50. package/dist/lib/umbrella.js +23 -0
  51. package/dist/lib/umbrellaRutter.d.ts +75 -0
  52. package/dist/lib/umbrellaRutter.js +82 -2
  53. package/dist/lib/utils/envVarsLoader.d.ts +49 -0
  54. package/dist/lib/utils/envVarsLoader.js +53 -0
  55. package/dist/lib/utils/helmConstructSerializer.d.ts +17 -0
  56. package/dist/lib/utils/helmConstructSerializer.js +22 -0
  57. package/dist/lib/utils/helmControlStructures.d.ts +194 -0
  58. package/dist/lib/utils/helmControlStructures.js +180 -0
  59. package/dist/lib/utils/helmHelpers/envHelpers.d.ts +13 -0
  60. package/dist/lib/utils/helmHelpers/envHelpers.js +13 -0
  61. package/dist/lib/utils/helmHelpers/gitopsHelpers.d.ts +13 -0
  62. package/dist/lib/utils/helmHelpers/gitopsHelpers.js +13 -0
  63. package/dist/lib/utils/helmHelpers/index.d.ts +74 -0
  64. package/dist/lib/utils/helmHelpers/index.js +85 -1
  65. package/dist/lib/utils/helmHelpers/observabilityHelpers.d.ts +13 -0
  66. package/dist/lib/utils/helmHelpers/observabilityHelpers.js +13 -0
  67. package/dist/lib/utils/helmHelpers/types.d.ts +23 -0
  68. package/dist/lib/utils/helmHelpers/types.js +4 -0
  69. package/dist/lib/utils/helmHelpers/validationHelpers.d.ts +13 -0
  70. package/dist/lib/utils/helmHelpers/validationHelpers.js +13 -0
  71. package/dist/lib/utils/helmHelpers.d.ts +62 -0
  72. package/dist/lib/utils/helmHelpers.js +77 -0
  73. package/dist/lib/utils/helmYamlSerializer.d.ts +77 -0
  74. package/dist/lib/utils/helmYamlSerializer.js +398 -21
  75. package/dist/lib/utils/logger.d.ts +153 -0
  76. package/dist/lib/utils/logger.js +170 -2
  77. package/dist/lib/utils/valuesRef.d.ts +181 -50
  78. package/dist/lib/utils/valuesRef.js +168 -170
  79. package/dist/lib/validation/inputValidator.d.ts +45 -0
  80. package/dist/lib/validation/inputValidator.js +67 -2
  81. package/dist/types/index.d.ts +34 -0
  82. package/dist/types/index.js +3 -0
  83. package/package.json +31 -38
package/dist/cli.js CHANGED
@@ -9,6 +9,11 @@ import { createLogger } from './lib/utils/logger.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
  const esmRequire = createRequire(import.meta.url);
12
+ /**
13
+ * Resolves the project-local tsx CLI entry point for spawning through Node.js.
14
+ * @returns Absolute filesystem path to the tsx CLI module.
15
+ * @since 2.12.2 Prevents reliance on npx downloads during synthesis.
16
+ */
12
17
  function getLocalTsxCliPath() {
13
18
  try {
14
19
  return esmRequire.resolve('tsx/cli');
@@ -22,6 +27,12 @@ function getLocalTsxCliPath() {
22
27
  process.exit(1);
23
28
  }
24
29
  }
30
+ /**
31
+ * Escapes a string for safe inclusion inside single-quoted TypeScript literals.
32
+ * @param value - Path or text segment that needs escaping.
33
+ * @returns Escaped string suitable for single-quoted literals.
34
+ * @since 2.12.2 Avoids malformed chart.ts rewrites when directories contain quotes.
35
+ */
25
36
  function escapeForSingleQuotedLiteral(value) {
26
37
  return value
27
38
  .replace(/\\/g, '\\\\')
@@ -30,6 +41,14 @@ function escapeForSingleQuotedLiteral(value) {
30
41
  .replace(/\n/g, '\\n')
31
42
  .replace(/\t/g, '\\t');
32
43
  }
44
+ /**
45
+ * Resolves and validates an output directory against traversal and file misdirection attempts.
46
+ * @param requestedOutDir - Raw output directory provided via CLI arguments.
47
+ * @param baseDir - Directory that relative paths must remain within.
48
+ * @param silent - When true, suppresses console error output on failure.
49
+ * @returns Absolute, security-checked directory path.
50
+ * @since 2.12.2 Protects against traversal while permitting explicit absolute destinations.
51
+ */
33
52
  function resolveOutputDirectory(requestedOutDir, baseDir, silent) {
34
53
  let validatedOutDir;
35
54
  try {
@@ -43,8 +62,11 @@ function resolveOutputDirectory(requestedOutDir, baseDir, silent) {
43
62
  }
44
63
  process.exit(1);
45
64
  }
46
- if (fs.existsSync(validatedOutDir)) {
65
+ if (
66
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
67
+ fs.existsSync(validatedOutDir)) {
47
68
  try {
69
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
48
70
  const stats = fs.statSync(validatedOutDir);
49
71
  if (!stats.isDirectory()) {
50
72
  if (!silent) {
@@ -63,37 +85,65 @@ function resolveOutputDirectory(requestedOutDir, baseDir, silent) {
63
85
  return validatedOutDir;
64
86
  }
65
87
  const TSX_CLI_PATH = getLocalTsxCliPath();
88
+ // Constants
66
89
  const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
67
90
  const UMBRELLA_FILE_NAME = 'umbrella.ts';
68
91
  const PACKAGE_JSON_FILE = 'package.json';
92
+ // Helper functions
93
+ /**
94
+ * Retrieves the version from the package manifest located next to the compiled CLI file
95
+ * @returns The version number or 'unknown' if not found
96
+ * @since 2.11.1 Uses the CLI directory as the base for manifest lookup in installed scenarios
97
+ */
69
98
  function getVersion() {
70
99
  try {
71
100
  const packagePath = path.resolve(__dirname, '..', PACKAGE_JSON_FILE);
72
101
  const allowedBase = path.resolve(__dirname, '..');
73
102
  const validatedPath = SecurityUtils.validatePath(packagePath, allowedBase);
103
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
74
104
  const packageJson = JSON.parse(fs.readFileSync(validatedPath, 'utf8'));
75
105
  if (packageJson.version) {
76
106
  return packageJson.version;
77
107
  }
78
108
  }
79
109
  catch {
110
+ // Fallback handled below
80
111
  }
81
112
  if (process.env.npm_package_version) {
82
113
  return process.env.npm_package_version;
83
114
  }
84
115
  return 'unknown';
85
116
  }
117
+ // Create CLI logger instance
86
118
  const cliLogger = createLogger('cli');
119
+ /**
120
+ * Enhanced logging function that respects silent flag globally
121
+ * @param msg - Message to log
122
+ * @param silent - Whether to suppress output
123
+ * @since 2.10.3
124
+ */
87
125
  function log(msg, silent = false) {
88
126
  if (!silent) {
89
127
  cliLogger.info(msg, { operation: 'cli_log' });
90
128
  }
91
129
  }
130
+ /**
131
+ * Enhanced error logging function that respects silent flag
132
+ * @param msg - Error message to log
133
+ * @param silent - Whether to suppress output
134
+ * @since 2.10.3
135
+ */
92
136
  function logError(msg, silent = false) {
93
137
  if (!silent) {
94
138
  cliLogger.error(msg, { operation: 'cli_error' });
95
139
  }
96
140
  }
141
+ /**
142
+ * Display usage information and exit
143
+ * @param msg - Optional error message
144
+ * @param silent - Whether to suppress output
145
+ * @since 2.9.2
146
+ */
97
147
  function usageAndExit(msg, silent = false) {
98
148
  if (msg) {
99
149
  logError(`Error: ${msg}`, silent);
@@ -135,37 +185,68 @@ function usageAndExit(msg, silent = false) {
135
185
  }
136
186
  process.exit(msg ? 1 : 0);
137
187
  }
138
- async function cmdInit(name, silent = false) {
188
+ /**
189
+ * Initialize a new chart
190
+ * @param name - The chart name to create
191
+ * @param silent - Whether to suppress output messages
192
+ * @param dryRun - When true, validate and report the operation without writing files
193
+ * @since 2.8.4 Updated to mention Helm chart generation instead of Kubernetes manifests
194
+ */
195
+ async function cmdInit(name, silent = false, dryRun = false) {
139
196
  if (!name)
140
197
  usageAndExit('Missing <chart-name>');
141
- const validName = name;
198
+ const validName = name; // Now guaranteed to be defined
199
+ // Validate chart name for security
142
200
  if (!SecurityUtils.isValidChartName(validName)) {
143
201
  usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
144
202
  }
145
203
  const cwd = process.cwd();
146
204
  const base = SecurityUtils.validatePath(path.join(cwd, validName), cwd);
147
205
  const chartFile = SecurityUtils.validatePath(path.join(base, 'chart.ts'), cwd);
206
+ if (dryRun) {
207
+ log(`Dry run: would create chart '${SecurityUtils.sanitizeLogMessage(validName)}' at ${SecurityUtils.sanitizeLogMessage(base)}`, silent);
208
+ return;
209
+ }
210
+ // Create directory
211
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
148
212
  fs.mkdirSync(base, { recursive: true });
213
+ // Import template generator
149
214
  const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
215
+ // Write chart file only - following CDK8s best practices
216
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
150
217
  fs.writeFileSync(chartFile, generateFlexibleSubchartTemplate(validName));
151
218
  log(`Chart created at ${base}`, silent);
152
219
  log(`Generated chart.ts file`, silent);
153
220
  log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
154
221
  }
222
+ /**
223
+ * Synthesizes a chart to generate a complete Helm chart structure.
224
+ * Creates Chart.yaml, values.yaml, templates/, and _helpers.tpl files.
225
+ * Supports both chart directory and output directory parameters.
226
+ *
227
+ * @param chartDirOrOutDir - Either the chart directory path or output directory path
228
+ * @param flags - CLI flags for controlling synthesis behavior
229
+ * @param explicitOutDir - Optional explicit destination directory for generated charts
230
+ * @since 2.8.4
231
+ * @since 2.12.2 Supports explicit output directory argument while retaining legacy behavior
232
+ */
155
233
  async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
156
234
  const cwd = process.cwd();
157
235
  let chartDir = cwd;
158
236
  let outDir;
237
+ // If the parameter is a directory containing chart.ts, use it as chart directory
159
238
  if (chartDirOrOutDir) {
160
239
  const resolvedPath = path.resolve(chartDirOrOutDir);
161
240
  const validatedPath = SecurityUtils.validatePath(resolvedPath, cwd, { allowAbsolute: true });
162
241
  const chartTsPath = SecurityUtils.validatePath(path.join(validatedPath, 'chart.ts'), cwd, {
163
242
  allowAbsolute: true,
164
243
  });
244
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
165
245
  if (fs.existsSync(chartTsPath)) {
166
246
  chartDir = validatedPath;
167
247
  }
168
248
  else {
249
+ // Otherwise, treat it as output directory for backward compatibility
169
250
  outDir = chartDirOrOutDir;
170
251
  }
171
252
  }
@@ -178,20 +259,30 @@ async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
178
259
  const defaultOutDir = SecurityUtils.validatePath(path.join(chartDir, 'dist'), cwd, {
179
260
  allowAbsolute: true,
180
261
  });
262
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
181
263
  if (!fs.existsSync(chartFile)) {
182
264
  console.error('chart.ts not found. Run `tl init` first.');
183
265
  process.exit(1);
184
266
  }
185
267
  const requestedOutDir = outDir ?? defaultOutDir;
186
268
  const validatedOutDir = resolveOutputDirectory(requestedOutDir, chartDir, flags?.silent);
269
+ if (flags?.dryRun) {
270
+ log(`Dry run: would synthesize ${SecurityUtils.sanitizeLogMessage(chartFile)} to ${SecurityUtils.sanitizeLogMessage(validatedOutDir)}`, flags.silent);
271
+ return;
272
+ }
273
+ // Read the original chart file and modify the writeHelmChart output directory
274
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
187
275
  const originalContent = fs.readFileSync(chartFile, 'utf8');
188
276
  const safeOutDirLiteral = escapeForSingleQuotedLiteral(validatedOutDir);
189
277
  let modifiedContent = originalContent.replace(/chart\.writeHelmChart\(['"][^'"]*['"]\)/, `chart.writeHelmChart('${safeOutDirLiteral}')`);
190
278
  if (modifiedContent === originalContent) {
191
279
  modifiedContent = originalContent.replace(/chart\.write\(['"][^'"]*['"]\)/, `chart.write('${safeOutDirLiteral}')`);
192
280
  }
281
+ // Create a temporary modified chart file
193
282
  const tempChartFile = SecurityUtils.validatePath(path.join(chartDir, '.timonel-temp-chart.ts'), cwd, { allowAbsolute: true });
283
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
194
284
  fs.writeFileSync(tempChartFile, modifiedContent);
285
+ // Create wrapper script for tsx execution
195
286
  const wrapperScript = `
196
287
  import { pathToFileURL } from 'url';
197
288
 
@@ -200,6 +291,7 @@ await import(pathToFileURL(${JSON.stringify(tempChartFile)}).href);
200
291
  `;
201
292
  const wrapperFile = path.join(chartDir, '.timonel-wrapper.mjs');
202
293
  try {
294
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
203
295
  fs.writeFileSync(wrapperFile, wrapperScript);
204
296
  const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
205
297
  stdio: flags?.silent ? 'pipe' : 'inherit',
@@ -214,14 +306,28 @@ await import(pathToFileURL(${JSON.stringify(tempChartFile)}).href);
214
306
  }
215
307
  }
216
308
  finally {
309
+ // Clean up wrapper file
310
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
217
311
  if (fs.existsSync(wrapperFile)) {
312
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
218
313
  fs.unlinkSync(wrapperFile);
219
314
  }
315
+ // Clean up temporary chart file
316
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
220
317
  if (fs.existsSync(tempChartFile)) {
318
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
221
319
  fs.unlinkSync(tempChartFile);
222
320
  }
223
321
  }
224
322
  }
323
+ /**
324
+ * Validates a Helm chart using helm lint command.
325
+ * Runs helm lint on the current directory to check chart validity.
326
+ *
327
+ * @param flags - CLI flags for controlling validation behavior
328
+ * @since 2.8.4
329
+ * @since 2.11.1 Supports --env and --set flags for environment-specific linting
330
+ */
225
331
  async function cmdValidate(flags) {
226
332
  const lintArgs = ['lint', '.'];
227
333
  if (flags?.env) {
@@ -239,6 +345,10 @@ async function cmdValidate(flags) {
239
345
  lintArgs.push('--set', setValue);
240
346
  }
241
347
  }
348
+ if (flags?.dryRun) {
349
+ log('Dry run: would execute Helm lint for the current chart', flags.silent);
350
+ return;
351
+ }
242
352
  const result = spawnSync('helm', lintArgs, {
243
353
  stdio: flags?.silent ? 'pipe' : 'inherit',
244
354
  encoding: 'utf8',
@@ -250,20 +360,39 @@ async function cmdValidate(flags) {
250
360
  process.exit(result.status ?? 1);
251
361
  }
252
362
  }
363
+ /**
364
+ * Validates Helm release name format to prevent command injection.
365
+ * @param release - The release name to validate
366
+ * @since 2.12.3
367
+ */
253
368
  function validateReleaseName(release) {
369
+ // Helm naming rules: lowercase alphanumeric with hyphens, max 53 chars
370
+ // eslint-disable-next-line security/detect-unsafe-regex -- Simple pattern, bounded length check
254
371
  if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(release) || release.length > 53) {
255
372
  console.error(`Invalid release name: ${SecurityUtils.sanitizeLogMessage(release)}`);
256
373
  console.error('Release name must be lowercase alphanumeric with hyphens (max 53 chars)');
257
374
  process.exit(1);
258
375
  }
259
376
  }
377
+ /**
378
+ * Validates Kubernetes namespace format to prevent command injection.
379
+ * @param namespace - The namespace to validate
380
+ * @since 2.12.3
381
+ */
260
382
  function validateNamespace(namespace) {
383
+ // Kubernetes naming rules: lowercase alphanumeric with hyphens, max 63 chars
384
+ // eslint-disable-next-line security/detect-unsafe-regex -- Simple pattern, bounded length check
261
385
  if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) || namespace.length > 63) {
262
386
  console.error(`Invalid namespace: ${SecurityUtils.sanitizeLogMessage(namespace)}`);
263
387
  console.error('Namespace must be lowercase alphanumeric with hyphens (max 63 chars)');
264
388
  process.exit(1);
265
389
  }
266
390
  }
391
+ /**
392
+ * Validates --set flag format to prevent command injection.
393
+ * @param setValue - The --set value to validate
394
+ * @since 2.12.3
395
+ */
267
396
  function validateSetFlag(setValue) {
268
397
  if (!/^[a-zA-Z0-9._[\]-]+=.+$/.test(setValue)) {
269
398
  console.error(`Invalid --set format: ${SecurityUtils.sanitizeLogMessage(setValue)}`);
@@ -271,9 +400,11 @@ function validateSetFlag(setValue) {
271
400
  process.exit(1);
272
401
  }
273
402
  }
274
- async function cmdDeploy(release, namespace, flags) {
275
- if (!release)
276
- usageAndExit('Missing <release>');
403
+ /**
404
+ * Build validated Helm arguments for deployment.
405
+ * @returns Validated argument list for `helm upgrade --install`
406
+ */
407
+ function buildDeployArgs(release, namespace, flags) {
277
408
  validateReleaseName(release);
278
409
  const args = ['upgrade', '--install', release, '.'];
279
410
  if (namespace) {
@@ -296,7 +427,30 @@ async function cmdDeploy(release, namespace, flags) {
296
427
  args.push('--set', setValue);
297
428
  }
298
429
  }
299
- const result = spawnSync('helm', args.filter((arg) => Boolean(arg)), {
430
+ return args;
431
+ }
432
+ /**
433
+ * Deploys a Helm chart to a Kubernetes cluster.
434
+ * Uses helm upgrade --install to deploy or update a release.
435
+ *
436
+ * @param release - The release name for the deployment
437
+ * @param namespace - Optional namespace for the deployment
438
+ * @param flags - CLI flags for controlling deployment behavior
439
+ * @since 2.8.4
440
+ * @since 2.11.1 Honors --env and --set flags when invoking helm upgrade
441
+ */
442
+ async function cmdDeploy(release, namespace, flags) {
443
+ if (!release)
444
+ usageAndExit('Missing <release>');
445
+ const args = buildDeployArgs(release, namespace, flags);
446
+ if (flags?.dryRun) {
447
+ const namespaceDescription = namespace
448
+ ? ` in namespace '${SecurityUtils.sanitizeLogMessage(namespace)}'`
449
+ : '';
450
+ log(`Dry run: would execute Helm upgrade --install for release '${SecurityUtils.sanitizeLogMessage(release)}'${namespaceDescription}`, flags.silent);
451
+ return;
452
+ }
453
+ const result = spawnSync('helm', args, {
300
454
  stdio: flags?.silent ? 'pipe' : 'inherit',
301
455
  encoding: 'utf8',
302
456
  });
@@ -307,6 +461,13 @@ async function cmdDeploy(release, namespace, flags) {
307
461
  process.exit(result.status ?? 1);
308
462
  }
309
463
  }
464
+ /**
465
+ * Lists available chart templates.
466
+ * Shows all available templates that can be used with tl init.
467
+ *
468
+ * @param flags - CLI flags for controlling output format
469
+ * @since 2.8.4
470
+ */
310
471
  async function cmdTemplates(flags) {
311
472
  const templates = [
312
473
  {
@@ -326,6 +487,7 @@ async function cmdTemplates(flags) {
326
487
  },
327
488
  ];
328
489
  if (flags?.silent) {
490
+ // In silent mode, output only JSON for programmatic consumption
329
491
  console.log(JSON.stringify(templates, null, 2));
330
492
  }
331
493
  else {
@@ -337,6 +499,15 @@ async function cmdTemplates(flags) {
337
499
  }
338
500
  }
339
501
  const UMBRELLA_SYNTH_MODES = ['dependencies', 'inline'];
502
+ /**
503
+ * Handles umbrella chart commands (init, add, synth).
504
+ * Routes to appropriate umbrella subcommand handlers.
505
+ *
506
+ * @param subcommand - The umbrella subcommand to execute
507
+ * @param args - Arguments for the subcommand
508
+ * @param flags - CLI flags for controlling behavior
509
+ * @since 2.8.4
510
+ */
340
511
  async function cmdUmbrella(subcommand, args, flags) {
341
512
  if (!subcommand)
342
513
  usageAndExit('Missing umbrella subcommand');
@@ -345,10 +516,10 @@ async function cmdUmbrella(subcommand, args, flags) {
345
516
  const mergedFlags = mergeCliFlags(flags, subcommandFlags);
346
517
  switch (subcommand) {
347
518
  case 'init':
348
- await cmdUmbrellaInit(workingArgs[0], mergedFlags.silent);
519
+ await cmdUmbrellaInit(workingArgs[0], mergedFlags.silent, mergedFlags.dryRun);
349
520
  break;
350
521
  case 'add':
351
- await cmdUmbrellaAdd(workingArgs[0], mergedFlags.silent);
522
+ await cmdUmbrellaAdd(workingArgs[0], mergedFlags.silent, mergedFlags.dryRun);
352
523
  break;
353
524
  case 'synth':
354
525
  await cmdUmbrellaSynth(workingArgs[0], mergedFlags);
@@ -357,44 +528,73 @@ async function cmdUmbrella(subcommand, args, flags) {
357
528
  usageAndExit(`Unknown umbrella subcommand: ${subcommand}`);
358
529
  }
359
530
  }
360
- async function cmdUmbrellaInit(name, silent = false) {
531
+ /**
532
+ * Initializes a new umbrella chart.
533
+ * Creates an umbrella chart structure for managing multiple subcharts.
534
+ *
535
+ * @param name - The name of the umbrella chart
536
+ * @param silent - Whether to suppress output messages
537
+ * @param dryRun - When true, validate and report the operation without writing files
538
+ * @since 2.8.4
539
+ */
540
+ async function cmdUmbrellaInit(name, silent = false, dryRun = false) {
361
541
  const MISSING_NAME_MSG = 'Missing umbrella chart name';
362
542
  if (!name)
363
543
  usageAndExit(MISSING_NAME_MSG);
364
- const validName = name;
544
+ const validName = name; // Now guaranteed to be defined
365
545
  const base = path.join(process.cwd(), validName);
366
546
  const umbrellaFile = path.join(base, UMBRELLA_FILE_NAME);
367
547
  const configFile = path.join(base, UMBRELLA_CONFIG_FILE);
548
+ if (dryRun) {
549
+ log(`Dry run: would create umbrella chart '${SecurityUtils.sanitizeLogMessage(validName)}' at ${SecurityUtils.sanitizeLogMessage(base)}`, silent);
550
+ return;
551
+ }
552
+ // Create directory
553
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
368
554
  fs.mkdirSync(base, { recursive: true });
555
+ // Create umbrella.ts
556
+ // Import template generator
369
557
  const { generateUmbrellaChart } = await import('./lib/templates/umbrella-chart.js');
558
+ // Write umbrella.ts
559
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
370
560
  fs.writeFileSync(umbrellaFile, generateUmbrellaChart(validName));
561
+ // Create umbrella.config.json
371
562
  const config = {
372
563
  name: validName,
373
564
  version: getVersion(),
374
565
  description: `${validName} umbrella chart`,
375
566
  subcharts: [],
376
567
  };
568
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
377
569
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
570
+ // Create charts directory for subcharts
378
571
  const chartsDir = path.join(base, 'charts');
572
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
379
573
  fs.mkdirSync(chartsDir, { recursive: true });
380
574
  log(`Umbrella chart structure created at ${base}`, silent);
381
575
  log(`Add subcharts with: tl umbrella add <subchart-name>`, silent);
382
576
  }
577
+ /** Convert a kebab-case subchart name to the generated camelCase import identifier. */
383
578
  function toCamelCase(str) {
384
579
  return str.replace(/-([a-z])/g, (g) => g[1]?.toUpperCase() || '');
385
580
  }
581
+ /** Find the final top-level import line in generated umbrella TypeScript source. */
386
582
  function findLastImportIndex(lines) {
387
583
  let lastImportIndex = -1;
388
584
  for (let i = 0; i < lines.length; i++) {
585
+ // eslint-disable-next-line security/detect-object-injection
389
586
  if (lines[i]?.trim().startsWith('import ')) {
390
587
  lastImportIndex = i;
588
+ // eslint-disable-next-line security/detect-object-injection
391
589
  }
392
590
  else if (lines[i]?.trim() && !lines[i]?.trim().startsWith('import ')) {
591
+ // Stop at first non-import, non-empty line
393
592
  break;
394
593
  }
395
594
  }
396
595
  return lastImportIndex;
397
596
  }
597
+ /** Add an import statement to generated umbrella source without duplicating it. */
398
598
  function addImportStatement(content, importStatement) {
399
599
  if (content.includes(importStatement)) {
400
600
  return content;
@@ -409,6 +609,7 @@ function addImportStatement(content, importStatement) {
409
609
  }
410
610
  return lines.join('\n');
411
611
  }
612
+ /** Rebuild the generated `SUBCHARTS` array body with a new unique entry. */
412
613
  function buildSubchartsContent(subchartsContent, subchartEntry) {
413
614
  const existingEntries = (subchartsContent || '')
414
615
  .split('\n')
@@ -425,6 +626,7 @@ function buildSubchartsContent(subchartsContent, subchartEntry) {
425
626
  }
426
627
  return `\n${lines.join('\n')}\n`;
427
628
  }
629
+ /** Add a generated subchart factory entry to the umbrella source array. */
428
630
  function addSubchartToArray(content, chartName, camelCaseName) {
429
631
  const subchartsRegex = /(const SUBCHARTS[\s\S]*?=\s*\[)([\s\S]*?)(\];)/;
430
632
  const match = content.match(subchartsRegex);
@@ -439,6 +641,7 @@ function addSubchartToArray(content, chartName, camelCaseName) {
439
641
  const newBody = buildSubchartsContent(body, subchartEntry);
440
642
  return content.replace(subchartsRegex, `${prefix}${newBody}${suffix}`);
441
643
  }
644
+ /** Merge CLI flags while preserving every repeated `--set` override. */
442
645
  function mergeCliFlags(base, override) {
443
646
  const merged = { ...(base || {}) };
444
647
  if (!override) {
@@ -459,66 +662,99 @@ function mergeCliFlags(base, override) {
459
662
  }
460
663
  return merged;
461
664
  }
665
+ /** Persist a new subchart import and factory entry into `umbrella.ts`. */
462
666
  function updateUmbrellaTs(subchartPath, chartName) {
463
667
  const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
464
668
  const content = processUmbrellaFile(umbrellaFile, subchartPath, chartName);
465
669
  fs.writeFileSync(umbrellaFile, content);
466
670
  }
671
+ /** Transform umbrella source text to reference a newly generated subchart. */
467
672
  function processUmbrellaFile(umbrellaFile, subchartPath, chartName) {
673
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
468
674
  let content = fs.readFileSync(umbrellaFile, 'utf8');
469
675
  const importData = createImportData(subchartPath, chartName);
470
676
  content = addImportStatement(content, importData.importStatement);
471
677
  content = addSubchartToArray(content, chartName, importData.camelCaseName);
472
678
  return content;
473
679
  }
680
+ /** Build the import path, identifier, and statement for a generated subchart. */
474
681
  function createImportData(subchartPath, chartName) {
475
682
  const importPath = `./charts/${subchartPath}/chart`;
476
683
  const camelCaseName = toCamelCase(chartName);
477
684
  const importStatement = `import ${camelCaseName} from '${importPath}';`;
478
685
  return { importPath, camelCaseName, importStatement };
479
686
  }
480
- async function cmdUmbrellaAdd(subchartPath, silent = false) {
687
+ /**
688
+ * Adds a subchart to an existing umbrella chart.
689
+ * Creates a new subchart and updates the umbrella configuration.
690
+ *
691
+ * @param subchartPath - The path/name for the new subchart
692
+ * @param silent - Whether to suppress output messages
693
+ * @param dryRun - When true, validate and report the operation without writing files
694
+ * @since 2.8.4
695
+ */
696
+ async function cmdUmbrellaAdd(subchartPath, silent = false, dryRun = false) {
481
697
  const MISSING_SUBCHART_MSG = 'Missing subchart name or path';
482
698
  if (!subchartPath)
483
699
  usageAndExit(MISSING_SUBCHART_MSG);
484
- const validSubchartPath = subchartPath;
700
+ const validSubchartPath = subchartPath; // Now guaranteed to be defined
485
701
  const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
486
702
  if (!fs.existsSync(configFile)) {
487
703
  console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
488
704
  process.exit(1);
489
705
  }
490
706
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
707
+ // Extract subchart name from path (last segment)
491
708
  const subchartName = path.basename(validSubchartPath);
709
+ // Create full subchart directory path
492
710
  const chartsRoot = path.join(process.cwd(), 'charts');
493
711
  const targetSubchartPath = path.join(chartsRoot, validSubchartPath);
494
712
  let subchartDir;
495
713
  try {
714
+ // @since 2.11.1 Ensure requested subchart path stays inside charts directory
496
715
  subchartDir = SecurityUtils.validatePath(targetSubchartPath, chartsRoot);
497
716
  }
498
717
  catch (error) {
499
718
  console.error(SecurityUtils.sanitizeLogMessage(error.message));
500
719
  process.exit(1);
501
720
  }
502
- fs.mkdirSync(subchartDir, { recursive: true });
503
721
  const relativeSubchartPath = path.relative(chartsRoot, subchartDir) || subchartName;
504
722
  const normalizedSubchartPath = relativeSubchartPath.split(path.sep).join('/');
723
+ if (dryRun) {
724
+ log(`Dry run: would add subchart '${SecurityUtils.sanitizeLogMessage(subchartName)}' at path '${SecurityUtils.sanitizeLogMessage(normalizedSubchartPath)}'`, silent);
725
+ return;
726
+ }
727
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
728
+ fs.mkdirSync(subchartDir, { recursive: true });
505
729
  const chartFile = SecurityUtils.validatePath(path.join(subchartDir, 'chart.ts'), chartsRoot);
506
730
  const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
507
731
  const subchartContent = generateFlexibleSubchartTemplate(subchartName);
732
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
508
733
  fs.writeFileSync(chartFile, subchartContent);
734
+ // Update config
509
735
  config.subcharts.push({
510
736
  name: subchartName,
511
737
  version: getVersion(),
512
738
  path: `./charts/${normalizedSubchartPath}/chart.ts`,
513
739
  });
514
740
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
741
+ // Update umbrella.ts file automatically with the path
515
742
  updateUmbrellaTs(normalizedSubchartPath, subchartName);
516
743
  log(`Subchart ${subchartName} added to umbrella at path '${normalizedSubchartPath}'`, silent);
517
744
  }
745
+ /**
746
+ * Synthesizes an umbrella chart to generate Helm charts for all subcharts.
747
+ * Executes the umbrella.ts file to generate charts for all configured subcharts.
748
+ *
749
+ * @param outDir - Optional output directory for generated charts
750
+ * @param flags - CLI flags for controlling synthesis behavior
751
+ * @since 2.8.4
752
+ */
518
753
  async function cmdUmbrellaSynth(outDir, flags) {
519
754
  const cwd = process.cwd();
520
755
  const umbrellaFile = SecurityUtils.validatePath(path.join(cwd, UMBRELLA_FILE_NAME), cwd);
521
756
  const defaultOutDir = SecurityUtils.validatePath(path.join(cwd, 'dist'), cwd);
757
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
522
758
  if (!fs.existsSync(umbrellaFile)) {
523
759
  console.error('umbrella.ts not found. Run `tl umbrella init` first.');
524
760
  process.exit(1);
@@ -529,9 +765,14 @@ async function cmdUmbrellaSynth(outDir, flags) {
529
765
  }
530
766
  await executeTypeScriptUmbrella(umbrellaFile, outDir ?? defaultOutDir, synthMode, flags);
531
767
  }
768
+ /** Execute an umbrella TypeScript entry point through the project-local tsx runtime. */
532
769
  async function executeTypeScriptUmbrella(resolvedPath, outDir, mode, flags) {
533
770
  const umbrellaBase = path.dirname(resolvedPath);
534
771
  const validatedOutDir = resolveOutputDirectory(outDir, umbrellaBase, flags?.silent);
772
+ if (flags?.dryRun) {
773
+ log(`Dry run: would synthesize umbrella chart ${SecurityUtils.sanitizeLogMessage(resolvedPath)} to ${SecurityUtils.sanitizeLogMessage(validatedOutDir)} in ${mode} mode`, flags.silent);
774
+ return;
775
+ }
535
776
  const wrapperScript = `
536
777
  import { pathToFileURL } from 'url';
537
778
 
@@ -552,6 +793,7 @@ console.log('Umbrella chart written to ' + output);
552
793
  const cwd = process.cwd();
553
794
  const wrapperFile = SecurityUtils.validatePath(path.join(cwd, '.timonel-umbrella-wrapper.mjs'), cwd);
554
795
  try {
796
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
555
797
  fs.writeFileSync(wrapperFile, wrapperScript);
556
798
  const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
557
799
  stdio: flags?.silent ? 'pipe' : 'inherit',
@@ -565,11 +807,21 @@ console.log('Umbrella chart written to ' + output);
565
807
  }
566
808
  }
567
809
  finally {
810
+ // Cleanup wrapper file
811
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
568
812
  if (fs.existsSync(wrapperFile)) {
813
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path validated by SecurityUtils
569
814
  fs.unlinkSync(wrapperFile);
570
815
  }
571
816
  }
572
817
  }
818
+ // Main CLI function
819
+ /**
820
+ * Parse command line flags with improved help handling
821
+ * @param args - Command line arguments
822
+ * @returns Parsed flags object
823
+ * @since 2.9.2
824
+ */
573
825
  function parseFlags(args) {
574
826
  const flags = {};
575
827
  const positionalArguments = [];
@@ -649,10 +901,11 @@ function parseFlags(args) {
649
901
  args.push(...positionalArguments);
650
902
  return flags;
651
903
  }
904
+ /** Dispatch a parsed top-level CLI command to its implementation. */
652
905
  async function executeCommand(command, args, flags) {
653
906
  switch (command) {
654
907
  case 'init':
655
- await cmdInit(args[0], flags.silent);
908
+ await cmdInit(args[0], flags.silent, flags.dryRun);
656
909
  break;
657
910
  case 'synth':
658
911
  await cmdSynth(args[0], flags, args[1]);
@@ -677,9 +930,15 @@ async function executeCommand(command, args, flags) {
677
930
  usageAndExit(`Unknown command: ${command}`, flags.silent);
678
931
  }
679
932
  }
933
+ /**
934
+ * Enhanced main function with better error handling
935
+ * @since 2.9.2
936
+ */
680
937
  async function main() {
681
938
  const args = process.argv.slice(2);
939
+ // Check for silent flag early
682
940
  const isSilent = args.includes('--silent');
941
+ // Handle help flags first, before extracting command
683
942
  if (args.includes('--help') || args.includes('-h')) {
684
943
  usageAndExit(undefined, isSilent);
685
944
  return;
@@ -688,6 +947,7 @@ async function main() {
688
947
  const flags = parseFlags(args);
689
948
  await executeCommand(command, args, flags);
690
949
  }
950
+ // Run CLI with enhanced error handling
691
951
  main().catch((error) => {
692
952
  const flags = parseFlags(process.argv.slice(2));
693
953
  if (!flags.silent) {