@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/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
- if (arg === '--entry' || arg === '-e') {
12
- options.entry = args[++i];
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 === '--out' || arg === '-o') {
15
- options.out = args[++i];
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,openbao
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
- const VALID_OPERATORS = [
115
- 'cert-manager',
116
- 'openbao',
117
- 'keycloak',
118
- 'external-dns',
119
- 'redis',
120
- 'envoy',
121
- 'prometheus',
122
- 'clickhouse',
123
- 'logging-operator',
124
- 'loki',
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 if provided
133
+ // Validate operators against the generated registry; accept aliases
132
134
  if (operators && operators.length > 0) {
133
- const invalid = operators.filter((op) => !VALID_OPERATORS.includes(op));
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: ${VALID_OPERATORS.join(', ')}`);
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 if requested
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
- dependencies[`@r8s/${op}`] = '^0.1.0';
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
- async function main() {
566
- const args = process.argv.slice(2);
567
- const options = parseArgs(args);
568
- if (options.help) {
569
- showHelp();
570
- process.exit(0);
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
- const command = args[0] || 'render';
573
- if (command === 'init') {
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
- if (command === 'add') {
745
- const packageName = args[1];
746
- if (!packageName) {
747
- console.error('Usage: r8s add <package-name>');
748
- console.error('Example: r8s add @acme/r8s-redis');
749
- process.exit(1);
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
- if (command === 'preview') {
766
- const name = args[1];
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
- // Build a TSX file that renders the component with dummy required props
780
- const requiredProps = comp.props.filter((p) => p.required);
781
- const dummyValues = {
782
- name: '"example"',
783
- image: '"example/app:v1"',
784
- host: '"example.com"',
785
- serviceName: '"example"',
786
- children: 'null',
787
- selector: '{ app: "example" }',
788
- };
789
- const propsStr = requiredProps
790
- .map((p) => `${p.name}={${dummyValues[p.name] ?? '"dummy"'}}`)
791
- .join(' ');
792
- const tsx = `import { ${comp.name} } from '${comp.package}'\nexport default <${comp.name} ${propsStr} />\n`;
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
- if (command === 'explain') {
815
- const name = args[1];
816
- if (!name) {
817
- console.error('Usage: r8s explain <component-name>');
818
- console.error('Example: r8s explain App');
819
- process.exit(1);
820
- }
821
- const { findComponent, operators } = await import('./catalog.js');
822
- const comp = findComponent(name);
823
- if (!comp) {
824
- console.error(`Component not found: ${name}`);
825
- process.exit(1);
826
- }
827
- console.log(`\n${comp.name} (${comp.package})`);
828
- console.log(`${comp.description}\n`);
829
- // Render with dummy props to discover what resources it creates
830
- const requiredProps = comp.props.filter((p) => p.required);
831
- const dummyValues = {
832
- name: '"example"',
833
- image: '"example/app:v1"',
834
- host: '"example.com"',
835
- serviceName: '"example"',
836
- children: 'null',
837
- selector: '{ app: "example" }',
838
- };
839
- const propsStr = requiredProps
840
- .map((p) => `${p.name}={${dummyValues[p.name] ?? '"dummy"'}}`)
841
- .join(' ');
842
- const tsx = `import { ${comp.name} } from '${comp.package}'\nexport default <${comp.name} ${propsStr} />\n`;
843
- const tmpFile = (0, path_1.resolve)(`.r8s-explain-${Date.now()}.tsx`);
844
- (0, fs_1.writeFileSync)(tmpFile, tsx, 'utf-8');
845
- try {
846
- const { bundleAndRender } = await import('./renderer.js');
847
- const result = await bundleAndRender(tmpFile);
848
- console.log('Resources created:');
849
- for (const r of result.resources) {
850
- console.log(` ${r.kind.padEnd(24)} ${r.metadata?.namespace ?? ''}/${r.metadata?.name ?? ''}`);
851
- }
852
- if (result.operators.length > 0) {
853
- console.log('\nOperators required:');
854
- for (const op of result.operators) {
855
- const meta = operators.find((o) => o.name === op.name);
856
- console.log(` ${op.name.padEnd(24)} ${meta?.description ?? ''}`);
857
- }
858
- }
859
- console.log(`\n${result.resources.length} resource(s), ${result.operators.length} operator(s).`);
860
- }
861
- catch (error) {
862
- console.error('Explain failed:', error instanceof Error ? error.message : error);
863
- console.error('\nThis component may require a Platform context.');
864
- process.exit(1);
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
- finally {
867
- try {
868
- require('fs').unlinkSync(tmpFile);
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
- return;
830
+ console.log(`\n${result.resources.length} resource(s), ${result.operators.length} operator(s).`);
873
831
  }
874
- if (command === 'validate') {
875
- const entryFile = args[1];
876
- if (!entryFile) {
877
- console.error('Usage: r8s validate <file.tsx>');
878
- console.error('Example: r8s validate infra/app.tsx');
879
- process.exit(1);
880
- }
881
- const resolved = (0, path_1.resolve)(entryFile);
882
- if (!(0, fs_1.existsSync)(resolved)) {
883
- console.error(`File not found: ${resolved}`);
884
- process.exit(1);
885
- }
886
- console.log(`Validating: ${resolved}\n`);
887
- // 1. Type-check with tsc using the project tsconfig
888
- try {
889
- const { execSync } = await import('child_process');
890
- // Use --noEmit with the project's tsconfig if available, else minimal flags
891
- const tsconfigPath = (0, path_1.resolve)('tsconfig.json');
892
- const tscCmd = (0, fs_1.existsSync)(tsconfigPath)
893
- ? `npx tsc --noEmit -p ${tsconfigPath}`
894
- : `npx tsc --noEmit --jsx react-jsx --jsxImportSource @r8s/core --moduleResolution bundler --target es2022 --module esnext ${resolved}`;
895
- execSync(tscCmd, {
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
- catch (error) {
972
- console.error('❌ Render failed:', (0, core_1.sanitizeErrorMessage)(error instanceof Error ? error.message : String(error)));
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
- return;
931
+ console.log('✅ References: all resolved');
976
932
  }
977
- if (command !== 'render') {
978
- console.error(`Unknown command: ${command}`);
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
- if (options.out) {
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
- console.error('Error:', (0, core_1.sanitizeErrorMessage)(error instanceof Error ? error.message : String(error)));
1008
- process.exit(1);
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();