@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/catalog.d.ts +33 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +716 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1001 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +141 -0
- package/dist/renderer.js.map +1 -0
- package/package.json +40 -0
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
|