@r8s/cli 0.1.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 ADDED
@@ -0,0 +1,1001 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ function parseArgs(args) {
7
+ const options = {};
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+ if (arg === '--entry' || arg === '-e') {
11
+ options.entry = args[++i];
12
+ }
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
+ }
34
+ }
35
+ return options;
36
+ }
37
+ function showHelp() {
38
+ console.log(`
39
+ r8s CLI - Render TSX components to Kubernetes YAML
40
+
41
+ Usage: r8s [command] [options]
42
+
43
+ Commands:
44
+ render Render k8s/r8s.tsx to YAML (default)
45
+ operators Render only operator manifests
46
+ init Scaffold a new r8s project
47
+ list List all available components and operators
48
+ info Show props and example for a component (e.g. r8s info App)
49
+ context Print a compact LLM context blob (component model + workflow)
50
+ search Search npm for r8s community recipes (e.g. r8s search database)
51
+ add Install a community recipe from npm (e.g. r8s add @acme/r8s-redis)
52
+ preview Render a component with dummy defaults to see output (e.g. r8s preview App)
53
+ explain Show resources and operators a component creates (e.g. r8s explain App)
54
+ validate Type-check and validate rendered output (e.g. r8s validate infra.tsx)
55
+
56
+ Options:
57
+ --entry, -e <path> Entry file path (default: k8s/r8s.tsx)
58
+ --out, -o <path> Output file path (default: stdout)
59
+ --include-operators Include operator manifests in rendered output
60
+ --operators-only Render only operator manifests (with render command)
61
+ --template, -t <name> Template for init (basic, fullstack) [default: basic]
62
+ --operators <list> Comma-separated list of operators to include
63
+ --strategy, -s <name> Deployment strategy:
64
+ - github-actions: Render YAML in CI (default)
65
+ - flux-controller: Keep .tsx files, render in-cluster
66
+ --help, -h Show this help message
67
+
68
+ Examples:
69
+ r8s render
70
+ r8s render --entry ./infra/manifest.tsx
71
+ r8s render --out ./output/k8s.yaml --include-operators
72
+ r8s operators --out ./operators.yaml
73
+ r8s init
74
+ r8s init my-project
75
+ r8s init my-project --template fullstack
76
+ r8s init my-project --strategy flux-controller
77
+ r8s init my-project --operators cert-manager,openbao
78
+ r8s list
79
+ r8s info App
80
+ r8s info Database
81
+ r8s context
82
+ r8s search database
83
+ r8s add @acme/r8s-redis
84
+ r8s preview App
85
+ r8s explain App
86
+ r8s validate infra.tsx
87
+ `);
88
+ }
89
+ async function findEntryFile(entryPath) {
90
+ if (entryPath) {
91
+ const resolved = (0, path_1.resolve)(entryPath);
92
+ if (!(0, fs_1.existsSync)(resolved)) {
93
+ throw new Error(`Entry file not found: ${resolved}`);
94
+ }
95
+ return resolved;
96
+ }
97
+ const defaults = ['k8s/r8s.tsx', 'k8s/r8s.tsx', 'k8s/index.tsx', 'infra/r8s.tsx'];
98
+ for (const defaultPath of defaults) {
99
+ const resolved = (0, path_1.resolve)(defaultPath);
100
+ if ((0, fs_1.existsSync)(resolved)) {
101
+ return resolved;
102
+ }
103
+ }
104
+ throw new Error('No entry file found. Expected one of:\n' +
105
+ defaults.map((d) => ` - ${d}`).join('\n') +
106
+ '\n\nUse --entry to specify a custom path.');
107
+ }
108
+ const VALID_OPERATORS = [
109
+ 'cert-manager',
110
+ 'openbao',
111
+ 'keycloak',
112
+ 'external-dns',
113
+ 'redis',
114
+ 'envoy',
115
+ 'prometheus',
116
+ 'clickhouse',
117
+ 'logging-operator',
118
+ 'loki',
119
+ ];
120
+ async function initProject(projectName, template, strategy = 'github-actions', operators) {
121
+ const projectDir = (0, path_1.resolve)(projectName);
122
+ if ((0, fs_1.existsSync)(projectDir)) {
123
+ throw new Error(`Directory ${projectName} already exists`);
124
+ }
125
+ // Validate operators if provided
126
+ if (operators && operators.length > 0) {
127
+ const invalid = operators.filter((op) => !VALID_OPERATORS.includes(op));
128
+ if (invalid.length > 0) {
129
+ throw new Error(`Invalid operators: ${invalid.join(', ')}. ` +
130
+ `Valid operators are: ${VALID_OPERATORS.join(', ')}`);
131
+ }
132
+ }
133
+ console.log(`Creating r8s project: ${projectName}`);
134
+ console.log(`Deployment strategy: ${strategy}`);
135
+ // Create directory structure
136
+ (0, fs_1.mkdirSync)((0, path_1.join)(projectDir, 'k8s'), { recursive: true });
137
+ // Create package.json
138
+ const dependencies = {
139
+ '@r8s/core': '^0.1.0',
140
+ '@r8s/recipes': '^0.1.0',
141
+ };
142
+ // Add operator packages if requested
143
+ if (operators) {
144
+ for (const op of operators) {
145
+ dependencies[`@r8s/${op}`] = '^0.1.0';
146
+ }
147
+ }
148
+ const scripts = {};
149
+ if (strategy === 'github-actions') {
150
+ scripts['render-k8s'] = 'r8s render';
151
+ }
152
+ const packageJson = {
153
+ name: projectName,
154
+ version: '0.1.0',
155
+ private: true,
156
+ scripts,
157
+ dependencies,
158
+ devDependencies: {
159
+ '@r8s/cli': '^0.1.0',
160
+ typescript: '^5.3.0',
161
+ },
162
+ };
163
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
164
+ // Create tsconfig.json
165
+ const tsConfig = {
166
+ compilerOptions: {
167
+ target: 'ES2022',
168
+ module: 'NodeNext',
169
+ moduleResolution: 'NodeNext',
170
+ jsx: 'react-jsx',
171
+ jsxImportSource: '@r8s/core',
172
+ strict: true,
173
+ esModuleInterop: true,
174
+ skipLibCheck: true,
175
+ forceConsistentCasingInFileNames: true,
176
+ },
177
+ include: ['k8s/**/*'],
178
+ };
179
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2) + '\n', 'utf-8');
180
+ // Create k8s/r8s.tsx based on template
181
+ let r8sContent;
182
+ if (template === 'fullstack') {
183
+ r8sContent = generateFullstackTemplate(strategy);
184
+ }
185
+ else {
186
+ r8sContent = generateBasicTemplate(strategy);
187
+ }
188
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'k8s', 'r8s.tsx'), r8sContent, 'utf-8');
189
+ // Create .gitignore
190
+ const gitignore = strategy === 'github-actions'
191
+ ? `node_modules/
192
+ dist/
193
+ # Ignore rendered manifests except in k8s directory
194
+ *.yaml
195
+ !k8s/*.yaml
196
+ !.github/
197
+ `
198
+ : `node_modules/
199
+ dist/
200
+ # Keep .tsx files, Flux renders them in-cluster
201
+ *.yaml
202
+ !k8s/*.yaml
203
+ !.github/
204
+ !flux/
205
+ `;
206
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, '.gitignore'), gitignore, 'utf-8');
207
+ // Create deployment strategy files
208
+ if (strategy === 'github-actions') {
209
+ createGitHubActionsWorkflow(projectDir);
210
+ }
211
+ else {
212
+ createFluxControllerFiles(projectDir, projectName);
213
+ }
214
+ // Create README.md
215
+ const readme = generateReadme(projectName, strategy);
216
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'README.md'), readme, 'utf-8');
217
+ console.log(`\n✅ Project created: ${projectName}`);
218
+ console.log(`\nDeployment strategy: ${strategy}`);
219
+ if (strategy === 'github-actions') {
220
+ console.log(`\nNext steps:`);
221
+ console.log(` cd ${projectName}`);
222
+ console.log(` npm install`);
223
+ console.log(` npm run render-k8s`);
224
+ console.log(`\nGitHub Actions will auto-render on push to main.`);
225
+ }
226
+ else {
227
+ console.log(`\nNext steps:`);
228
+ console.log(` cd ${projectName}`);
229
+ console.log(` npm install`);
230
+ console.log(` git init && git add . && git commit -m "init"`);
231
+ console.log(` # Push to a Git repository`);
232
+ console.log(` # Configure FluxCD to point to your repo`);
233
+ console.log(`\nFluxCD will render .tsx files in-cluster.`);
234
+ console.log(`See flux/ directory for example manifests.`);
235
+ }
236
+ }
237
+ function generateBasicTemplate(strategy) {
238
+ const fluxComment = strategy === 'flux-controller'
239
+ ? `// This file stays as .tsx - FluxCD renders it in-cluster via r8s-controller\n`
240
+ : '';
241
+ return `${fluxComment}import { App } from '@r8s/recipes';
242
+
243
+ export default () => (
244
+ <App
245
+ name="myapp"
246
+ image="myapp/web:v1.2.3"
247
+ host="myapp.example.com"
248
+ />
249
+ );
250
+ `;
251
+ }
252
+ function generateFullstackTemplate(strategy) {
253
+ const fluxComment = strategy === 'flux-controller'
254
+ ? `// This file stays as .tsx - FluxCD renders it in-cluster via r8s-controller\n`
255
+ : '';
256
+ return `${fluxComment}import { App, Database } from '@r8s/recipes';
257
+
258
+ export default () => (
259
+ <>
260
+ <Database
261
+ name="app-db"
262
+ namespace="production"
263
+ storage="10Gi"
264
+ />
265
+
266
+ <App
267
+ name="api"
268
+ namespace="production"
269
+ image="myapp/api:v1.2.3"
270
+ port={3000}
271
+ host="api.example.com"
272
+ replicas={3}
273
+ tls={{ secretName: 'api-tls', clusterIssuer: 'letsencrypt' }}
274
+ env={{ LOG_LEVEL: 'info' }}
275
+ secrets={{ DATABASE_URL: 'api-secrets' }}
276
+ />
277
+
278
+ <App
279
+ name="frontend"
280
+ namespace="production"
281
+ image="myapp/frontend:v1.2.3"
282
+ port={80}
283
+ host="app.example.com"
284
+ replicas={2}
285
+ tls={{ secretName: 'app-tls', clusterIssuer: 'letsencrypt' }}
286
+ />
287
+ </>
288
+ );
289
+ `;
290
+ }
291
+ function createGitHubActionsWorkflow(projectDir) {
292
+ (0, fs_1.mkdirSync)((0, path_1.join)(projectDir, '.github', 'workflows'), { recursive: true });
293
+ const workflowContent = `name: Render Kubernetes Manifests
294
+
295
+ on:
296
+ push:
297
+ branches: [main, master]
298
+ paths:
299
+ - 'k8s/**'
300
+ - 'package.json'
301
+ - 'package-lock.json'
302
+ pull_request:
303
+ branches: [main, master]
304
+ paths:
305
+ - 'k8s/**'
306
+
307
+ jobs:
308
+ render:
309
+ runs-on: ubuntu-latest
310
+ permissions:
311
+ contents: write
312
+
313
+ steps:
314
+ - name: Checkout repository
315
+ uses: actions/checkout@v4
316
+
317
+ - name: Setup Node.js
318
+ uses: actions/setup-node@v4
319
+ with:
320
+ node-version: '20'
321
+ cache: 'npm'
322
+
323
+ - name: Install dependencies
324
+ run: npm ci
325
+
326
+ - name: Render Kubernetes manifests
327
+ run: npx r8s render --out k8s/manifest.yaml
328
+
329
+ - name: Check for changes
330
+ id: git-check
331
+ run: |
332
+ git diff --quiet k8s/manifest.yaml || echo "changed=true" >> \$GITHUB_OUTPUT
333
+
334
+ - name: Commit rendered manifests
335
+ if: steps.git-check.outputs.changed == 'true' && github.event_name == 'push'
336
+ run: |
337
+ git config --local user.email "action@github.com"
338
+ git config --local user.name "GitHub Action"
339
+ git add k8s/manifest.yaml
340
+ git commit -m "chore: render kubernetes manifests [skip ci]"
341
+ git push
342
+ `;
343
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, '.github', 'workflows', 'render.yaml'), workflowContent, 'utf-8');
344
+ }
345
+ function createFluxControllerFiles(projectDir, projectName) {
346
+ // Create flux/ directory with example manifests
347
+ (0, fs_1.mkdirSync)((0, path_1.join)(projectDir, 'flux'), { recursive: true });
348
+ const gitRepository = `apiVersion: source.toolkit.fluxcd.io/v1
349
+ kind: GitRepository
350
+ metadata:
351
+ name: ${projectName}
352
+ namespace: flux-system
353
+ spec:
354
+ interval: 1m
355
+ url: https://github.com/your-org/${projectName}
356
+ ref:
357
+ branch: main
358
+ ---
359
+ apiVersion: kustomize.toolkit.fluxcd.io/v1
360
+ kind: Kustomization
361
+ metadata:
362
+ name: ${projectName}
363
+ namespace: flux-system
364
+ spec:
365
+ interval: 10m
366
+ path: ./k8s/rendered
367
+ prune: true
368
+ sourceRef:
369
+ kind: GitRepository
370
+ name: ${projectName}
371
+ `;
372
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'flux', 'gitrepository.yaml'), gitRepository, 'utf-8');
373
+ const webhook = `apiVersion: notification.toolkit.fluxcd.io/v1
374
+ kind: Receiver
375
+ metadata:
376
+ name: ${projectName}-webhook
377
+ namespace: flux-system
378
+ spec:
379
+ type: github
380
+ events:
381
+ - ping
382
+ - push
383
+ secretRef:
384
+ name: ${projectName}-webhook-token
385
+ resources:
386
+ - apiVersion: source.toolkit.fluxcd.io/v1
387
+ kind: GitRepository
388
+ name: ${projectName}
389
+ namespace: flux-system
390
+ ---
391
+ apiVersion: v1
392
+ kind: Secret
393
+ metadata:
394
+ name: ${projectName}-webhook-token
395
+ namespace: flux-system
396
+ type: Opaque
397
+ stringData:
398
+ token: "replace-me-with-20-char-random-string"
399
+ `;
400
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'flux', 'webhook.yaml'), webhook, 'utf-8');
401
+ const readme = `# FluxCD Setup for ${projectName}
402
+
403
+ ## Prerequisites
404
+
405
+ 1. FluxCD installed on your cluster
406
+ 2. r8s-controller image available (or build your own)
407
+
408
+ ## Setup
409
+
410
+ ### 1. Configure FluxCD
411
+
412
+ Apply the manifests in this directory:
413
+
414
+ \`\`\`bash
415
+ kubectl apply -f flux/gitrepository.yaml
416
+ kubectl apply -f flux/webhook.yaml
417
+ \`\`\`
418
+
419
+ ### 2. Configure GitHub Webhook
420
+
421
+ 1. Go to your repository → Settings → Webhooks
422
+ 2. Add webhook:
423
+ - Payload URL: \`https://flux-webhook.yourdomain.com/hook/flux-system/${projectName}-webhook\`
424
+ - Content type: \`application/json\`
425
+ - Secret: (the token from flux/webhook.yaml)
426
+ - Events: Push
427
+
428
+ ### 3. Configure r8s-controller
429
+
430
+ The r8s-controller runs as an init container in the Flux source-controller.
431
+
432
+ See https://github.com/berget-ai/r8s/tree/main/packages/flux-controller for setup instructions.
433
+
434
+ ## How It Works
435
+
436
+ 1. You push .tsx files to git
437
+ 2. GitHub webhook triggers Flux reconciliation
438
+ 3. Flux clones repo to /data
439
+ 4. r8s-controller renders TSX → YAML to /data/rendered
440
+ 5. Flux Kustomization applies rendered YAML
441
+
442
+ ## Local Development
443
+
444
+ \`\`\`bash
445
+ # Render locally for testing
446
+ npm install
447
+ npx r8s render --out k8s/manifest.yaml
448
+ \`\`\`
449
+ `;
450
+ (0, fs_1.writeFileSync)((0, path_1.join)(projectDir, 'flux', 'README.md'), readme, 'utf-8');
451
+ }
452
+ function generateReadme(projectName, strategy) {
453
+ if (strategy === 'github-actions') {
454
+ return `# ${projectName}
455
+
456
+ Generated with r8s (GitHub Actions strategy).
457
+
458
+ ## Getting Started
459
+
460
+ \`\`\`bash
461
+ # Install dependencies
462
+ npm install
463
+
464
+ # Render Kubernetes manifests locally
465
+ npm run render-k8s
466
+
467
+ # Or use the CLI directly
468
+ npx r8s render
469
+ npx r8s render --out k8s/manifest.yaml
470
+ \`\`\`
471
+
472
+ ## Project Structure
473
+
474
+ \`\`\`
475
+ .
476
+ ├── .github/
477
+ │ └── workflows/
478
+ │ └── render.yaml # Auto-render on push
479
+ ├── k8s/
480
+ │ ├── r8s.tsx # Your Kubernetes components
481
+ │ └── manifest.yaml # Generated YAML (auto-committed)
482
+ ├── package.json
483
+ └── tsconfig.json
484
+ \`\`\`
485
+
486
+ ## Deployment Strategy: GitHub Actions
487
+
488
+ This project uses **GitHub Actions** to render TSX → YAML:
489
+
490
+ 1. You edit \`k8s/r8s.tsx\` and push to \`main\`
491
+ 2. GitHub Actions renders the TSX to \`k8s/manifest.yaml\`
492
+ 3. The rendered YAML is committed back to the repository
493
+ 4. Your GitOps tool (Flux, ArgoCD) picks up the YAML and applies it
494
+
495
+ ## Learn More
496
+
497
+ - [r8s Documentation](https://github.com/berget-ai/r8s)
498
+ - [FluxCD Integration](https://github.com/berget-ai/r8s/tree/main/packages/flux-controller)
499
+ `;
500
+ }
501
+ else {
502
+ return `# ${projectName}
503
+
504
+ Generated with r8s (Flux Controller strategy).
505
+
506
+ ## Getting Started
507
+
508
+ \`\`\`bash
509
+ # Install dependencies
510
+ npm install
511
+
512
+ # Render locally for testing
513
+ npx r8s render --out k8s/manifest.yaml
514
+ \`\`\`
515
+
516
+ ## Project Structure
517
+
518
+ \`\`\`
519
+ .
520
+ ├── flux/
521
+ │ ├── gitrepository.yaml # Flux GitRepository manifest
522
+ │ ├── webhook.yaml # Webhook receiver for instant sync
523
+ │ └── README.md # Flux setup instructions
524
+ ├── k8s/
525
+ │ └── r8s.tsx # Your Kubernetes components (kept as .tsx)
526
+ ├── package.json
527
+ └── tsconfig.json
528
+ \`\`\`
529
+
530
+ ## Deployment Strategy: Flux Controller
531
+
532
+ This project uses **FluxCD with r8s-controller** to render TSX → YAML in-cluster:
533
+
534
+ 1. You edit \`k8s/r8s.tsx\` and push to \`main\`
535
+ 2. GitHub webhook triggers Flux reconciliation (instant)
536
+ 3. Flux clones repo to /data
537
+ 4. r8s-controller (init container) renders TSX → YAML to /data/rendered
538
+ 5. Flux Kustomization applies rendered YAML to cluster
539
+
540
+ ## Benefits
541
+
542
+ - **No CI build step** — rendering happens in-cluster
543
+ - **Git is source of truth** — only .tsx files in repo
544
+ - **Instant updates** — webhook triggers reconciliation immediately
545
+ - **Type safety** — catch errors at build time
546
+
547
+ ## Setup
548
+
549
+ See \`flux/README.md\` for detailed setup instructions.
550
+
551
+ ## Learn More
552
+
553
+ - [r8s Documentation](https://github.com/berget-ai/r8s)
554
+ - [FluxCD Controller](https://github.com/berget-ai/r8s/tree/main/packages/flux-controller)
555
+ - [FluxCD Webhooks](https://github.com/berget-ai/r8s/tree/main/packages/flux-controller/WEBHOOKS.md)
556
+ `;
557
+ }
558
+ }
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);
565
+ }
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;
737
+ }
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;
758
+ }
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');
767
+ const comp = findComponent(name);
768
+ if (!comp) {
769
+ console.error(`Component not found: ${name}`);
770
+ console.error('Use "r8s list" to see available components.');
771
+ process.exit(1);
772
+ }
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;
807
+ }
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);
859
+ }
860
+ finally {
861
+ try {
862
+ require('fs').unlinkSync(tmpFile);
863
+ }
864
+ catch { }
865
+ }
866
+ return;
867
+ }
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');
963
+ }
964
+ }
965
+ catch (error) {
966
+ console.error('❌ Render failed:', error instanceof Error ? error.message : error);
967
+ process.exit(1);
968
+ }
969
+ return;
970
+ }
971
+ if (command !== 'render') {
972
+ console.error(`Unknown command: ${command}`);
973
+ showHelp();
974
+ process.exit(1);
975
+ }
976
+ try {
977
+ const entryFile = await findEntryFile(options.entry);
978
+ console.error(`Rendering: ${entryFile}`);
979
+ const { renderToYaml } = await import('./renderer.js');
980
+ const yamlOutput = await renderToYaml(entryFile, {
981
+ includeOperators: options.includeOperators,
982
+ operatorsOnly: options.operatorsOnly,
983
+ });
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
+ }
994
+ }
995
+ catch (error) {
996
+ console.error('Error:', error instanceof Error ? error.message : error);
997
+ process.exit(1);
998
+ }
999
+ }
1000
+ main();
1001
+ //# sourceMappingURL=cli.js.map