timonel 2.8.3 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,31 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync, execSync } from 'child_process';
3
- import { mkdirSync, existsSync, writeFileSync, rmSync, unlinkSync } from 'fs';
4
- import { join, dirname, isAbsolute, resolve } from 'path';
2
+ import { spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
6
  import { SecurityUtils } from './lib/security.js';
9
7
  const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const HELM_INSTALL_URL = 'https://helm.sh/docs/intro/install/';
12
- const HELM_ENV_VAR_MSG = 'Or set HELM_BIN environment variable to helm binary path.';
8
+ const __dirname = path.dirname(__filename);
9
+ const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
10
+ const UMBRELLA_FILE_NAME = 'umbrella.ts';
11
+ function log(msg, silent = false) {
12
+ if (!silent) {
13
+ console.log(msg);
14
+ }
15
+ }
13
16
  function usageAndExit(msg) {
14
- if (msg)
15
- console.error(SecurityUtils.sanitizeLogMessage(msg));
17
+ if (msg) {
18
+ console.error(`Error: ${msg}`);
19
+ }
16
20
  console.log([
17
- 'timonel (tl) - programmatic Helm chart generator',
21
+ 'Usage: tl <command> [options]',
18
22
  '',
19
- 'Usage:',
20
- ' tl init <chart-name> Scaffold example at charts/<name>/chart.ts',
21
- ' tl synth <projectDir> [outDir] Run charts/<...>/chart.ts and write chart',
22
- ' tl validate <projectDir> Validate chart.ts without generating files',
23
- ' tl diff <projectDir> <chartDir> Compare generated chart with existing',
24
- ' tl deploy <projectDir> <release> Synth and deploy with helm install/upgrade',
25
- ' tl package <chartDir> [outDir] Run `helm package` into outDir (requires Helm)',
23
+ 'Commands:',
24
+ ' tl init <chart-name> Create new chart',
25
+ ' tl synth [outDir] Generate Helm chart',
26
+ ' tl validate Validate chart',
27
+ ' tl deploy <release> [namespace] Deploy chart',
28
+ ' tl templates List available templates',
26
29
  '',
27
30
  'Umbrella Charts:',
28
- ' tl umbrella init <name> Create umbrella chart structure',
31
+ ' tl umbrella init <n> Create umbrella chart structure',
29
32
  ' tl umbrella add <subchart> Add subchart to umbrella',
30
33
  ' tl umbrella synth [outDir] Generate umbrella chart',
31
34
  '',
@@ -39,12 +42,12 @@ function usageAndExit(msg) {
39
42
  '',
40
43
  'Examples:',
41
44
  ' tl init my-app',
42
- ' tl synth charts/my-app charts/my-app/dist',
43
- ' tl validate charts/my-app',
44
- ' tl deploy charts/my-app my-release --env prod',
45
- ' tl synth charts/my-app --dry-run --silent',
46
- ' tl synth charts/my-app --set replicas=5 --set image.tag=v2.0.0',
47
- ' tl deploy charts/my-app my-release --set service.port=8080',
45
+ ' tl synth my-app my-app/dist',
46
+ ' tl validate my-app',
47
+ ' tl deploy my-app my-release --env prod',
48
+ ' tl synth my-app --dry-run --silent',
49
+ ' tl synth my-app --set replicas=5 --set image.tag=v2.0.0',
50
+ ' tl deploy my-app my-release --set service.port=8080',
48
51
  ].join('\n'));
49
52
  process.exit(msg ? 1 : 0);
50
53
  }
@@ -53,100 +56,50 @@ async function cmdInit(name, silent = false) {
53
56
  usageAndExit('Missing <chart-name>');
54
57
  const validName = name;
55
58
  if (!SecurityUtils.isValidChartName(validName)) {
56
- usageAndExit('Invalid chart name: must be RFC 1123 compliant (lowercase alphanumeric with hyphens)');
59
+ usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
57
60
  }
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)) {
63
- console.error(`File already exists: ${file}`);
64
- process.exit(1);
65
- }
66
- writeFileSync(file, exampleChartTs(validName));
67
- log(SecurityUtils.sanitizeLogMessage(`Scaffold created at ${file}`), silent);
61
+ const base = path.join(process.cwd(), validName);
62
+ const chartFile = path.join(base, 'chart.ts');
63
+ fs.mkdirSync(base, { recursive: true });
64
+ const { generateBasicChart } = await import('./lib/templates/basic-chart.js');
65
+ fs.writeFileSync(chartFile, generateBasicChart(validName));
66
+ log(`Chart created at ${base}`, silent);
67
+ log(`Generated chart.ts file`, silent);
68
+ log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
68
69
  }
69
- async function cmdValidate(projectDir, silent = false) {
70
- if (!projectDir)
71
- usageAndExit('Missing <projectDir>');
72
- const proj = projectDir;
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)) {
78
- console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
70
+ async function cmdSynth(chartDirOrOutDir, flags) {
71
+ let chartDir = process.cwd();
72
+ let outDir;
73
+ if (chartDirOrOutDir && fs.existsSync(path.join(chartDirOrOutDir, 'chart.ts'))) {
74
+ chartDir = path.resolve(chartDirOrOutDir);
75
+ }
76
+ else {
77
+ outDir = chartDirOrOutDir;
78
+ }
79
+ const chartFile = path.join(chartDir, 'chart.ts');
80
+ const defaultOutDir = path.join(chartDir, 'dist');
81
+ if (!fs.existsSync(chartFile)) {
82
+ console.error('chart.ts not found. Run `tl init` first.');
79
83
  process.exit(1);
80
84
  }
81
- const helm = process.env['HELM_BIN'] || 'helm';
82
- try {
83
- execSync(`${helm} version --short`, { stdio: 'ignore' });
84
- }
85
- catch {
86
- console.error('✗ Helm not found. Install Helm to enable chart validation.');
87
- console.error(` Install: ${HELM_INSTALL_URL}`);
88
- console.error(` ${HELM_ENV_VAR_MSG}`);
89
- process.exit(1);
90
- }
91
- const tempDir = join(process.cwd(), '.timonel-validate');
92
- try {
93
- await cmdSynth(projectDir, tempDir, { dryRun: false, silent: true, set: {} });
94
- const lintResult = spawnSync(helm, ['lint', tempDir], {
95
- stdio: silent ? 'pipe' : 'inherit',
96
- encoding: 'utf8',
97
- });
98
- if (lintResult.status !== 0) {
99
- if (silent && lintResult.stderr) {
100
- console.error(SecurityUtils.sanitizeLogMessage(lintResult.stderr));
101
- }
102
- console.error('✗ Chart validation failed: Helm lint errors found');
103
- process.exit(1);
104
- }
105
- log('✓ Chart validation passed', silent);
106
- }
107
- catch (error) {
108
- const errorMsg = error instanceof Error ? error.message : String(error);
109
- console.error('✗ Chart validation failed:', SecurityUtils.sanitizeLogMessage(errorMsg));
110
- process.exit(1);
111
- }
112
- finally {
113
- if (existsSync(tempDir)) {
114
- rmSync(tempDir, { recursive: true, force: true });
115
- }
116
- }
117
- }
118
- async function executeTypeScriptChart(resolvedPath, outDir, flags) {
85
+ const resolvedOutDir = outDir ? path.resolve(outDir) : defaultOutDir;
86
+ const originalContent = fs.readFileSync(chartFile, 'utf8');
87
+ const modifiedContent = originalContent.replace(/chart\.writeHelmChart\(['"][^'"]*['"]\)/, `chart.writeHelmChart('${resolvedOutDir}')`);
88
+ const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
89
+ fs.writeFileSync(tempChartFile, modifiedContent);
119
90
  const wrapperScript = `
120
91
  import { pathToFileURL } from 'url';
121
92
 
122
- const mod = await import(pathToFileURL('${resolvedPath}').href);
123
- const runner = mod.default || mod.run || mod.synth;
124
- if (typeof runner !== 'function') {
125
- console.error('chart.ts must export a default/run/synth function');
126
- process.exit(1);
127
- }
128
-
129
- ${flags?.set && Object.keys(flags.set).length > 0
130
- ? `
131
- // Apply --set overrides if provided
132
- if (mod.rutter && typeof mod.rutter.setValues === 'function') {
133
- mod.rutter.setValues(${JSON.stringify(flags.set)});
134
- }
135
- `
136
- : ''}
137
-
138
- const outDir = '${outDir}';
139
- const fs = await import('fs');
140
- fs.mkdirSync(outDir, { recursive: true });
141
- await Promise.resolve(runner(outDir));
142
- console.log('Chart written to ' + outDir);
93
+ // Import the modified chart file which should execute the synthesis directly
94
+ await import(pathToFileURL('${tempChartFile}').href);
143
95
  `;
144
- const wrapperFile = join(process.cwd(), '.timonel-wrapper.mjs');
96
+ const wrapperFile = path.join(chartDir, '.timonel-wrapper.mjs');
145
97
  try {
146
- writeFileSync(wrapperFile, wrapperScript);
147
- const result = spawnSync('npx', ['tsx', wrapperFile], {
98
+ fs.writeFileSync(wrapperFile, wrapperScript);
99
+ const result = spawnSync('npx', ['tsx', wrapperFile].filter(Boolean), {
148
100
  stdio: flags?.silent ? 'pipe' : 'inherit',
149
101
  encoding: 'utf8',
102
+ cwd: chartDir,
150
103
  });
151
104
  if (result.status !== 0) {
152
105
  if (flags?.silent && result.stderr) {
@@ -156,118 +109,77 @@ console.log('Chart written to ' + outDir);
156
109
  }
157
110
  }
158
111
  finally {
159
- if (existsSync(wrapperFile)) {
160
- unlinkSync(wrapperFile);
161
- }
162
- }
163
- }
164
- async function cmdSynth(projectDir, out, flags) {
165
- if (!projectDir)
166
- usageAndExit('Missing <projectDir>');
167
- const proj = projectDir;
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)) {
173
- console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
174
- process.exit(1);
175
- }
176
- if (!SecurityUtils.isValidTypeScriptFile(chartTs)) {
177
- console.error('Security: Only TypeScript files (.ts) are allowed');
178
- process.exit(1);
179
- }
180
- const resolvedPath = resolve(chartTs);
181
- const outDir = out || join(dirname(chartTs), 'dist');
182
- if (flags?.dryRun) {
183
- log(`[DRY RUN] Would write chart to ${outDir}`, flags.silent);
184
- return;
185
- }
186
- try {
187
- await executeTypeScriptChart(resolvedPath, outDir, flags);
188
- }
189
- catch (error) {
190
- const errorMsg = error instanceof Error ? error.message : String(error);
191
- console.error(SecurityUtils.sanitizeLogMessage(`Failed to execute chart: ${errorMsg}`));
192
- process.exit(1);
193
- }
194
- }
195
- async function cmdDiff(projectDir, chartDir, silent = false) {
196
- if (!projectDir || !chartDir)
197
- usageAndExit('Missing <projectDir> or <chartDir>');
198
- const validProjectDir = projectDir;
199
- const validChartDir = chartDir;
200
- const tempDir = join(process.cwd(), '.timonel-temp');
201
- try {
202
- await cmdSynth(validProjectDir, tempDir);
203
- const diffCmd = process.platform === 'win32' ? 'fc' : 'diff';
204
- const args = process.platform === 'win32'
205
- ? ['/N', validChartDir, tempDir]
206
- : ['-r', validChartDir, tempDir];
207
- log(`> ${diffCmd} ${args.join(' ')}`, silent);
208
- const res = spawnSync(diffCmd, args, { stdio: 'inherit' });
209
- if (res.status === 0) {
210
- log('✓ No differences found', silent);
112
+ if (fs.existsSync(wrapperFile)) {
113
+ fs.unlinkSync(wrapperFile);
211
114
  }
212
- else if (res.status === 1) {
213
- log('⚠ Differences found', silent);
214
- process.exit(1);
115
+ if (fs.existsSync(tempChartFile)) {
116
+ fs.unlinkSync(tempChartFile);
215
117
  }
216
118
  }
217
- finally {
218
- if (existsSync(tempDir)) {
219
- rmSync(tempDir, { recursive: true, force: true });
119
+ }
120
+ async function cmdValidate(flags) {
121
+ const result = spawnSync('helm', ['lint', '.'], {
122
+ stdio: flags?.silent ? 'pipe' : 'inherit',
123
+ encoding: 'utf8',
124
+ });
125
+ if (result.status !== 0) {
126
+ if (flags?.silent && result.stderr) {
127
+ console.error(SecurityUtils.sanitizeLogMessage(result.stderr));
220
128
  }
129
+ process.exit(result.status ?? 1);
221
130
  }
222
131
  }
223
- async function cmdDeploy(projectDir, releaseName, flags) {
224
- if (!projectDir || !releaseName)
225
- usageAndExit('Missing <projectDir> or <release>');
226
- const validProjectDir = projectDir;
227
- const validReleaseName = releaseName;
228
- const helm = process.env['HELM_BIN'] || 'helm';
229
- try {
230
- execSync(`${helm} version --short`, { stdio: 'ignore' });
231
- }
232
- catch {
233
- console.error('✗ Helm not found. Install Helm to enable deployment.');
234
- console.error(` Install: ${HELM_INSTALL_URL}`);
235
- console.error(` ${HELM_ENV_VAR_MSG}`);
236
- process.exit(1);
237
- }
238
- const tempDir = join(process.cwd(), '.timonel-deploy');
239
- try {
240
- await cmdSynth(validProjectDir, tempDir, flags);
241
- const checkCmd = `${helm} status ${releaseName}`;
242
- const releaseExists = spawnSync('sh', ['-c', checkCmd], { stdio: 'ignore' }).status === 0;
243
- const action = releaseExists ? 'upgrade' : 'install';
244
- const args = [action, validReleaseName, tempDir];
245
- if (flags?.env) {
246
- const valuesFile = join(tempDir, `values-${flags.env}.yaml`);
247
- if (existsSync(valuesFile)) {
248
- args.push('-f', valuesFile);
249
- }
250
- }
251
- if (flags?.dryRun) {
252
- args.push('--dry-run');
253
- }
254
- log(`> ${helm} ${args.join(' ')}`, flags?.silent);
255
- const res = spawnSync(helm, args, { stdio: 'inherit' });
256
- if (res.status !== 0) {
257
- process.exit(res.status ?? 1);
132
+ async function cmdDeploy(release, namespace, flags) {
133
+ if (!release)
134
+ usageAndExit('Missing <release>');
135
+ const args = ['upgrade', '--install', release, '.'];
136
+ if (namespace) {
137
+ args.push('--namespace', namespace);
138
+ }
139
+ const result = spawnSync('helm', args.filter((arg) => Boolean(arg)), {
140
+ stdio: flags?.silent ? 'pipe' : 'inherit',
141
+ encoding: 'utf8',
142
+ });
143
+ if (result.status !== 0) {
144
+ if (flags?.silent && result.stderr) {
145
+ console.error(SecurityUtils.sanitizeLogMessage(result.stderr));
258
146
  }
259
- log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${validReleaseName}`, flags?.silent);
147
+ process.exit(result.status ?? 1);
260
148
  }
261
- finally {
262
- if (existsSync(tempDir)) {
263
- rmSync(tempDir, { recursive: true, force: true });
264
- }
149
+ }
150
+ async function cmdTemplates(flags) {
151
+ const templates = [
152
+ {
153
+ name: 'basic-chart',
154
+ description: 'Basic chart with deployment and service',
155
+ usage: 'tl init my-app',
156
+ },
157
+ {
158
+ name: 'umbrella-chart',
159
+ description: 'Umbrella chart for managing multiple subcharts',
160
+ usage: 'tl umbrella init my-app',
161
+ },
162
+ {
163
+ name: 'subchart',
164
+ description: 'Subchart for use with umbrella charts',
165
+ usage: 'tl umbrella add my-subchart',
166
+ },
167
+ ];
168
+ if (flags?.silent) {
169
+ console.log(JSON.stringify(templates));
170
+ }
171
+ else {
172
+ console.log('Available templates:');
173
+ templates.forEach((t) => {
174
+ console.log(`\n${t.name}`);
175
+ console.log(` Description: ${t.description}`);
176
+ console.log(` Usage: ${t.usage}`);
177
+ });
265
178
  }
266
179
  }
267
180
  async function cmdUmbrella(subcommand, args, flags) {
268
- const UMBRELLA_USAGE_MSG = 'Missing umbrella subcommand (init|add|synth)';
269
181
  if (!subcommand)
270
- usageAndExit(UMBRELLA_USAGE_MSG);
182
+ usageAndExit('Missing umbrella subcommand');
271
183
  switch (subcommand) {
272
184
  case 'init':
273
185
  await cmdUmbrellaInit(args?.[0], flags?.silent);
@@ -282,8 +194,6 @@ async function cmdUmbrella(subcommand, args, flags) {
282
194
  usageAndExit(`Unknown umbrella subcommand: ${subcommand}`);
283
195
  }
284
196
  }
285
- const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
286
- const UMBRELLA_FILE_NAME = 'umbrella.ts';
287
197
  async function cmdUmbrellaInit(name, silent = false) {
288
198
  const MISSING_NAME_MSG = 'Missing umbrella chart name';
289
199
  if (!name)
@@ -293,64 +203,91 @@ async function cmdUmbrellaInit(name, silent = false) {
293
203
  const umbrellaFile = path.join(base, UMBRELLA_FILE_NAME);
294
204
  const configFile = path.join(base, UMBRELLA_CONFIG_FILE);
295
205
  fs.mkdirSync(base, { recursive: true });
296
- fs.mkdirSync(path.join(base, 'charts'), { recursive: true });
297
- fs.writeFileSync(umbrellaFile, exampleUmbrellaTs(validName));
206
+ const { generateUmbrellaChart } = await import('./lib/templates/umbrella-chart.js');
207
+ fs.writeFileSync(umbrellaFile, generateUmbrellaChart(validName));
298
208
  const config = {
299
209
  name: validName,
300
210
  version: '0.1.0',
301
- description: `${name} umbrella chart`,
211
+ description: `${validName} umbrella chart`,
302
212
  subcharts: [],
303
213
  };
304
214
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
215
+ const chartsDir = path.join(base, 'charts');
216
+ fs.mkdirSync(chartsDir, { recursive: true });
305
217
  log(`Umbrella chart structure created at ${base}`, silent);
306
218
  log(`Add subcharts with: tl umbrella add <subchart-name>`, silent);
307
219
  }
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');
220
+ function toCamelCase(str) {
221
+ return str.replace(/-([a-z])/g, (g) => g[1]?.toUpperCase() || '');
222
+ }
223
+ function findLastImportIndex(lines) {
224
+ let lastImportIndex = -1;
225
+ for (let i = 0; i < lines.length; i++) {
226
+ if (lines[i]?.trim().startsWith('import ')) {
227
+ lastImportIndex = i;
324
228
  }
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);
229
+ else if (lines[i]?.trim() && !lines[i]?.trim().startsWith('import ')) {
230
+ break;
329
231
  }
330
232
  }
233
+ return lastImportIndex;
234
+ }
235
+ function addImportStatement(content, importStatement) {
236
+ if (content.includes(importStatement)) {
237
+ return content;
238
+ }
239
+ const lines = content.split('\n');
240
+ const lastImportIndex = findLastImportIndex(lines);
241
+ if (lastImportIndex >= 0) {
242
+ lines.splice(lastImportIndex + 1, 0, importStatement);
243
+ }
244
+ else {
245
+ lines.splice(0, 0, importStatement);
246
+ }
247
+ return lines.join('\n');
248
+ }
249
+ function buildSubchartsContent(subchartsContent, subchartEntry) {
250
+ const hasExistingEntries = /\{\s*name:\s*['"]/.test(subchartsContent || '');
251
+ if (!hasExistingEntries) {
252
+ return `\n // Add your subcharts here:\n ${subchartEntry},\n `;
253
+ }
254
+ const trimmedContent = subchartsContent?.trimEnd() ?? '';
255
+ const needsComma = !trimmedContent.endsWith(',');
256
+ const comma = needsComma ? ',' : '';
257
+ return trimmedContent + `${comma}\n ${subchartEntry}`;
258
+ }
259
+ function addSubchartToArray(content, chartName, camelCaseName) {
331
260
  const subchartsRegex = /subcharts:\s*\[([\s\S]*?)\]/;
332
261
  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
- }
262
+ if (!match) {
263
+ return content;
351
264
  }
265
+ const subchartsContent = match[1];
266
+ if (subchartsContent?.includes(`name: '${chartName}'`)) {
267
+ return content;
268
+ }
269
+ const subchartEntry = `{ name: '${chartName}', chart: ${camelCaseName} }`;
270
+ const newSubchartsContent = buildSubchartsContent(subchartsContent, subchartEntry);
271
+ return content.replace(subchartsRegex, `subcharts: [${newSubchartsContent}\n ]`);
272
+ }
273
+ function updateUmbrellaTs(subchartPath, chartName) {
274
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
275
+ const content = processUmbrellaFile(umbrellaFile, subchartPath, chartName);
352
276
  fs.writeFileSync(umbrellaFile, content);
353
277
  }
278
+ function processUmbrellaFile(umbrellaFile, subchartPath, chartName) {
279
+ let content = fs.readFileSync(umbrellaFile, 'utf8');
280
+ const importData = createImportData(subchartPath, chartName);
281
+ content = addImportStatement(content, importData.importStatement);
282
+ content = addSubchartToArray(content, chartName, importData.camelCaseName);
283
+ return content;
284
+ }
285
+ function createImportData(subchartPath, chartName) {
286
+ const importPath = `./charts/${subchartPath}/chart`;
287
+ const camelCaseName = toCamelCase(chartName);
288
+ const importStatement = `import ${camelCaseName} from '${importPath}';`;
289
+ return { importPath, camelCaseName, importStatement };
290
+ }
354
291
  async function cmdUmbrellaAdd(subchartPath, silent = false) {
355
292
  const MISSING_SUBCHART_MSG = 'Missing subchart name or path';
356
293
  if (!subchartPath)
@@ -366,7 +303,9 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
366
303
  const subchartDir = path.join(process.cwd(), 'charts', validSubchartPath);
367
304
  fs.mkdirSync(subchartDir, { recursive: true });
368
305
  const chartFile = path.join(subchartDir, 'chart.ts');
369
- fs.writeFileSync(chartFile, exampleSubchartTs(subchartName));
306
+ const { generateSubchartTemplate } = await import('./lib/templates/subchart.js');
307
+ const subchartContent = generateSubchartTemplate(subchartName);
308
+ fs.writeFileSync(chartFile, subchartContent);
370
309
  config.subcharts.push({
371
310
  name: subchartName,
372
311
  version: '0.1.0',
@@ -376,6 +315,16 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
376
315
  updateUmbrellaTs(validSubchartPath, subchartName);
377
316
  log(`Subchart ${subchartName} added to umbrella at path '${validSubchartPath}'`, silent);
378
317
  }
318
+ async function cmdUmbrellaSynth(outDir, flags) {
319
+ const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
320
+ const defaultOutDir = path.join(process.cwd(), 'dist');
321
+ if (!fs.existsSync(umbrellaFile)) {
322
+ console.error('umbrella.ts not found. Run `tl umbrella init` first.');
323
+ process.exit(1);
324
+ }
325
+ const resolvedOutDir = outDir ? path.resolve(outDir) : defaultOutDir;
326
+ await executeTypeScriptUmbrella(umbrellaFile, resolvedOutDir, flags);
327
+ }
379
328
  async function executeTypeScriptUmbrella(resolvedPath, outDir, flags) {
380
329
  const wrapperScript = `
381
330
  import { pathToFileURL } from 'url';
@@ -396,7 +345,7 @@ console.log('Umbrella chart written to ' + output);
396
345
  const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
397
346
  try {
398
347
  fs.writeFileSync(wrapperFile, wrapperScript);
399
- const result = spawnSync('npx', ['tsx', wrapperFile], {
348
+ const result = spawnSync('npx', ['tsx', wrapperFile].filter(Boolean), {
400
349
  stdio: flags?.silent ? 'pipe' : 'inherit',
401
350
  encoding: 'utf8',
402
351
  });
@@ -413,453 +362,80 @@ console.log('Umbrella chart written to ' + output);
413
362
  }
414
363
  }
415
364
  }
416
- async function cmdUmbrellaSynth(outDir, flags) {
417
- const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
418
- if (!fs.existsSync(configFile)) {
419
- console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
420
- process.exit(1);
421
- }
422
- const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
423
- if (!fs.existsSync(umbrellaFile)) {
424
- console.error('umbrella.ts not found.');
425
- process.exit(1);
426
- }
427
- if (!SecurityUtils.isValidTypeScriptFile(umbrellaFile)) {
428
- console.error('Security: Only TypeScript files (.ts) are allowed');
429
- process.exit(1);
430
- }
431
- const validatedPath = SecurityUtils.validatePath(umbrellaFile, process.cwd());
432
- const resolvedPath = path.resolve(validatedPath);
433
- const output = outDir || path.join(process.cwd(), 'dist');
434
- if (flags?.dryRun) {
435
- log(`[DRY RUN] Would write umbrella chart to ${output}`, flags.silent);
436
- return;
437
- }
438
- try {
439
- await executeTypeScriptUmbrella(resolvedPath, output, flags);
440
- }
441
- catch (error) {
442
- const errorMsg = error instanceof Error ? error.message : String(error);
443
- console.error(SecurityUtils.sanitizeLogMessage(`Failed to execute umbrella: ${errorMsg}`));
444
- process.exit(1);
445
- }
446
- }
447
- async function cmdPackage(chartDir, out, silent = false) {
448
- if (!chartDir)
449
- usageAndExit('Missing <chartDir>');
450
- const validChartDir = chartDir;
451
- const src = path.isAbsolute(validChartDir)
452
- ? validChartDir
453
- : path.join(process.cwd(), validChartDir);
454
- const chartYaml = path.join(src, 'Chart.yaml');
455
- if (!fs.existsSync(chartYaml)) {
456
- console.error(`Chart.yaml not found in ${src}`);
457
- process.exit(1);
458
- }
459
- const outDir = out ? (path.isAbsolute(out) ? out : path.join(process.cwd(), out)) : src;
460
- fs.mkdirSync(outDir, { recursive: true });
461
- const helm = process.env['HELM_BIN'] || 'helm';
462
- const args = ['package', src, '-d', outDir];
463
- log(`> ${helm} ${args.join(' ')}`, silent);
464
- const res = spawnSync(helm, args, { stdio: 'inherit' });
465
- if (res.error) {
466
- console.error(`✗ Failed to execute Helm: ${res.error.message}`);
467
- console.error(` Install: ${HELM_INSTALL_URL}`);
468
- console.error(` ${HELM_ENV_VAR_MSG}`);
469
- process.exit(1);
470
- }
471
- if (res.status !== 0) {
472
- process.exit(res.status ?? 1);
473
- }
474
- }
475
- function exampleUmbrellaTs(name) {
476
- return `import { createUmbrella } from 'timonel';
477
- import { Rutter } from 'timonel';
478
-
479
- // Import subcharts (these will be created with 'tl umbrella add')
480
- // import { rutter as fluentBit } from './charts/fluent-bit/chart';
481
- // import { rutter as openObserve } from './charts/openobserve/chart';
482
-
483
- // Create umbrella chart
484
- const umbrella = createUmbrella({
485
- meta: {
486
- name: '${name}',
487
- version: '0.1.0',
488
- description: '${name} umbrella chart',
489
- appVersion: '1.0.0',
490
- },
491
- subcharts: [
492
- // Add your subcharts here:
493
- // { name: 'fluent-bit', rutter: fluentBit },
494
- // { name: 'openobserve', rutter: openObserve },
495
- ],
496
- defaultValues: {
497
- global: {
498
- // Global values shared across subcharts
499
- },
500
- },
501
- envValues: {
502
- dev: {
503
- // Development environment overrides
504
- },
505
- prod: {
506
- // Production environment overrides
507
- },
508
- },
509
- });
510
-
511
- export default function run(outDir: string) {
512
- umbrella.write(outDir);
513
- }
514
- `;
515
- }
516
- function exampleSubchartTs(name) {
517
- return `import { Rutter } from 'timonel';
518
- import { valuesRef, helm } from 'timonel/lib/helm';
519
-
520
- // Create a new Rutter instance
521
- const rutter = new Rutter({
522
- meta: {
523
- name: '${name}',
524
- version: '0.1.0',
525
- description: '${name} subchart',
526
- appVersion: '1.0.0',
527
- },
528
- defaultValues: {
529
- image: { repository: 'nginx', tag: '1.27' },
530
- replicas: 1,
531
- service: { port: 80 },
532
- },
533
- });
534
-
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
- ]
365
+ function parseFlags(args) {
366
+ const flags = {};
367
+ while (args.length > 0 && args[0]?.startsWith('-')) {
368
+ const flag = args.shift();
369
+ switch (flag) {
370
+ case '--dry-run':
371
+ flags.dryRun = true;
372
+ break;
373
+ case '--silent':
374
+ flags.silent = true;
375
+ break;
376
+ case '--env': {
377
+ const envValue = args.shift();
378
+ if (envValue)
379
+ flags.env = envValue;
380
+ break;
570
381
  }
571
- ]
572
- }
573
- }
574
- }
575
- }, '${name}-deployment');
576
-
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');
602
-
603
- export default function run(outDir: string) {
604
- rutter.write(outDir);
605
- }
606
-
607
- // Export rutter for umbrella chart
608
- export { rutter };
609
- `;
610
- }
611
- function exampleChartTs(name) {
612
- return `import { Rutter } from 'timonel';
613
- import { valuesRef, helm } from 'timonel/lib/helm';
614
-
615
- // Define chart metadata and default/env values
616
- const rutter = new Rutter({
617
- meta: {
618
- name: '${name}',
619
- version: '0.1.0',
620
- description: 'Example Helm chart generated with timonel + cdk8s',
621
- appVersion: '1.0.0',
622
- },
623
- defaultValues: {
624
- image: { repository: 'nginx', tag: '1.27' },
625
- replicas: 1,
626
- service: { port: 80 },
627
- ingress: { enabled: false, host: 'example.com', className: 'nginx' },
628
- namespace: { create: false, name: '${name}' },
629
- },
630
- envValues: {
631
- dev: { replicas: 1 },
632
- prod: { replicas: 3 },
633
- },
634
- });
635
-
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
- }
645
- },
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');
687
-
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');
713
-
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');
763
-
764
- export default function run(outDir: string) {
765
- rutter.write(outDir);
766
- }
767
- `;
768
- }
769
- function parseArgs() {
770
- const argv = process.argv.slice(2);
771
- const flags = { dryRun: false, silent: false, set: {} };
772
- const args = [];
773
- let cmd = '';
774
- for (let i = 0; i < argv.length; i++) {
775
- const arg = argv[i];
776
- if (arg === '--dry-run') {
777
- flags.dryRun = true;
778
- }
779
- else if (arg === '--silent') {
780
- flags.silent = true;
781
- }
782
- else if (arg === '--env' && i + 1 < argv.length) {
783
- const nextArg = argv[++i];
784
- if (nextArg)
785
- flags.env = nextArg;
786
- }
787
- else if (arg === '--version-bump' && i + 1 < argv.length) {
788
- const nextArg = argv[++i];
789
- if (nextArg)
790
- flags.versionBump = nextArg;
791
- }
792
- else if (arg === '--set' && i + 1 < argv.length) {
793
- const setValue = argv[++i];
794
- if (setValue) {
795
- const [key, value] = setValue.split('=', 2);
796
- if (key && value !== undefined) {
797
- flags.set[key] = value;
382
+ case '--set': {
383
+ const setValue = args.shift();
384
+ if (setValue) {
385
+ flags.set = flags.set || [];
386
+ flags.set.push(setValue);
798
387
  }
388
+ break;
799
389
  }
390
+ case '--version':
391
+ case '-v':
392
+ console.log('Timonel v0.1.0');
393
+ process.exit(0);
394
+ break;
395
+ case '--help':
396
+ case '-h':
397
+ usageAndExit();
398
+ break;
399
+ default:
400
+ usageAndExit(`Unknown flag: ${flag}`);
800
401
  }
801
- else if (arg === '--version' || arg === '-v') {
802
- showVersion();
803
- process.exit(0);
804
- }
805
- else if (arg === '--help' || arg === '-h') {
806
- usageAndExit();
807
- }
808
- else if (arg && !arg.startsWith('--')) {
809
- if (!cmd) {
810
- cmd = arg;
811
- }
812
- else {
813
- args.push(arg);
814
- }
815
- }
816
- }
817
- return { cmd, args, flags };
818
- }
819
- function showVersion() {
820
- const packagePath = path.join(__dirname, '..', 'package.json');
821
- try {
822
- const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
823
- console.log(`timonel v${packageJson.version}`);
824
- }
825
- catch {
826
- console.log('timonel (version unknown)');
827
- }
828
- }
829
- function log(message, silent = false) {
830
- if (!silent) {
831
- console.log(message);
832
402
  }
403
+ return flags;
833
404
  }
834
- async function main() {
835
- const { cmd, args, flags } = parseArgs();
836
- switch (cmd) {
405
+ async function executeCommand(command, args, flags) {
406
+ switch (command) {
837
407
  case 'init':
838
408
  await cmdInit(args[0], flags.silent);
839
409
  break;
840
- case 'validate':
841
- await cmdValidate(args[0], flags.silent);
842
- break;
843
410
  case 'synth':
844
- await cmdSynth(args[0], args[1], flags);
411
+ await cmdSynth(args[0], flags);
845
412
  break;
846
- case 'diff':
847
- await cmdDiff(args[0], args[1], flags.silent);
413
+ case 'validate':
414
+ await cmdValidate(flags);
848
415
  break;
849
416
  case 'deploy':
850
417
  await cmdDeploy(args[0], args[1], flags);
851
418
  break;
852
- case 'package':
853
- await cmdPackage(args[0], args[1], flags.silent);
419
+ case 'templates':
420
+ await cmdTemplates(flags);
854
421
  break;
855
422
  case 'umbrella':
856
423
  await cmdUmbrella(args[0], args.slice(1), flags);
857
424
  break;
425
+ case undefined:
426
+ usageAndExit('Missing command');
427
+ break;
858
428
  default:
859
- usageAndExit();
429
+ usageAndExit(`Unknown command: ${command}`);
860
430
  }
861
431
  }
862
- main().catch((err) => {
863
- console.error(err);
432
+ async function main() {
433
+ const args = process.argv.slice(2);
434
+ const command = args.shift();
435
+ const flags = parseFlags(args);
436
+ await executeCommand(command, args, flags);
437
+ }
438
+ main().catch((error) => {
439
+ console.error('Error:', error.message);
864
440
  process.exit(1);
865
441
  });