genbox 1.0.12 → 1.0.13

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.
@@ -0,0 +1,529 @@
1
+ "use strict";
2
+ /**
3
+ * Validate Command
4
+ *
5
+ * Validates configuration files and checks for issues:
6
+ * 1. Schema validation (required fields, correct types)
7
+ * 2. Reference validation (apps exist, profiles reference valid apps)
8
+ * 3. Dependency validation (connected services exist)
9
+ * 4. Detection warnings (implicit values that should be explicit)
10
+ *
11
+ * Usage:
12
+ * genbox validate # Validate genbox.yaml
13
+ * genbox validate --resolved # Validate resolved.yaml
14
+ * genbox validate --strict # Fail on warnings
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ var __importDefault = (this && this.__importDefault) || function (mod) {
50
+ return (mod && mod.__esModule) ? mod : { "default": mod };
51
+ };
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.validateCommand = void 0;
54
+ const commander_1 = require("commander");
55
+ const chalk_1 = __importDefault(require("chalk"));
56
+ const fs = __importStar(require("fs"));
57
+ const path = __importStar(require("path"));
58
+ const yaml = __importStar(require("js-yaml"));
59
+ const config_loader_1 = require("../config-loader");
60
+ const config_explainer_1 = require("../config-explainer");
61
+ const strict_mode_1 = require("../strict-mode");
62
+ exports.validateCommand = new commander_1.Command('validate')
63
+ .description('Validate configuration files for errors and warnings')
64
+ .option('--resolved', 'Validate resolved.yaml instead of genbox.yaml')
65
+ .option('--strict', 'Treat warnings as errors')
66
+ .option('--json', 'Output as JSON')
67
+ .option('-q, --quiet', 'Only output errors')
68
+ .action(async (options) => {
69
+ const cwd = process.cwd();
70
+ if (!options.quiet) {
71
+ console.log(chalk_1.default.cyan('\nšŸ” Validating configuration...\n'));
72
+ }
73
+ try {
74
+ const result = options.resolved
75
+ ? await validateResolvedConfig(cwd)
76
+ : await validateGenboxConfig(cwd);
77
+ // Add detection warnings
78
+ if (!options.resolved) {
79
+ const detectionWarnings = await getDetectionWarnings(cwd);
80
+ result.warnings.push(...detectionWarnings);
81
+ // Add strict mode violations
82
+ const configLoader = new config_loader_1.ConfigLoader();
83
+ const loadResult = await configLoader.load(cwd);
84
+ if (loadResult.config) {
85
+ const strictSettings = (0, strict_mode_1.getStrictModeSettings)(loadResult.config);
86
+ const strictResult = (0, strict_mode_1.validateStrictMode)(loadResult.config, {
87
+ ...strictSettings,
88
+ // CLI --strict flag overrides config
89
+ warnings_as_errors: options.strict || strictSettings.warnings_as_errors,
90
+ });
91
+ // Convert strict violations to validation issues
92
+ for (const violation of strictResult.violations) {
93
+ const issue = {
94
+ path: violation.path,
95
+ message: `[Strict] ${violation.message}`,
96
+ severity: violation.severity,
97
+ suggestion: violation.suggestion,
98
+ };
99
+ if (violation.severity === 'error') {
100
+ result.errors.push(issue);
101
+ }
102
+ else {
103
+ result.warnings.push(issue);
104
+ }
105
+ }
106
+ // Show strict mode summary if violations exist
107
+ if (!options.quiet && strictResult.violations.length > 0) {
108
+ console.log(chalk_1.default.bold('Strict Mode Analysis:'));
109
+ console.log(` Explicit values: ${strictResult.summary.explicit_values}`);
110
+ console.log(` $detect markers: ${strictResult.summary.detect_markers}`);
111
+ console.log(` Inferred values: ${chalk_1.default.yellow(String(strictResult.summary.inferred_values))}`);
112
+ console.log();
113
+ }
114
+ }
115
+ }
116
+ // In strict mode, warnings become errors
117
+ if (options.strict) {
118
+ result.errors.push(...result.warnings.map(w => ({ ...w, severity: 'error' })));
119
+ result.warnings = [];
120
+ result.valid = result.errors.length === 0;
121
+ }
122
+ // Output
123
+ if (options.json) {
124
+ console.log(JSON.stringify(result, null, 2));
125
+ }
126
+ else {
127
+ displayValidationResult(result, options.quiet);
128
+ }
129
+ if (!result.valid) {
130
+ process.exit(1);
131
+ }
132
+ }
133
+ catch (error) {
134
+ if (options.json) {
135
+ console.log(JSON.stringify({
136
+ valid: false,
137
+ errors: [{ path: '', message: String(error), severity: 'error' }],
138
+ warnings: [],
139
+ }, null, 2));
140
+ }
141
+ else {
142
+ console.error(chalk_1.default.red('Validation failed:'), error);
143
+ }
144
+ process.exit(1);
145
+ }
146
+ });
147
+ async function validateGenboxConfig(cwd) {
148
+ const errors = [];
149
+ const warnings = [];
150
+ // Load configuration
151
+ const configLoader = new config_loader_1.ConfigLoader();
152
+ const loadResult = await configLoader.load(cwd);
153
+ if (!loadResult.found || !loadResult.config) {
154
+ errors.push({
155
+ path: 'genbox.yaml',
156
+ message: 'No genbox.yaml found. Run "genbox init" first.',
157
+ severity: 'error',
158
+ });
159
+ return { valid: false, errors, warnings };
160
+ }
161
+ const config = loadResult.config;
162
+ // Schema validation
163
+ validateSchema(config, errors, warnings);
164
+ // Reference validation
165
+ validateReferences(config, errors, warnings);
166
+ // Profile validation
167
+ validateProfiles(config, errors, warnings);
168
+ // Dependency validation
169
+ validateDependencies(config, errors, warnings);
170
+ return {
171
+ valid: errors.length === 0,
172
+ errors,
173
+ warnings,
174
+ };
175
+ }
176
+ async function validateResolvedConfig(cwd) {
177
+ const errors = [];
178
+ const warnings = [];
179
+ const resolvedPath = path.join(cwd, '.genbox', 'resolved.yaml');
180
+ if (!fs.existsSync(resolvedPath)) {
181
+ errors.push({
182
+ path: '.genbox/resolved.yaml',
183
+ message: 'No resolved.yaml found. Run "genbox resolve" first.',
184
+ severity: 'error',
185
+ });
186
+ return { valid: false, errors, warnings };
187
+ }
188
+ try {
189
+ const content = fs.readFileSync(resolvedPath, 'utf8');
190
+ const resolved = yaml.load(content);
191
+ // Check for unresolved $detect markers
192
+ checkUnresolvedDetectMarkers(resolved, '', errors);
193
+ // Validate resolved structure
194
+ if (!resolved.name) {
195
+ errors.push({
196
+ path: 'name',
197
+ message: 'Resolved config missing required field: name',
198
+ severity: 'error',
199
+ });
200
+ }
201
+ if (!resolved.apps || !Array.isArray(resolved.apps)) {
202
+ errors.push({
203
+ path: 'apps',
204
+ message: 'Resolved config missing apps array',
205
+ severity: 'error',
206
+ });
207
+ }
208
+ // Check each app has required fields
209
+ if (Array.isArray(resolved.apps)) {
210
+ for (const app of resolved.apps) {
211
+ if (!app.name) {
212
+ errors.push({
213
+ path: 'apps[?]',
214
+ message: 'App missing required field: name',
215
+ severity: 'error',
216
+ });
217
+ }
218
+ if (!app.type) {
219
+ warnings.push({
220
+ path: `apps.${app.name || '?'}.type`,
221
+ message: 'App has no type specified',
222
+ severity: 'warning',
223
+ suggestion: 'Add explicit type: frontend | backend | worker | gateway',
224
+ });
225
+ }
226
+ }
227
+ }
228
+ }
229
+ catch (err) {
230
+ errors.push({
231
+ path: '.genbox/resolved.yaml',
232
+ message: `Failed to parse resolved.yaml: ${err}`,
233
+ severity: 'error',
234
+ });
235
+ }
236
+ return {
237
+ valid: errors.length === 0,
238
+ errors,
239
+ warnings,
240
+ };
241
+ }
242
+ function validateSchema(config, errors, warnings) {
243
+ // Required fields
244
+ if (!config.version) {
245
+ errors.push({
246
+ path: 'version',
247
+ message: 'Missing required field: version',
248
+ severity: 'error',
249
+ suggestion: 'Add "version: 3" at the top of genbox.yaml',
250
+ });
251
+ }
252
+ else if (config.version !== '3.0' && config.version !== 3 && config.version !== 4) {
253
+ warnings.push({
254
+ path: 'version',
255
+ message: `Version ${config.version} may not be fully supported`,
256
+ severity: 'warning',
257
+ suggestion: 'Consider upgrading to version 3 or 4',
258
+ });
259
+ }
260
+ if (!config.project?.name) {
261
+ errors.push({
262
+ path: 'project.name',
263
+ message: 'Missing required field: project.name',
264
+ severity: 'error',
265
+ });
266
+ }
267
+ // Apps validation
268
+ if (!config.apps || Object.keys(config.apps).length === 0) {
269
+ warnings.push({
270
+ path: 'apps',
271
+ message: 'No apps defined in configuration',
272
+ severity: 'warning',
273
+ suggestion: 'Define at least one app in the apps section',
274
+ });
275
+ }
276
+ // Validate each app
277
+ for (const [appName, appConfig] of Object.entries(config.apps || {})) {
278
+ if (!appConfig.path) {
279
+ errors.push({
280
+ path: `apps.${appName}.path`,
281
+ message: `App '${appName}' missing required field: path`,
282
+ severity: 'error',
283
+ });
284
+ }
285
+ // Check for $detect markers
286
+ if (appConfig.type === '$detect') {
287
+ warnings.push({
288
+ path: `apps.${appName}.type`,
289
+ message: `App '${appName}' uses $detect for type - run "genbox resolve" to compute actual value`,
290
+ severity: 'warning',
291
+ });
292
+ }
293
+ if (appConfig.port === '$detect') {
294
+ warnings.push({
295
+ path: `apps.${appName}.port`,
296
+ message: `App '${appName}' uses $detect for port - run "genbox resolve" to compute actual value`,
297
+ severity: 'warning',
298
+ });
299
+ }
300
+ }
301
+ }
302
+ function validateReferences(config, errors, _warnings) {
303
+ const appNames = new Set(Object.keys(config.apps || {}));
304
+ // Validate profile app references
305
+ for (const [profileName, profile] of Object.entries(config.profiles || {})) {
306
+ if (profile.apps) {
307
+ for (const appName of profile.apps) {
308
+ if (!appNames.has(appName)) {
309
+ errors.push({
310
+ path: `profiles.${profileName}.apps`,
311
+ message: `Profile '${profileName}' references non-existent app: ${appName}`,
312
+ severity: 'error',
313
+ suggestion: `Available apps: ${Array.from(appNames).join(', ')}`,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ // Validate connect_to references
319
+ if (profile.connect_to) {
320
+ for (const [appName, connections] of Object.entries(profile.connect_to)) {
321
+ if (!appNames.has(appName)) {
322
+ errors.push({
323
+ path: `profiles.${profileName}.connect_to.${appName}`,
324
+ message: `Profile '${profileName}' connect_to references non-existent app: ${appName}`,
325
+ severity: 'error',
326
+ });
327
+ }
328
+ // Validate connection targets
329
+ for (const [targetName] of Object.entries(connections || {})) {
330
+ // Infrastructure names are in the infrastructure section (it's a Record, not array)
331
+ const infraNames = new Set(Object.keys(config.infrastructure || {}));
332
+ if (!appNames.has(targetName) && !infraNames.has(targetName)) {
333
+ errors.push({
334
+ path: `profiles.${profileName}.connect_to.${appName}.${targetName}`,
335
+ message: `Connection target '${targetName}' not found in apps or infrastructure`,
336
+ severity: 'error',
337
+ });
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ // Validate repos references (repos is a Record, not array)
344
+ for (const [repoName, repo] of Object.entries(config.repos || {})) {
345
+ // RepoConfig doesn't have apps field - skip this validation for now
346
+ // The apps are defined in the apps section with repo references
347
+ }
348
+ }
349
+ function validateProfiles(config, errors, warnings) {
350
+ const profiles = config.profiles || {};
351
+ // Check for recommended profiles
352
+ if (!profiles['quick'] && !profiles['minimal']) {
353
+ warnings.push({
354
+ path: 'profiles',
355
+ message: 'No quick/minimal profile defined for fast iteration',
356
+ severity: 'warning',
357
+ suggestion: 'Add a "quick" profile with minimal services for fast local development',
358
+ });
359
+ }
360
+ // Validate profile sizes
361
+ const validSizes = ['small', 'medium', 'large', 'xlarge'];
362
+ for (const [profileName, profile] of Object.entries(profiles)) {
363
+ if (profile.size && !validSizes.includes(profile.size)) {
364
+ errors.push({
365
+ path: `profiles.${profileName}.size`,
366
+ message: `Invalid size '${profile.size}' in profile '${profileName}'`,
367
+ severity: 'error',
368
+ suggestion: `Valid sizes: ${validSizes.join(', ')}`,
369
+ });
370
+ }
371
+ }
372
+ }
373
+ function validateDependencies(config, errors, warnings) {
374
+ const appNames = new Set(Object.keys(config.apps || {}));
375
+ const infraNames = new Set(Object.keys(config.infrastructure || {}));
376
+ // Check app dependencies
377
+ for (const [appName, appConfig] of Object.entries(config.apps || {})) {
378
+ if (appConfig.dependencies) {
379
+ for (const depName of Object.keys(appConfig.dependencies)) {
380
+ if (!infraNames.has(depName) && !appNames.has(depName)) {
381
+ errors.push({
382
+ path: `apps.${appName}.dependencies.${depName}`,
383
+ message: `Dependency '${depName}' not found in infrastructure or apps`,
384
+ severity: 'error',
385
+ suggestion: `Define '${depName}' in the infrastructure section or check the name`,
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ // Check for circular dependencies
392
+ const appDeps = new Map();
393
+ for (const [appName, appConfig] of Object.entries(config.apps || {})) {
394
+ const deps = Object.keys(appConfig.dependencies || {}).filter(d => appNames.has(d));
395
+ appDeps.set(appName, deps);
396
+ }
397
+ for (const appName of appNames) {
398
+ const cycle = findCycle(appName, appDeps);
399
+ if (cycle) {
400
+ errors.push({
401
+ path: `apps.${appName}.dependencies`,
402
+ message: `Circular dependency detected: ${cycle.join(' → ')}`,
403
+ severity: 'error',
404
+ });
405
+ break; // Only report once
406
+ }
407
+ }
408
+ // Warn about apps with no dependencies defined
409
+ for (const [appName, appConfig] of Object.entries(config.apps || {})) {
410
+ if (!appConfig.dependencies || Object.keys(appConfig.dependencies).length === 0) {
411
+ if (appConfig.type === 'backend' || appConfig.type === 'worker') {
412
+ warnings.push({
413
+ path: `apps.${appName}.dependencies`,
414
+ message: `Backend app '${appName}' has no dependencies defined`,
415
+ severity: 'warning',
416
+ suggestion: 'Consider defining database or cache dependencies',
417
+ });
418
+ }
419
+ }
420
+ }
421
+ }
422
+ function findCycle(start, deps, visited = new Set(), path = []) {
423
+ if (visited.has(start)) {
424
+ const cycleStart = path.indexOf(start);
425
+ if (cycleStart !== -1) {
426
+ return [...path.slice(cycleStart), start];
427
+ }
428
+ return null;
429
+ }
430
+ visited.add(start);
431
+ path.push(start);
432
+ for (const dep of deps.get(start) || []) {
433
+ const cycle = findCycle(dep, deps, visited, path);
434
+ if (cycle)
435
+ return cycle;
436
+ }
437
+ path.pop();
438
+ return null;
439
+ }
440
+ function checkUnresolvedDetectMarkers(obj, currentPath, errors) {
441
+ if (obj === '$detect') {
442
+ errors.push({
443
+ path: currentPath,
444
+ message: `Unresolved $detect marker at ${currentPath}`,
445
+ severity: 'error',
446
+ suggestion: 'Run "genbox scan" then "genbox resolve" to compute this value',
447
+ });
448
+ return;
449
+ }
450
+ if (obj && typeof obj === 'object') {
451
+ if (Array.isArray(obj)) {
452
+ obj.forEach((item, index) => {
453
+ checkUnresolvedDetectMarkers(item, `${currentPath}[${index}]`, errors);
454
+ });
455
+ }
456
+ else {
457
+ for (const [key, value] of Object.entries(obj)) {
458
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
459
+ checkUnresolvedDetectMarkers(value, newPath, errors);
460
+ }
461
+ }
462
+ }
463
+ }
464
+ async function getDetectionWarnings(cwd) {
465
+ const warnings = [];
466
+ try {
467
+ const configLoader = new config_loader_1.ConfigLoader();
468
+ const loadResult = await configLoader.load(cwd);
469
+ if (!loadResult.found || !loadResult.config) {
470
+ return warnings;
471
+ }
472
+ const explainer = new config_explainer_1.ConfigExplainer(loadResult);
473
+ const detectionWarnings = explainer.getDetectionWarnings();
474
+ for (const warning of detectionWarnings) {
475
+ warnings.push({
476
+ path: 'detection',
477
+ message: warning,
478
+ severity: 'warning',
479
+ suggestion: 'Make this value explicit in genbox.yaml',
480
+ });
481
+ }
482
+ }
483
+ catch {
484
+ // Ignore errors in detection warnings
485
+ }
486
+ return warnings;
487
+ }
488
+ function displayValidationResult(result, quiet) {
489
+ const { errors, warnings, valid } = result;
490
+ // Display errors
491
+ if (errors.length > 0) {
492
+ console.log(chalk_1.default.red.bold(`\nāŒ ${errors.length} Error(s):\n`));
493
+ for (const error of errors) {
494
+ console.log(chalk_1.default.red(` • ${error.path}: ${error.message}`));
495
+ if (error.suggestion) {
496
+ console.log(chalk_1.default.dim(` → ${error.suggestion}`));
497
+ }
498
+ }
499
+ }
500
+ // Display warnings (unless quiet)
501
+ if (!quiet && warnings.length > 0) {
502
+ console.log(chalk_1.default.yellow.bold(`\nāš ļø ${warnings.length} Warning(s):\n`));
503
+ for (const warning of warnings) {
504
+ console.log(chalk_1.default.yellow(` • ${warning.path}: ${warning.message}`));
505
+ if (warning.suggestion) {
506
+ console.log(chalk_1.default.dim(` → ${warning.suggestion}`));
507
+ }
508
+ }
509
+ }
510
+ // Summary
511
+ console.log();
512
+ if (valid) {
513
+ console.log(chalk_1.default.green.bold('āœ“ Configuration is valid'));
514
+ if (warnings.length > 0) {
515
+ console.log(chalk_1.default.yellow(` (${warnings.length} warning(s) - consider addressing them)`));
516
+ }
517
+ }
518
+ else {
519
+ console.log(chalk_1.default.red.bold('āœ— Configuration has errors'));
520
+ console.log(chalk_1.default.dim(' Fix the errors above and run validate again'));
521
+ }
522
+ // Next steps
523
+ if (valid) {
524
+ console.log(chalk_1.default.bold('\nšŸ“ Next steps:\n'));
525
+ console.log(' 1. Run ' + chalk_1.default.cyan('genbox resolve') + ' to compute final configuration');
526
+ console.log(' 2. Run ' + chalk_1.default.cyan('genbox create <name>') + ' to provision');
527
+ console.log();
528
+ }
529
+ }