@r8s/cli 0.2.0 → 0.3.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/catalog.d.ts.map +1 -1
- package/dist/catalog.js +13 -77
- package/dist/catalog.js.map +1 -1
- package/dist/cli.js +437 -453
- package/dist/cli.js.map +1 -1
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -4,38 +4,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const core_1 = require("@r8s/core");
|
|
7
|
+
const FLAG_TABLE = {
|
|
8
|
+
'--help': (o) => ((o.help = true), false),
|
|
9
|
+
'-h': (o) => ((o.help = true), false),
|
|
10
|
+
'--out': (o, v) => ((o.out = v), true),
|
|
11
|
+
'-o': (o, v) => ((o.out = v), true),
|
|
12
|
+
'--entry': (o, v) => ((o.entry = v), true),
|
|
13
|
+
'-e': (o, v) => ((o.entry = v), true),
|
|
14
|
+
'--template': (o, v) => ((o.template = v), true),
|
|
15
|
+
'-t': (o, v) => ((o.template = v), true),
|
|
16
|
+
'--strategy': (o, v) => ((o.strategy = v), true),
|
|
17
|
+
'--operators': (o, v) => ((o.operators = v), true),
|
|
18
|
+
'--include-operators': (o) => ((o.includeOperators = true), false),
|
|
19
|
+
'--operators-only': (o) => ((o.operatorsOnly = true), false),
|
|
20
|
+
'--skip-secret-guardrails': (o) => ((o.skipSecretGuardrails = true), false),
|
|
21
|
+
};
|
|
7
22
|
function parseArgs(args) {
|
|
8
23
|
const options = {};
|
|
24
|
+
const positionals = [];
|
|
9
25
|
for (let i = 0; i < args.length; i++) {
|
|
10
26
|
const arg = args[i];
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
const handler = FLAG_TABLE[arg];
|
|
28
|
+
if (handler) {
|
|
29
|
+
const needsValue = handler(options, args[i + 1]);
|
|
30
|
+
if (needsValue)
|
|
31
|
+
i++;
|
|
13
32
|
}
|
|
14
|
-
else if (arg
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
else if (arg === '--template' || arg === '-t') {
|
|
18
|
-
options.template = args[++i];
|
|
19
|
-
}
|
|
20
|
-
else if (arg === '--operators') {
|
|
21
|
-
options.operators = args[++i];
|
|
22
|
-
}
|
|
23
|
-
else if (arg === '--strategy' || arg === '-s') {
|
|
24
|
-
options.strategy = args[++i];
|
|
25
|
-
}
|
|
26
|
-
else if (arg === '--include-operators') {
|
|
27
|
-
options.includeOperators = true;
|
|
28
|
-
}
|
|
29
|
-
else if (arg === '--operators-only') {
|
|
30
|
-
options.operatorsOnly = true;
|
|
31
|
-
}
|
|
32
|
-
else if (arg === '--skip-secret-guardrails') {
|
|
33
|
-
options.skipSecretGuardrails = true;
|
|
34
|
-
}
|
|
35
|
-
else if (arg === '--help' || arg === '-h') {
|
|
36
|
-
options.help = true;
|
|
33
|
+
else if (!arg.startsWith('-')) {
|
|
34
|
+
positionals.push(arg);
|
|
37
35
|
}
|
|
38
36
|
}
|
|
37
|
+
// Preserve command + its args in place for the command dispatch
|
|
38
|
+
args.length = 0;
|
|
39
|
+
args.push(...positionals);
|
|
39
40
|
return options;
|
|
40
41
|
}
|
|
41
42
|
function showHelp() {
|
|
@@ -80,7 +81,7 @@ Examples:
|
|
|
80
81
|
r8s init my-project
|
|
81
82
|
r8s init my-project --template fullstack
|
|
82
83
|
r8s init my-project --strategy flux-controller
|
|
83
|
-
r8s init my-project --operators cert-manager,
|
|
84
|
+
r8s init my-project --operators cert-manager,external-dns
|
|
84
85
|
r8s list
|
|
85
86
|
r8s info App
|
|
86
87
|
r8s info Database
|
|
@@ -111,29 +112,32 @@ async function findEntryFile(entryPath) {
|
|
|
111
112
|
defaults.map((d) => ` - ${d}`).join('\n') +
|
|
112
113
|
'\n\nUse --entry to specify a custom path.');
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
-
'
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
async function operatorPackages() {
|
|
116
|
+
const { operators, operatorMetadata } = await import('@r8s/crds');
|
|
117
|
+
return operatorMetadata
|
|
118
|
+
.map((meta) => {
|
|
119
|
+
const key = meta.name;
|
|
120
|
+
const slug = key.endsWith('-operator') && key !== 'paperclip-operator'
|
|
121
|
+
? key.slice(0, -'-operator'.length)
|
|
122
|
+
: key;
|
|
123
|
+
const version = operators[key]().version;
|
|
124
|
+
return { key, slug, packageName: `@r8s/operator-${slug}`, version };
|
|
125
|
+
})
|
|
126
|
+
.filter((info) => info.slug !== 'cnpg'); // cnpg flows through @r8s/recipes
|
|
127
|
+
}
|
|
126
128
|
async function initProject(projectName, template, strategy = 'github-actions', operators) {
|
|
127
129
|
const projectDir = (0, path_1.resolve)(projectName);
|
|
128
130
|
if ((0, fs_1.existsSync)(projectDir)) {
|
|
129
131
|
throw new Error(`Directory ${projectName} already exists`);
|
|
130
132
|
}
|
|
131
|
-
// Validate operators
|
|
133
|
+
// Validate operators against the generated registry; accept aliases
|
|
132
134
|
if (operators && operators.length > 0) {
|
|
133
|
-
const
|
|
135
|
+
const catalog = await operatorPackages();
|
|
136
|
+
const resolvable = new Set(catalog.flatMap((i) => [i.key, i.slug, i.packageName]));
|
|
137
|
+
const invalid = operators.filter((op) => !resolvable.has(op));
|
|
134
138
|
if (invalid.length > 0) {
|
|
135
139
|
throw new Error(`Invalid operators: ${invalid.join(', ')}. ` +
|
|
136
|
-
`Valid operators are: ${
|
|
140
|
+
`Valid operators are: ${[...resolvable].sort().join(', ')}`);
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
console.log(`Creating r8s project: ${projectName}`);
|
|
@@ -145,10 +149,15 @@ async function initProject(projectName, template, strategy = 'github-actions', o
|
|
|
145
149
|
'@r8s/core': '^0.1.0',
|
|
146
150
|
'@r8s/recipes': '^0.1.0',
|
|
147
151
|
};
|
|
148
|
-
// Add operator packages
|
|
149
|
-
if (operators) {
|
|
152
|
+
// Add operator packages — real package names + mirrored versions
|
|
153
|
+
if (operators && operators.length > 0) {
|
|
154
|
+
const catalog = await operatorPackages();
|
|
150
155
|
for (const op of operators) {
|
|
151
|
-
|
|
156
|
+
const info = catalog.find((i) => i.key === op || i.slug === op || i.packageName === op) ??
|
|
157
|
+
(() => {
|
|
158
|
+
throw new Error(`Unknown operator: ${op}`);
|
|
159
|
+
})();
|
|
160
|
+
dependencies[info.packageName] = `^${info.version}`;
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
163
|
const scripts = {};
|
|
@@ -562,423 +571,370 @@ See \`flux/README.md\` for detailed setup instructions.
|
|
|
562
571
|
`;
|
|
563
572
|
}
|
|
564
573
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
/** Render a component from the catalog with dummy values for its required props. */
|
|
575
|
+
function writeDummyTsx(comp, tag) {
|
|
576
|
+
const dummyValues = {
|
|
577
|
+
name: '"example"',
|
|
578
|
+
image: '"example/app:v1"',
|
|
579
|
+
host: '"example.com"',
|
|
580
|
+
serviceName: '"example"',
|
|
581
|
+
children: 'null',
|
|
582
|
+
selector: '{ app: "example" }',
|
|
583
|
+
};
|
|
584
|
+
const requiredProps = comp.props.filter((p) => p.required);
|
|
585
|
+
const propsStr = requiredProps
|
|
586
|
+
.map((p) => `${p.name}={${dummyValues[p.name] ?? '"dummy"'}}`)
|
|
587
|
+
.join(' ');
|
|
588
|
+
const tsx = `import { ${comp.name} } from '${comp.package}'\nexport default <${comp.name} ${propsStr} />\n`;
|
|
589
|
+
const tmpFile = (0, path_1.resolve)(`.r8s-${tag}-${Date.now()}.tsx`);
|
|
590
|
+
(0, fs_1.writeFileSync)(tmpFile, tsx, 'utf-8');
|
|
591
|
+
return tmpFile;
|
|
592
|
+
}
|
|
593
|
+
function removeQuietly(file) {
|
|
594
|
+
try {
|
|
595
|
+
require('fs').unlinkSync(file);
|
|
571
596
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const projectName = args[1] || 'r8s-app';
|
|
575
|
-
const template = options.template || 'basic';
|
|
576
|
-
const strategy = options.strategy || 'github-actions';
|
|
577
|
-
const operators = options.operators
|
|
578
|
-
?.split(',')
|
|
579
|
-
.map((op) => op.trim())
|
|
580
|
-
.filter(Boolean);
|
|
581
|
-
if (strategy !== 'github-actions' && strategy !== 'flux-controller') {
|
|
582
|
-
console.error(`Invalid strategy: ${strategy}. Valid: github-actions, flux-controller`);
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
try {
|
|
586
|
-
await initProject(projectName, template, strategy, operators);
|
|
587
|
-
}
|
|
588
|
-
catch (error) {
|
|
589
|
-
console.error('Error:', error instanceof Error ? error.message : error);
|
|
590
|
-
process.exit(1);
|
|
591
|
-
}
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (command === 'operators') {
|
|
595
|
-
try {
|
|
596
|
-
const entryFile = await findEntryFile(options.entry);
|
|
597
|
-
console.error(`Rendering operators from: ${entryFile}`);
|
|
598
|
-
const { renderToOperatorsYaml } = await import('./renderer.js');
|
|
599
|
-
const yamlOutput = await renderToOperatorsYaml(entryFile);
|
|
600
|
-
if (options.out) {
|
|
601
|
-
const { writeFileSync, mkdirSync } = await import('fs');
|
|
602
|
-
const { dirname } = await import('path');
|
|
603
|
-
mkdirSync(dirname((0, path_1.resolve)(options.out)), { recursive: true });
|
|
604
|
-
writeFileSync((0, path_1.resolve)(options.out), yamlOutput, 'utf-8');
|
|
605
|
-
console.error(`Output written to: ${(0, path_1.resolve)(options.out)}`);
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
console.log(yamlOutput);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
catch (error) {
|
|
612
|
-
console.error('Error:', error instanceof Error ? error.message : error);
|
|
613
|
-
process.exit(1);
|
|
614
|
-
}
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (command === 'list') {
|
|
618
|
-
const { allComponents, operators } = await import('./catalog.js');
|
|
619
|
-
const comps = allComponents();
|
|
620
|
-
console.log('\nComponents:\n');
|
|
621
|
-
const byCat = new Map();
|
|
622
|
-
for (const c of comps) {
|
|
623
|
-
const arr = byCat.get(c.category) ?? [];
|
|
624
|
-
arr.push(c);
|
|
625
|
-
byCat.set(c.category, arr);
|
|
626
|
-
}
|
|
627
|
-
for (const [cat, items] of byCat) {
|
|
628
|
-
console.log(` ${cat}:`);
|
|
629
|
-
for (const c of items) {
|
|
630
|
-
console.log(` ${c.name.padEnd(12)} ${c.package.padEnd(20)} ${c.description}`);
|
|
631
|
-
}
|
|
632
|
-
console.log();
|
|
633
|
-
}
|
|
634
|
-
console.log('Operators:\n');
|
|
635
|
-
for (const op of operators) {
|
|
636
|
-
console.log(` ${op.name.padEnd(24)} ${op.description}`);
|
|
637
|
-
}
|
|
638
|
-
console.log('\nUse "r8s info <name>" for props and examples.');
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
if (command === 'info') {
|
|
642
|
-
const name = args[1];
|
|
643
|
-
if (!name) {
|
|
644
|
-
console.error('Usage: r8s info <component-name>');
|
|
645
|
-
console.error('Example: r8s info App');
|
|
646
|
-
process.exit(1);
|
|
647
|
-
}
|
|
648
|
-
const { findComponent } = await import('./catalog.js');
|
|
649
|
-
const comp = findComponent(name);
|
|
650
|
-
if (!comp) {
|
|
651
|
-
console.error(`Component not found: ${name}`);
|
|
652
|
-
console.error('Use "r8s list" to see available components.');
|
|
653
|
-
process.exit(1);
|
|
654
|
-
}
|
|
655
|
-
console.log(`\n${comp.name} (${comp.package})`);
|
|
656
|
-
console.log(`${comp.category}`);
|
|
657
|
-
console.log(`\n${comp.description}\n`);
|
|
658
|
-
console.log('Props:');
|
|
659
|
-
for (const p of comp.props) {
|
|
660
|
-
const req = p.required ? 'required' : 'optional';
|
|
661
|
-
const def = p.default ? ` [default: ${p.default}]` : '';
|
|
662
|
-
console.log(` ${p.name.padEnd(16)} ${p.type.padEnd(36)} ${req}${def}`);
|
|
663
|
-
console.log(` ${' '.repeat(18)}${p.description}`);
|
|
664
|
-
}
|
|
665
|
-
console.log('\nExample:');
|
|
666
|
-
console.log(` ${comp.example}`);
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (command === 'context') {
|
|
670
|
-
const { allComponents, operators } = await import('./catalog.js');
|
|
671
|
-
const comps = allComponents();
|
|
672
|
-
console.log('# r8s context for LLMs\n');
|
|
673
|
-
console.log('## Workflow');
|
|
674
|
-
console.log('1. Write TSX that default-exports a JSX element');
|
|
675
|
-
console.log('2. Run: r8s render --entry <file.tsx> --out <file.yaml>');
|
|
676
|
-
console.log('3. Commit the YAML. GitOps (FluxCD/ArgoCD) applies it.');
|
|
677
|
-
console.log('4. Never hand-edit YAML — change TSX and re-render.\n');
|
|
678
|
-
console.log('## Rules');
|
|
679
|
-
console.log('- Lowercase elements (<deployment>, <service>) are raw K8s resources.');
|
|
680
|
-
console.log('- PascalCase elements (<App>, <Database>) are recipe components.');
|
|
681
|
-
console.log('- Components are TypeScript functions — testable with render() + vitest.');
|
|
682
|
-
console.log('- Entry file must default-export a JSX element or function.\n');
|
|
683
|
-
console.log('## Components\n');
|
|
684
|
-
for (const c of comps) {
|
|
685
|
-
const required = c.props.filter((p) => p.required).map((p) => `${p.name}: ${p.type}`);
|
|
686
|
-
console.log(`${c.name} (${c.package}) — ${c.description}`);
|
|
687
|
-
console.log(` Required: ${required.join(', ') || 'none'}`);
|
|
688
|
-
console.log(` Example: ${c.example.replace(/\n/g, ' ')}`);
|
|
689
|
-
console.log();
|
|
690
|
-
}
|
|
691
|
-
console.log('## Operators (auto-declared by recipes)');
|
|
692
|
-
for (const op of operators) {
|
|
693
|
-
console.log(` ${op.name} — ${op.description}`);
|
|
694
|
-
}
|
|
695
|
-
console.log('\n## Commands');
|
|
696
|
-
console.log(' r8s init [name] Scaffold a project');
|
|
697
|
-
console.log(' r8s render --entry f.tsx Render to stdout');
|
|
698
|
-
console.log(' r8s render --out k8s.yaml Render to file');
|
|
699
|
-
console.log(' r8s list List all components');
|
|
700
|
-
console.log(' r8s info <name> Show props for a component');
|
|
701
|
-
console.log(' r8s preview <name> Render a component with dummy props');
|
|
702
|
-
console.log(' r8s explain <name> Show resources + operators a component creates');
|
|
703
|
-
console.log(' r8s validate <file.tsx> Type-check + reference-check');
|
|
704
|
-
console.log(' r8s search <term> Search npm for community recipes');
|
|
705
|
-
console.log(' r8s add <package> Install a community recipe from npm');
|
|
706
|
-
console.log(' r8s context This output');
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
if (command === 'search') {
|
|
710
|
-
const term = args.slice(1).join(' ');
|
|
711
|
-
if (!term) {
|
|
712
|
-
console.error('Usage: r8s search <term>');
|
|
713
|
-
console.error('Example: r8s search database');
|
|
714
|
-
process.exit(1);
|
|
715
|
-
}
|
|
716
|
-
console.log(`Searching npm for r8s recipes matching "${term}"...\n`);
|
|
717
|
-
try {
|
|
718
|
-
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(`keywords:r8s ${term}`)}&size=25`;
|
|
719
|
-
const res = await fetch(url);
|
|
720
|
-
const data = await res.json();
|
|
721
|
-
if (!data.objects || data.objects.length === 0) {
|
|
722
|
-
console.log('No packages found.');
|
|
723
|
-
console.log('\nTo publish a recipe, add "r8s" to the keywords in package.json.');
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
console.log('Package Version Description');
|
|
727
|
-
console.log('─'.repeat(80));
|
|
728
|
-
for (const obj of data.objects) {
|
|
729
|
-
const pkg = obj.package;
|
|
730
|
-
const name = pkg.name.padEnd(30);
|
|
731
|
-
const version = pkg.version.padEnd(10);
|
|
732
|
-
const desc = (pkg.description ?? '').substring(0, 38);
|
|
733
|
-
console.log(`${name} ${version} ${desc}`);
|
|
734
|
-
}
|
|
735
|
-
console.log(`\n${data.total} package(s) found.`);
|
|
736
|
-
console.log('Install with: r8s add <package-name>');
|
|
737
|
-
}
|
|
738
|
-
catch (error) {
|
|
739
|
-
console.error('Search failed:', error instanceof Error ? error.message : error);
|
|
740
|
-
process.exit(1);
|
|
741
|
-
}
|
|
742
|
-
return;
|
|
597
|
+
catch {
|
|
598
|
+
// best-effort cleanup of a temp preview file
|
|
743
599
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
console.log(`Installing ${packageName}...`);
|
|
752
|
-
const { execSync } = await import('child_process');
|
|
753
|
-
try {
|
|
754
|
-
execSync(`npm install ${packageName}`, { stdio: 'inherit' });
|
|
755
|
-
console.log(`\n✅ ${packageName} installed.`);
|
|
756
|
-
console.log(`Import components in your TSX:`);
|
|
757
|
-
console.log(` import { MyComponent } from '${packageName}'`);
|
|
758
|
-
}
|
|
759
|
-
catch (error) {
|
|
760
|
-
console.error('Install failed:', error instanceof Error ? error.message : error);
|
|
761
|
-
process.exit(1);
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
600
|
+
}
|
|
601
|
+
/** Components must exist in the catalog — resolve names must too. */
|
|
602
|
+
function requireComponent(name, usage) {
|
|
603
|
+
if (!name) {
|
|
604
|
+
console.error(usage);
|
|
605
|
+
process.exit(1);
|
|
764
606
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (!name) {
|
|
768
|
-
console.error('Usage: r8s preview <component-name>');
|
|
769
|
-
console.error('Example: r8s preview App');
|
|
770
|
-
process.exit(1);
|
|
771
|
-
}
|
|
772
|
-
const { findComponent } = await import('./catalog.js');
|
|
607
|
+
// dynamic import kept local so `r8s list` stays cold-start fast
|
|
608
|
+
return import('./catalog.js').then(({ findComponent }) => {
|
|
773
609
|
const comp = findComponent(name);
|
|
774
610
|
if (!comp) {
|
|
775
611
|
console.error(`Component not found: ${name}`);
|
|
776
612
|
console.error('Use "r8s list" to see available components.');
|
|
777
613
|
process.exit(1);
|
|
778
614
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const tmpFile = (0, path_1.resolve)(`.r8s-preview-${Date.now()}.tsx`);
|
|
794
|
-
(0, fs_1.writeFileSync)(tmpFile, tsx, 'utf-8');
|
|
795
|
-
try {
|
|
796
|
-
const { renderToYaml } = await import('./renderer.js');
|
|
797
|
-
const yaml = await renderToYaml(tmpFile);
|
|
798
|
-
console.log(`# Preview of ${comp.name} with dummy required props\n`);
|
|
799
|
-
console.log(yaml);
|
|
800
|
-
}
|
|
801
|
-
catch (error) {
|
|
802
|
-
console.error('Preview failed:', error instanceof Error ? error.message : error);
|
|
803
|
-
console.error('\nThis component may require a Platform context or specific props.');
|
|
804
|
-
process.exit(1);
|
|
805
|
-
}
|
|
806
|
-
finally {
|
|
807
|
-
try {
|
|
808
|
-
require('fs').unlinkSync(tmpFile);
|
|
809
|
-
}
|
|
810
|
-
catch { }
|
|
811
|
-
}
|
|
812
|
-
return;
|
|
615
|
+
return comp;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async function cmdInit({ args, options }) {
|
|
619
|
+
const projectName = args[1] || 'r8s-app';
|
|
620
|
+
const template = options.template || 'basic';
|
|
621
|
+
const strategy = options.strategy || 'github-actions';
|
|
622
|
+
const operators = options.operators
|
|
623
|
+
?.split(',')
|
|
624
|
+
.map((op) => op.trim())
|
|
625
|
+
.filter(Boolean);
|
|
626
|
+
if (strategy !== 'github-actions' && strategy !== 'flux-controller') {
|
|
627
|
+
console.error(`Invalid strategy: ${strategy}. Valid: github-actions, flux-controller`);
|
|
628
|
+
process.exit(1);
|
|
813
629
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
630
|
+
try {
|
|
631
|
+
await initProject(projectName, template, strategy, operators);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Render-to-file or stdout: the shared output path for render/operators. */
|
|
639
|
+
async function writeOutput(yaml, out) {
|
|
640
|
+
if (out) {
|
|
641
|
+
const { writeFileSync, mkdirSync } = await import('fs');
|
|
642
|
+
const { dirname } = await import('path');
|
|
643
|
+
mkdirSync(dirname((0, path_1.resolve)(out)), { recursive: true });
|
|
644
|
+
writeFileSync((0, path_1.resolve)(out), yaml, 'utf-8');
|
|
645
|
+
console.error(`Output written to: ${(0, path_1.resolve)(out)}`);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
console.log(yaml);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function failWith(prefix, error, sanitize = false) {
|
|
652
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
653
|
+
console.error(prefix, sanitize ? (0, core_1.sanitizeErrorMessage)(msg) : msg);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
async function cmdOperators({ options }) {
|
|
657
|
+
try {
|
|
658
|
+
const entryFile = await findEntryFile(options.entry);
|
|
659
|
+
console.error(`Rendering operators from: ${entryFile}`);
|
|
660
|
+
const { renderToOperatorsYaml } = await import('./renderer.js');
|
|
661
|
+
await writeOutput(await renderToOperatorsYaml(entryFile), options.out);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
failWith('Error:', error);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function cmdList() {
|
|
668
|
+
const { allComponents, operators } = await import('./catalog.js');
|
|
669
|
+
const comps = allComponents();
|
|
670
|
+
console.log('\nComponents:\n');
|
|
671
|
+
const byCat = new Map();
|
|
672
|
+
for (const c of comps) {
|
|
673
|
+
const arr = byCat.get(c.category) ?? [];
|
|
674
|
+
arr.push(c);
|
|
675
|
+
byCat.set(c.category, arr);
|
|
676
|
+
}
|
|
677
|
+
for (const [cat, items] of byCat) {
|
|
678
|
+
console.log(` ${cat}:`);
|
|
679
|
+
for (const c of items) {
|
|
680
|
+
console.log(` ${c.name.padEnd(12)} ${c.package.padEnd(20)} ${c.description}`);
|
|
865
681
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
682
|
+
console.log();
|
|
683
|
+
}
|
|
684
|
+
console.log('Operators:\n');
|
|
685
|
+
for (const op of operators) {
|
|
686
|
+
console.log(` ${op.name.padEnd(24)} ${op.description}`);
|
|
687
|
+
}
|
|
688
|
+
console.log('\nUse "r8s info <name>" for props and examples.');
|
|
689
|
+
}
|
|
690
|
+
async function cmdInfo({ args }) {
|
|
691
|
+
const comp = await requireComponent(args[1], 'Usage: r8s info <component-name>');
|
|
692
|
+
console.log(`\n${comp.name} (${comp.package})`);
|
|
693
|
+
console.log(`${comp.category}`);
|
|
694
|
+
console.log(`\n${comp.description}\n`);
|
|
695
|
+
console.log('Props:');
|
|
696
|
+
for (const p of comp.props) {
|
|
697
|
+
const req = p.required ? 'required' : 'optional';
|
|
698
|
+
const def = p.default ? ` [default: ${p.default}]` : '';
|
|
699
|
+
console.log(` ${p.name.padEnd(16)} ${p.type.padEnd(36)} ${req}${def}`);
|
|
700
|
+
console.log(` ${' '.repeat(18)}${p.description}`);
|
|
701
|
+
}
|
|
702
|
+
console.log('\nExample:');
|
|
703
|
+
console.log(` ${comp.example}`);
|
|
704
|
+
}
|
|
705
|
+
async function cmdContext() {
|
|
706
|
+
const { allComponents, operators } = await import('./catalog.js');
|
|
707
|
+
const comps = allComponents();
|
|
708
|
+
console.log('# r8s context for LLMs\n');
|
|
709
|
+
console.log('## Workflow');
|
|
710
|
+
console.log('1. Write TSX that default-exports a JSX element');
|
|
711
|
+
console.log('2. Run: r8s render --entry <file.tsx> --out <file.yaml>');
|
|
712
|
+
console.log('3. Commit the YAML. GitOps (FluxCD/ArgoCD) applies it.');
|
|
713
|
+
console.log('4. Never hand-edit YAML — change TSX and re-render.\n');
|
|
714
|
+
console.log('## Rules');
|
|
715
|
+
console.log('- Lowercase elements (<deployment>, <service>) are raw K8s resources.');
|
|
716
|
+
console.log('- PascalCase elements (<App>, <Database>) are recipe components.');
|
|
717
|
+
console.log('- Components are TypeScript functions — testable with render() + vitest.');
|
|
718
|
+
console.log('- Entry file must default-export a JSX element or function.\n');
|
|
719
|
+
console.log('## Components\n');
|
|
720
|
+
for (const c of comps) {
|
|
721
|
+
const required = c.props.filter((p) => p.required).map((p) => `${p.name}: ${p.type}`);
|
|
722
|
+
console.log(`${c.name} (${c.package}) — ${c.description}`);
|
|
723
|
+
console.log(` Required: ${required.join(', ') || 'none'}`);
|
|
724
|
+
console.log(` Example: ${c.example.replace(/\n/g, ' ')}`);
|
|
725
|
+
console.log();
|
|
726
|
+
}
|
|
727
|
+
console.log('## Operators (auto-declared by recipes)');
|
|
728
|
+
for (const op of operators) {
|
|
729
|
+
console.log(` ${op.name} — ${op.description}`);
|
|
730
|
+
}
|
|
731
|
+
console.log('\n## Commands');
|
|
732
|
+
console.log(' r8s init [name] Scaffold a project');
|
|
733
|
+
console.log(' r8s render --entry f.tsx Render to stdout');
|
|
734
|
+
console.log(' r8s render --out k8s.yaml Render to file');
|
|
735
|
+
console.log(' r8s list List all components');
|
|
736
|
+
console.log(' r8s info <name> Show props for a component');
|
|
737
|
+
console.log(' r8s preview <name> Render a component with dummy props');
|
|
738
|
+
console.log(' r8s explain <name> Show resources + operators a component creates');
|
|
739
|
+
console.log(' r8s validate <file.tsx> Type-check + reference-check');
|
|
740
|
+
console.log(' r8s search <term> Search npm for community recipes');
|
|
741
|
+
console.log(' r8s add <package> Install a community recipe from npm');
|
|
742
|
+
console.log(' r8s context This output');
|
|
743
|
+
}
|
|
744
|
+
async function cmdSearch({ args }) {
|
|
745
|
+
const term = args.slice(1).join(' ');
|
|
746
|
+
if (!term) {
|
|
747
|
+
console.error('Usage: r8s search <term>');
|
|
748
|
+
console.error('Example: r8s search database');
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
console.log(`Searching npm for r8s recipes matching "${term}"...\n`);
|
|
752
|
+
try {
|
|
753
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(`keywords:r8s ${term}`)}&size=25`;
|
|
754
|
+
const res = await fetch(url);
|
|
755
|
+
const data = await res.json();
|
|
756
|
+
if (!data.objects || data.objects.length === 0) {
|
|
757
|
+
console.log('No packages found.');
|
|
758
|
+
console.log('\nTo publish a recipe, add "r8s" to the keywords in package.json.');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
console.log('Package Version Description');
|
|
762
|
+
console.log('─'.repeat(80));
|
|
763
|
+
for (const obj of data.objects) {
|
|
764
|
+
const pkg = obj.package;
|
|
765
|
+
console.log(`${pkg.name.padEnd(30)} ${String(pkg.version).padEnd(10)} ${(pkg.description ?? '').substring(0, 38)}`);
|
|
766
|
+
}
|
|
767
|
+
console.log(`\n${data.total} package(s) found.`);
|
|
768
|
+
console.log('Install with: r8s add <package-name>');
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
failWith('Search failed:', error);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function cmdAdd({ args }) {
|
|
775
|
+
const packageName = args[1];
|
|
776
|
+
if (!packageName) {
|
|
777
|
+
console.error('Usage: r8s add <package-name>');
|
|
778
|
+
console.error('Example: r8s add @acme/r8s-redis');
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
console.log(`Installing ${packageName}...`);
|
|
782
|
+
const { execSync } = await import('child_process');
|
|
783
|
+
try {
|
|
784
|
+
execSync(`npm install ${packageName}`, { stdio: 'inherit' });
|
|
785
|
+
console.log(`\n✅ ${packageName} installed.`);
|
|
786
|
+
console.log(`Import components in your TSX:`);
|
|
787
|
+
console.log(` import { MyComponent } from '${packageName}'`);
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
failWith('Install failed:', error);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async function cmdPreview({ args }) {
|
|
794
|
+
const comp = await requireComponent(args[1], 'Usage: r8s preview <component-name>');
|
|
795
|
+
const tmpFile = writeDummyTsx(comp, 'preview');
|
|
796
|
+
try {
|
|
797
|
+
const { renderToYaml } = await import('./renderer.js');
|
|
798
|
+
console.log(`# Preview of ${comp.name} with dummy required props\n`);
|
|
799
|
+
console.log(await renderToYaml(tmpFile));
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.error('Preview failed:', error instanceof Error ? error.message : error);
|
|
803
|
+
console.error('\nThis component may require a Platform context or specific props.');
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
removeQuietly(tmpFile);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async function cmdExplain({ args }) {
|
|
811
|
+
const comp = await requireComponent(args[1], 'Usage: r8s explain <component-name>');
|
|
812
|
+
const { operators } = await import('./catalog.js');
|
|
813
|
+
console.log(`\n${comp.name} (${comp.package})`);
|
|
814
|
+
console.log(`${comp.description}\n`);
|
|
815
|
+
const tmpFile = writeDummyTsx(comp, 'explain');
|
|
816
|
+
try {
|
|
817
|
+
const { bundleAndRender } = await import('./renderer.js');
|
|
818
|
+
const result = await bundleAndRender(tmpFile);
|
|
819
|
+
console.log('Resources created:');
|
|
820
|
+
for (const r of result.resources) {
|
|
821
|
+
console.log(` ${r.kind.padEnd(24)} ${r.metadata?.namespace ?? ''}/${r.metadata?.name ?? ''}`);
|
|
822
|
+
}
|
|
823
|
+
if (result.operators.length > 0) {
|
|
824
|
+
console.log('\nOperators required:');
|
|
825
|
+
for (const op of result.operators) {
|
|
826
|
+
const meta = operators.find((o) => o.name === op.name);
|
|
827
|
+
console.log(` ${op.name.padEnd(24)} ${meta?.description ?? ''}`);
|
|
869
828
|
}
|
|
870
|
-
catch { }
|
|
871
829
|
}
|
|
872
|
-
|
|
830
|
+
console.log(`\n${result.resources.length} resource(s), ${result.operators.length} operator(s).`);
|
|
873
831
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
stdio: 'pipe',
|
|
897
|
-
cwd: process.cwd(),
|
|
898
|
-
});
|
|
899
|
-
console.log('✅ TypeScript: no errors');
|
|
900
|
-
}
|
|
901
|
-
catch (error) {
|
|
902
|
-
const stdout = error.stdout?.toString() ?? '';
|
|
903
|
-
const stderr = error.stderr?.toString() ?? '';
|
|
904
|
-
console.error('❌ TypeScript errors:');
|
|
905
|
-
console.error(stdout || stderr || error.message);
|
|
906
|
-
process.exit(1);
|
|
907
|
-
}
|
|
908
|
-
// 2. Render and check references
|
|
909
|
-
try {
|
|
910
|
-
const { bundleAndRender } = await import('./renderer.js');
|
|
911
|
-
const result = await bundleAndRender(resolved);
|
|
912
|
-
const resources = result.resources;
|
|
913
|
-
const names = new Set(resources.map((r) => `${r.kind}/${r.metadata?.namespace ?? ''}/${r.metadata?.name ?? ''}`));
|
|
914
|
-
const issues = [];
|
|
915
|
-
// Check HTTPRoute backendRefs
|
|
916
|
-
for (const route of resources.filter((r) => r.kind === 'HTTPRoute' || r.kind === 'Ingress')) {
|
|
917
|
-
const refs = route.kind === 'HTTPRoute'
|
|
918
|
-
? (route.spec?.rules?.flatMap((r) => r.backendRefs ?? []) ?? [])
|
|
919
|
-
: (route.spec?.rules?.flatMap((r) => r.http?.paths?.map((p) => p.backend?.service) ?? []) ?? []);
|
|
920
|
-
for (const ref of refs) {
|
|
921
|
-
const svcName = ref.name;
|
|
922
|
-
const svc = resources.find((r) => r.kind === 'Service' && r.metadata.name === svcName);
|
|
923
|
-
if (!svc) {
|
|
924
|
-
issues.push(`⚠️ ${route.kind} ${route.metadata.name} → Service "${svcName}" not found (operator-managed?)`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
// Check Deployment volume refs
|
|
929
|
-
for (const d of resources.filter((r) => r.kind === 'Deployment' || r.kind === 'StatefulSet')) {
|
|
930
|
-
const vols = d.spec?.template?.spec?.volumes ?? [];
|
|
931
|
-
for (const vol of vols) {
|
|
932
|
-
if (vol.secret) {
|
|
933
|
-
const sec = resources.find((r) => r.kind === 'Secret' && r.metadata.name === vol.secret.secretName);
|
|
934
|
-
if (!sec)
|
|
935
|
-
issues.push(`⚠️ ${d.kind} ${d.metadata.name} → Secret "${vol.secret.secretName}" not found`);
|
|
936
|
-
}
|
|
937
|
-
if (vol.configMap) {
|
|
938
|
-
const cm = resources.find((r) => r.kind === 'ConfigMap' && r.metadata.name === vol.configMap.name);
|
|
939
|
-
if (!cm)
|
|
940
|
-
issues.push(`⚠️ ${d.kind} ${d.metadata.name} → ConfigMap "${vol.configMap.name}" not found`);
|
|
941
|
-
}
|
|
942
|
-
if (vol.persistentVolumeClaim) {
|
|
943
|
-
const pvc = resources.find((r) => r.kind === 'PersistentVolumeClaim' &&
|
|
944
|
-
r.metadata.name === vol.persistentVolumeClaim.claimName);
|
|
945
|
-
if (!pvc)
|
|
946
|
-
issues.push(`⚠️ ${d.kind} ${d.metadata.name} → PVC "${vol.persistentVolumeClaim.claimName}" not found`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
// Check empty DNSEndpoint targets
|
|
951
|
-
for (const dns of resources.filter((r) => r.kind === 'DNSEndpoint')) {
|
|
952
|
-
for (const ep of dns.spec?.endpoints ?? []) {
|
|
953
|
-
if (!ep.targets || ep.targets.length === 0) {
|
|
954
|
-
issues.push(`⚠️ DNSEndpoint ${dns.metadata.name} has empty targets`);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
console.log(`✅ Render: ${resources.length} resources, ${result.operators.length} operators`);
|
|
959
|
-
if (issues.length > 0) {
|
|
960
|
-
console.log(`\n${issues.length} reference issue(s) found:`);
|
|
961
|
-
for (const issue of issues) {
|
|
962
|
-
console.log(` ${issue}`);
|
|
963
|
-
}
|
|
964
|
-
console.log('\nSome references may be operator-managed (e.g. Keycloak Service).');
|
|
965
|
-
process.exit(1);
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
console.log('✅ References: all resolved');
|
|
832
|
+
catch (error) {
|
|
833
|
+
console.error('Explain failed:', error instanceof Error ? error.message : error);
|
|
834
|
+
console.error('\nThis component may require a Platform context.');
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
finally {
|
|
838
|
+
removeQuietly(tmpFile);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/** Reference checks after render: routes→Services, volumes→Secrets/ConfigMaps/PVCs, DNS targets. */
|
|
842
|
+
function missingKinds(resources) {
|
|
843
|
+
return (kind, name) => !resources.some((r) => r.kind === kind && r.metadata?.name === name);
|
|
844
|
+
}
|
|
845
|
+
function routeIssues(resources, missing) {
|
|
846
|
+
const issues = [];
|
|
847
|
+
for (const route of resources.filter((r) => r.kind === 'HTTPRoute' || r.kind === 'Ingress')) {
|
|
848
|
+
const refs = route.kind === 'HTTPRoute'
|
|
849
|
+
? (route.spec?.rules?.flatMap((r) => r.backendRefs ?? []) ?? [])
|
|
850
|
+
: (route.spec?.rules?.flatMap((r) => r.http?.paths?.map((p) => p.backend?.service) ?? []) ?? []);
|
|
851
|
+
for (const ref of refs) {
|
|
852
|
+
if (missing('Service', ref.name)) {
|
|
853
|
+
issues.push(`⚠️ ${route.kind} ${route.metadata.name} → Service "${ref.name}" not found (operator-managed?)`);
|
|
969
854
|
}
|
|
970
855
|
}
|
|
971
|
-
|
|
972
|
-
|
|
856
|
+
}
|
|
857
|
+
return issues;
|
|
858
|
+
}
|
|
859
|
+
function volumeIssues(resources, missing) {
|
|
860
|
+
const targetsOf = (vol) => [
|
|
861
|
+
[vol.secret?.secretName, 'Secret'],
|
|
862
|
+
[vol.configMap?.name, 'ConfigMap'],
|
|
863
|
+
[vol.persistentVolumeClaim?.claimName, 'PersistentVolumeClaim'],
|
|
864
|
+
];
|
|
865
|
+
const issuesFor = (d, vol) => targetsOf(vol)
|
|
866
|
+
.filter(([target, kind]) => target !== undefined && missing(kind, target))
|
|
867
|
+
.map(([target, kind]) => `⚠️ ${d.kind} ${d.metadata.name} → ${kind} "${target}" not found`);
|
|
868
|
+
return resources
|
|
869
|
+
.filter((r) => r.kind === 'Deployment' || r.kind === 'StatefulSet')
|
|
870
|
+
.flatMap((d) => (d.spec?.template?.spec?.volumes ?? []).flatMap((vol) => issuesFor(d, vol)));
|
|
871
|
+
}
|
|
872
|
+
function dnsIssues(resources) {
|
|
873
|
+
return resources
|
|
874
|
+
.filter((r) => r.kind === 'DNSEndpoint')
|
|
875
|
+
.flatMap((dns) => dns.spec?.endpoints ?? [])
|
|
876
|
+
.filter((ep) => !ep.targets || ep.targets.length === 0)
|
|
877
|
+
.map((ep) => `⚠️ DNSEndpoint ${ep.dnsName ?? ''} has empty targets`);
|
|
878
|
+
}
|
|
879
|
+
function referenceIssues(resources) {
|
|
880
|
+
const missing = missingKinds(resources);
|
|
881
|
+
return [
|
|
882
|
+
...routeIssues(resources, missing),
|
|
883
|
+
...volumeIssues(resources, missing),
|
|
884
|
+
...dnsIssues(resources),
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
function typecheckEntry(resolved) {
|
|
888
|
+
try {
|
|
889
|
+
const { execSync } = require('child_process');
|
|
890
|
+
const tsconfigPath = (0, path_1.resolve)('tsconfig.json');
|
|
891
|
+
const tscCmd = (0, fs_1.existsSync)(tsconfigPath)
|
|
892
|
+
? `npx tsc --noEmit -p ${tsconfigPath}`
|
|
893
|
+
: `npx tsc --noEmit --jsx react-jsx --jsxImportSource @r8s/core --moduleResolution bundler --target es2022 --module esnext ${resolved}`;
|
|
894
|
+
execSync(tscCmd, { stdio: 'pipe', cwd: process.cwd() });
|
|
895
|
+
console.log('✅ TypeScript: no errors');
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
const stdout = error.stdout?.toString() ?? '';
|
|
899
|
+
const stderr = error.stderr?.toString() ?? '';
|
|
900
|
+
console.error('❌ TypeScript errors:');
|
|
901
|
+
console.error(stdout || stderr || error.message);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async function cmdValidate({ args }) {
|
|
906
|
+
const entryFile = args[1];
|
|
907
|
+
if (!entryFile) {
|
|
908
|
+
console.error('Usage: r8s validate <file.tsx>');
|
|
909
|
+
console.error('Example: r8s validate infra/app.tsx');
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
const resolved = (0, path_1.resolve)(entryFile);
|
|
913
|
+
if (!(0, fs_1.existsSync)(resolved)) {
|
|
914
|
+
console.error(`File not found: ${resolved}`);
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
console.log(`Validating: ${resolved}\n`);
|
|
918
|
+
typecheckEntry(resolved);
|
|
919
|
+
try {
|
|
920
|
+
const { bundleAndRender } = await import('./renderer.js');
|
|
921
|
+
const result = await bundleAndRender(resolved);
|
|
922
|
+
const issues = referenceIssues(result.resources);
|
|
923
|
+
console.log(`✅ Render: ${result.resources.length} resources, ${result.operators.length} operators`);
|
|
924
|
+
if (issues.length > 0) {
|
|
925
|
+
console.log(`\n${issues.length} reference issue(s) found:`);
|
|
926
|
+
for (const issue of issues)
|
|
927
|
+
console.log(` ${issue}`);
|
|
928
|
+
console.log('\nSome references may be operator-managed (e.g. Keycloak Service).');
|
|
973
929
|
process.exit(1);
|
|
974
930
|
}
|
|
975
|
-
|
|
931
|
+
console.log('✅ References: all resolved');
|
|
976
932
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
showHelp();
|
|
980
|
-
process.exit(1);
|
|
933
|
+
catch (error) {
|
|
934
|
+
failWith('❌ Render failed:', error, true);
|
|
981
935
|
}
|
|
936
|
+
}
|
|
937
|
+
async function cmdRender({ options }) {
|
|
982
938
|
try {
|
|
983
939
|
const entryFile = await findEntryFile(options.entry);
|
|
984
940
|
console.error(`Rendering: ${entryFile}`);
|
|
@@ -992,20 +948,48 @@ async function main() {
|
|
|
992
948
|
// stay faithful so local dev workflows still apply real values.
|
|
993
949
|
redactSecrets: options.skipSecretGuardrails && !options.out,
|
|
994
950
|
});
|
|
995
|
-
|
|
996
|
-
const { writeFileSync, mkdirSync } = await import('fs');
|
|
997
|
-
const { dirname } = await import('path');
|
|
998
|
-
mkdirSync(dirname((0, path_1.resolve)(options.out)), { recursive: true });
|
|
999
|
-
writeFileSync((0, path_1.resolve)(options.out), yamlOutput, 'utf-8');
|
|
1000
|
-
console.error(`Output written to: ${(0, path_1.resolve)(options.out)}`);
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
console.log(yamlOutput);
|
|
1004
|
-
}
|
|
951
|
+
await writeOutput(yamlOutput, options.out);
|
|
1005
952
|
}
|
|
1006
953
|
catch (error) {
|
|
1007
|
-
|
|
1008
|
-
|
|
954
|
+
failWith('Error:', error, true);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function main() {
|
|
958
|
+
const args = process.argv.slice(2);
|
|
959
|
+
const options = parseArgs(args);
|
|
960
|
+
if (options.help) {
|
|
961
|
+
showHelp();
|
|
962
|
+
process.exit(0);
|
|
963
|
+
}
|
|
964
|
+
const ctx = { args, options };
|
|
965
|
+
switch (args[0] || 'render') {
|
|
966
|
+
case 'init':
|
|
967
|
+
return cmdInit(ctx);
|
|
968
|
+
case 'operators':
|
|
969
|
+
return cmdOperators(ctx);
|
|
970
|
+
case 'list':
|
|
971
|
+
return cmdList();
|
|
972
|
+
case 'info':
|
|
973
|
+
return cmdInfo(ctx);
|
|
974
|
+
case 'context':
|
|
975
|
+
return cmdContext();
|
|
976
|
+
case 'search':
|
|
977
|
+
return cmdSearch(ctx);
|
|
978
|
+
case 'add':
|
|
979
|
+
return cmdAdd(ctx);
|
|
980
|
+
case 'preview':
|
|
981
|
+
return cmdPreview(ctx);
|
|
982
|
+
case 'explain':
|
|
983
|
+
return cmdExplain(ctx);
|
|
984
|
+
case 'validate':
|
|
985
|
+
return cmdValidate(ctx);
|
|
986
|
+
case 'render':
|
|
987
|
+
return cmdRender(ctx);
|
|
988
|
+
default: {
|
|
989
|
+
console.error(`Unknown command: ${args[0]}`);
|
|
990
|
+
showHelp();
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
1009
993
|
}
|
|
1010
994
|
}
|
|
1011
995
|
main();
|