@r8s/cli 0.1.0 → 0.2.1

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