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.
- package/CHANGELOG.md +7 -0
- package/README.md +323 -695
- package/dist/cli.js +275 -15
- package/dist/index.d.ts +5 -1
- package/dist/index.js +12 -0
- package/dist/lib/helm.d.ts +471 -0
- package/dist/lib/helm.js +483 -0
- package/dist/lib/helmChartWriter.d.ts +157 -0
- package/dist/lib/helmChartWriter.js +171 -1
- package/dist/lib/policy/configurationLoader.d.ts +132 -0
- package/dist/lib/policy/configurationLoader.js +132 -0
- package/dist/lib/policy/errorContextGenerator.d.ts +89 -0
- package/dist/lib/policy/errorContextGenerator.js +99 -2
- package/dist/lib/policy/errors.d.ts +35 -0
- package/dist/lib/policy/errors.js +36 -0
- package/dist/lib/policy/index.d.ts +9 -0
- package/dist/lib/policy/index.js +16 -0
- package/dist/lib/policy/parallelExecutor.d.ts +90 -0
- package/dist/lib/policy/parallelExecutor.js +86 -3
- package/dist/lib/policy/pluginLoader.d.ts +92 -0
- package/dist/lib/policy/pluginLoader.js +92 -1
- package/dist/lib/policy/pluginRegistry.d.ts +54 -0
- package/dist/lib/policy/pluginRegistry.js +56 -0
- package/dist/lib/policy/policyEngine.d.ts +137 -0
- package/dist/lib/policy/policyEngine.js +191 -5
- package/dist/lib/policy/resultAggregator.d.ts +46 -0
- package/dist/lib/policy/resultAggregator.js +69 -1
- package/dist/lib/policy/resultFormatter.d.ts +88 -0
- package/dist/lib/policy/resultFormatter.js +101 -0
- package/dist/lib/policy/types.d.ts +136 -0
- package/dist/lib/policy/types.js +8 -0
- package/dist/lib/policy/validationCache.d.ts +146 -0
- package/dist/lib/policy/validationCache.js +142 -6
- package/dist/lib/resources/baseResourceProvider.d.ts +45 -0
- package/dist/lib/resources/baseResourceProvider.js +48 -1
- package/dist/lib/resources/cloud/aws/awsResources.d.ts +192 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +163 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +131 -0
- package/dist/lib/resources/cloud/aws/karpenterResources.js +77 -0
- package/dist/lib/rutter.d.ts +381 -3
- package/dist/lib/rutter.js +439 -28
- package/dist/lib/security.d.ts +123 -0
- package/dist/lib/security.js +162 -4
- package/dist/lib/templates/flexible-subchart.d.ts +52 -0
- package/dist/lib/templates/flexible-subchart.js +70 -0
- package/dist/lib/templates/umbrella-chart.d.ts +27 -0
- package/dist/lib/templates/umbrella-chart.js +89 -0
- package/dist/lib/types.d.ts +26 -0
- package/dist/lib/umbrella.d.ts +23 -0
- package/dist/lib/umbrella.js +23 -0
- package/dist/lib/umbrellaRutter.d.ts +75 -0
- package/dist/lib/umbrellaRutter.js +82 -2
- package/dist/lib/utils/envVarsLoader.d.ts +49 -0
- package/dist/lib/utils/envVarsLoader.js +53 -0
- package/dist/lib/utils/helmConstructSerializer.d.ts +17 -0
- package/dist/lib/utils/helmConstructSerializer.js +22 -0
- package/dist/lib/utils/helmControlStructures.d.ts +194 -0
- package/dist/lib/utils/helmControlStructures.js +180 -0
- package/dist/lib/utils/helmHelpers/envHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/envHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/gitopsHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/gitopsHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/index.d.ts +74 -0
- package/dist/lib/utils/helmHelpers/index.js +85 -1
- package/dist/lib/utils/helmHelpers/observabilityHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/observabilityHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/types.d.ts +23 -0
- package/dist/lib/utils/helmHelpers/types.js +4 -0
- package/dist/lib/utils/helmHelpers/validationHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/validationHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers.d.ts +62 -0
- package/dist/lib/utils/helmHelpers.js +77 -0
- package/dist/lib/utils/helmYamlSerializer.d.ts +77 -0
- package/dist/lib/utils/helmYamlSerializer.js +398 -21
- package/dist/lib/utils/logger.d.ts +153 -0
- package/dist/lib/utils/logger.js +170 -2
- package/dist/lib/utils/valuesRef.d.ts +181 -50
- package/dist/lib/utils/valuesRef.js +168 -170
- package/dist/lib/validation/inputValidator.d.ts +45 -0
- package/dist/lib/validation/inputValidator.js +67 -2
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.js +3 -0
- 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 (
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|