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,379 @@
1
+ "use strict";
2
+ /**
3
+ * Config Explainer
4
+ *
5
+ * Traces where each configuration value comes from through the resolution hierarchy:
6
+ * 1. CLI flags (highest priority)
7
+ * 2. Profile overrides
8
+ * 3. Project config (genbox.yaml explicit values)
9
+ * 4. Workspace config
10
+ * 5. Detected values (from scanner)
11
+ * 6. Schema defaults (lowest priority)
12
+ *
13
+ * Also identifies values that were auto-detected vs explicitly configured.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.ConfigExplainer = void 0;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const yaml = __importStar(require("js-yaml"));
53
+ // Schema defaults for known fields
54
+ const SCHEMA_DEFAULTS = {
55
+ 'defaults.size': 'medium',
56
+ 'defaults.branch': 'main',
57
+ 'defaults.database.mode': 'local',
58
+ };
59
+ // Fields that support $detect markers (in v4)
60
+ const DETECTABLE_FIELDS = [
61
+ 'apps.*.port',
62
+ 'apps.*.type',
63
+ 'apps.*.framework',
64
+ 'apps.*.commands.install',
65
+ 'apps.*.commands.dev',
66
+ 'apps.*.commands.build',
67
+ 'apps.*.commands.start',
68
+ ];
69
+ class ConfigExplainer {
70
+ loadResult;
71
+ detectedConfig = null;
72
+ cliOverrides = {};
73
+ constructor(loadResult) {
74
+ this.loadResult = loadResult;
75
+ this.loadDetectedConfig();
76
+ }
77
+ /**
78
+ * Set CLI overrides for explanation
79
+ */
80
+ setCliOverrides(overrides) {
81
+ this.cliOverrides = overrides;
82
+ }
83
+ /**
84
+ * Load detected configuration if available
85
+ */
86
+ loadDetectedConfig() {
87
+ const detectedPath = path.join(this.loadResult.root, '.genbox', 'detected.yaml');
88
+ if (fs.existsSync(detectedPath)) {
89
+ try {
90
+ const content = fs.readFileSync(detectedPath, 'utf8');
91
+ this.detectedConfig = yaml.load(content);
92
+ }
93
+ catch {
94
+ // Ignore parse errors
95
+ }
96
+ }
97
+ }
98
+ /**
99
+ * Explain where a configuration value comes from
100
+ */
101
+ explain(configPath, profileName) {
102
+ const sources = [];
103
+ // 1. Check CLI overrides
104
+ const cliValue = this.getNestedValue(this.cliOverrides, configPath);
105
+ if (cliValue !== undefined) {
106
+ sources.push({
107
+ source: 'cli',
108
+ value: cliValue,
109
+ used: true,
110
+ explicit: true,
111
+ });
112
+ }
113
+ // 2. Check profile overrides
114
+ if (profileName && this.loadResult.config?.profiles?.[profileName]) {
115
+ const profile = this.loadResult.config.profiles[profileName];
116
+ const profileValue = this.getProfileValue(profile, configPath);
117
+ if (profileValue !== undefined) {
118
+ sources.push({
119
+ source: 'profile',
120
+ value: profileValue,
121
+ used: sources.length === 0,
122
+ profileName,
123
+ explicit: true,
124
+ });
125
+ }
126
+ }
127
+ // 3. Check project config (explicit values)
128
+ const projectSource = this.loadResult.sources.find(s => s.type === 'project');
129
+ if (projectSource) {
130
+ const projectValue = this.getNestedValue(projectSource.config, configPath);
131
+ if (projectValue !== undefined) {
132
+ const isDetectMarker = projectValue === '$detect';
133
+ sources.push({
134
+ source: 'project',
135
+ value: projectValue,
136
+ used: sources.length === 0 && !isDetectMarker,
137
+ filePath: projectSource.path,
138
+ explicit: !isDetectMarker,
139
+ });
140
+ // If it's a $detect marker, add the detected value
141
+ if (isDetectMarker) {
142
+ const detected = this.getDetectedValue(configPath);
143
+ if (detected) {
144
+ sources.push({
145
+ source: 'detected',
146
+ value: detected.value,
147
+ used: sources.length === 1, // Used if project has $detect
148
+ detectedFrom: detected.source,
149
+ explicit: false,
150
+ });
151
+ }
152
+ }
153
+ }
154
+ }
155
+ // 4. Check workspace config
156
+ const workspaceSource = this.loadResult.sources.find(s => s.type === 'workspace');
157
+ if (workspaceSource) {
158
+ const workspaceValue = this.getNestedValue(workspaceSource.config, configPath);
159
+ if (workspaceValue !== undefined) {
160
+ sources.push({
161
+ source: 'workspace',
162
+ value: workspaceValue,
163
+ used: sources.length === 0,
164
+ filePath: workspaceSource.path,
165
+ explicit: true,
166
+ });
167
+ }
168
+ }
169
+ // 5. Check if value was inferred/detected (for apps without explicit values)
170
+ if (sources.length === 0 || sources.every(s => !s.used)) {
171
+ const inferred = this.getInferredValue(configPath);
172
+ if (inferred !== undefined) {
173
+ sources.push({
174
+ source: 'inferred',
175
+ value: inferred.value,
176
+ used: sources.length === 0,
177
+ detectedFrom: inferred.source,
178
+ explicit: false,
179
+ });
180
+ }
181
+ }
182
+ // 6. Check schema defaults
183
+ const defaultValue = SCHEMA_DEFAULTS[configPath];
184
+ if (defaultValue !== undefined) {
185
+ sources.push({
186
+ source: 'default',
187
+ value: defaultValue,
188
+ used: sources.length === 0,
189
+ explicit: false,
190
+ });
191
+ }
192
+ return sources;
193
+ }
194
+ /**
195
+ * Get all detection warnings
196
+ */
197
+ getDetectionWarnings() {
198
+ const warnings = [];
199
+ const config = this.loadResult.config;
200
+ if (!config)
201
+ return warnings;
202
+ // Check each app for implicit type/port/framework
203
+ for (const [appName, appConfig] of Object.entries(config.apps || {})) {
204
+ // Check type - was it inferred?
205
+ if (appConfig.type) {
206
+ const typeExplanation = this.explain(`apps.${appName}.type`);
207
+ const typeSource = typeExplanation.find(s => s.used);
208
+ if (typeSource && !typeSource.explicit) {
209
+ warnings.push(`App '${appName}' type '${appConfig.type}' was inferred from ${typeSource.detectedFrom || 'naming conventions'}. ` +
210
+ `Add explicit 'type: ${appConfig.type}' to silence this warning.`);
211
+ }
212
+ }
213
+ // Check port - was it extracted via regex?
214
+ if (appConfig.port) {
215
+ const portExplanation = this.explain(`apps.${appName}.port`);
216
+ const portSource = portExplanation.find(s => s.used);
217
+ if (portSource && !portSource.explicit) {
218
+ warnings.push(`App '${appName}' port ${appConfig.port} was extracted from ${portSource.detectedFrom || 'npm scripts'}. ` +
219
+ `Add explicit 'port: ${appConfig.port}' to ensure consistent behavior.`);
220
+ }
221
+ }
222
+ // Check framework - was it detected?
223
+ if (appConfig.framework) {
224
+ const frameworkExplanation = this.explain(`apps.${appName}.framework`);
225
+ const frameworkSource = frameworkExplanation.find(s => s.used);
226
+ if (frameworkSource && !frameworkSource.explicit) {
227
+ warnings.push(`App '${appName}' framework '${appConfig.framework}' was detected from ${frameworkSource.detectedFrom || 'dependencies'}. ` +
228
+ `Add explicit 'framework: ${appConfig.framework}' for clarity.`);
229
+ }
230
+ }
231
+ }
232
+ return warnings;
233
+ }
234
+ /**
235
+ * Get a nested value from an object using dot notation
236
+ */
237
+ getNestedValue(obj, path) {
238
+ if (!obj || typeof obj !== 'object')
239
+ return undefined;
240
+ const parts = path.split('.');
241
+ let current = obj;
242
+ for (const part of parts) {
243
+ if (current && typeof current === 'object' && part in current) {
244
+ current = current[part];
245
+ }
246
+ else {
247
+ return undefined;
248
+ }
249
+ }
250
+ return current;
251
+ }
252
+ /**
253
+ * Get value from a profile (handles profile-specific field names)
254
+ */
255
+ getProfileValue(profile, path) {
256
+ // Map common paths to profile fields
257
+ const mappings = {
258
+ 'size': 'size',
259
+ 'apps': 'apps',
260
+ 'connect_to': 'connect_to',
261
+ 'database.mode': 'database',
262
+ 'branch': 'branch',
263
+ };
264
+ const field = mappings[path];
265
+ if (field) {
266
+ if (path === 'database.mode' && profile.database) {
267
+ return profile.database.mode;
268
+ }
269
+ return profile[field];
270
+ }
271
+ return undefined;
272
+ }
273
+ /**
274
+ * Get detected value from detected.yaml
275
+ */
276
+ getDetectedValue(path) {
277
+ if (!this.detectedConfig)
278
+ return undefined;
279
+ // Map config paths to detected config structure
280
+ const match = path.match(/^apps\.(\w+)\.(\w+)$/);
281
+ if (match) {
282
+ const [, appName, field] = match;
283
+ const detectedApp = this.detectedConfig.apps?.[appName];
284
+ if (detectedApp) {
285
+ const value = detectedApp[field];
286
+ const sourceField = `${field}_source`;
287
+ const source = detectedApp[sourceField];
288
+ if (value !== undefined) {
289
+ return { value, source: source || 'detected.yaml' };
290
+ }
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+ /**
296
+ * Get inferred value (from current config that wasn't explicitly set)
297
+ */
298
+ getInferredValue(path) {
299
+ const config = this.loadResult.config;
300
+ if (!config)
301
+ return undefined;
302
+ // Check if we have a detected config with inference info
303
+ if (this.detectedConfig) {
304
+ return this.getDetectedValue(path);
305
+ }
306
+ // For apps, we may have implicit inference based on scanning
307
+ const match = path.match(/^apps\.(\w+)\.(\w+)$/);
308
+ if (match) {
309
+ const [, appName, field] = match;
310
+ const appConfig = config.apps?.[appName];
311
+ if (!appConfig)
312
+ return undefined;
313
+ // These fields are commonly inferred
314
+ if (field === 'type' && appConfig.type) {
315
+ // Type was likely inferred from dependencies or naming
316
+ return {
317
+ value: appConfig.type,
318
+ source: this.inferTypeSource(appName, appConfig),
319
+ };
320
+ }
321
+ if (field === 'port' && appConfig.port) {
322
+ return {
323
+ value: appConfig.port,
324
+ source: 'package.json scripts (regex extraction)',
325
+ };
326
+ }
327
+ if (field === 'framework' && appConfig.framework) {
328
+ return {
329
+ value: appConfig.framework,
330
+ source: 'package.json dependencies',
331
+ };
332
+ }
333
+ }
334
+ return undefined;
335
+ }
336
+ /**
337
+ * Infer where type detection came from
338
+ */
339
+ inferTypeSource(appName, app) {
340
+ // Check common patterns
341
+ if (appName.includes('web') || appName.includes('frontend') || appName.includes('ui')) {
342
+ return `naming convention ('${appName}' contains frontend keyword)`;
343
+ }
344
+ if (appName.includes('api') || appName.includes('backend') || appName.includes('server')) {
345
+ return `naming convention ('${appName}' contains backend keyword)`;
346
+ }
347
+ if (appName.includes('worker') || appName.includes('queue')) {
348
+ return `naming convention ('${appName}' contains worker keyword)`;
349
+ }
350
+ // Default
351
+ return 'dependency analysis (react/express/@nestjs/etc.)';
352
+ }
353
+ /**
354
+ * Check if a field supports $detect markers
355
+ */
356
+ isDetectable(path) {
357
+ return DETECTABLE_FIELDS.some(pattern => {
358
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '[^.]+') + '$');
359
+ return regex.test(path);
360
+ });
361
+ }
362
+ /**
363
+ * Get the final resolved value for a path
364
+ */
365
+ getFinalValue(path, profileName) {
366
+ const sources = this.explain(path, profileName);
367
+ const usedSource = sources.find(s => s.used);
368
+ return usedSource?.value;
369
+ }
370
+ /**
371
+ * Check if a value was explicitly set (not inferred/detected)
372
+ */
373
+ isExplicit(path, profileName) {
374
+ const sources = this.explain(path, profileName);
375
+ const usedSource = sources.find(s => s.used);
376
+ return usedSource?.explicit ?? false;
377
+ }
378
+ }
379
+ exports.ConfigExplainer = ConfigExplainer;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * Detected Configuration Schema
4
+ *
5
+ * This file defines the structure of .genbox/detected.yaml
6
+ * which is generated by 'genbox scan' and consumed by 'genbox resolve'
7
+ *
8
+ * The detected config stores auto-detected values along with
9
+ * metadata about how they were detected.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createEmptyDetectedConfig = createEmptyDetectedConfig;
13
+ exports.applyDetectedValues = applyDetectedValues;
14
+ exports.findDetectMarkers = findDetectMarkers;
15
+ exports.hasUnresolvedMarkers = hasUnresolvedMarkers;
16
+ /**
17
+ * Create an empty detected config template
18
+ */
19
+ function createEmptyDetectedConfig() {
20
+ return {
21
+ _meta: {
22
+ generated_at: new Date().toISOString(),
23
+ genbox_version: '0.0.0',
24
+ scanned_root: process.cwd(),
25
+ },
26
+ structure: {
27
+ type: 'single-app',
28
+ confidence: 'low',
29
+ indicators: [],
30
+ },
31
+ runtimes: [],
32
+ apps: {},
33
+ };
34
+ }
35
+ /**
36
+ * Merge detected values into a config, respecting $detect markers
37
+ */
38
+ function applyDetectedValues(config, detected) {
39
+ const applied = [];
40
+ function walk(obj, path = '') {
41
+ for (const [key, value] of Object.entries(obj)) {
42
+ const currentPath = path ? `${path}.${key}` : key;
43
+ if (value === '$detect') {
44
+ // Replace with detected value
45
+ const detectedValue = getDetectedValue(detected, currentPath);
46
+ if (detectedValue !== undefined) {
47
+ obj[key] = detectedValue;
48
+ applied.push(currentPath);
49
+ }
50
+ }
51
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
52
+ walk(value, currentPath);
53
+ }
54
+ }
55
+ }
56
+ const configCopy = JSON.parse(JSON.stringify(config));
57
+ walk(configCopy);
58
+ return { config: configCopy, applied };
59
+ }
60
+ /**
61
+ * Get a detected value by config path
62
+ */
63
+ function getDetectedValue(detected, path) {
64
+ // Handle apps.{name}.{field} pattern
65
+ const appMatch = path.match(/^apps\.([^.]+)\.(.+)$/);
66
+ if (appMatch) {
67
+ const [, appName, fieldPath] = appMatch;
68
+ const app = detected.apps[appName];
69
+ if (!app)
70
+ return undefined;
71
+ // Handle nested paths like commands.dev
72
+ const fields = fieldPath.split('.');
73
+ let value = app;
74
+ for (const field of fields) {
75
+ if (value && typeof value === 'object' && field in value) {
76
+ value = value[field];
77
+ }
78
+ else {
79
+ return undefined;
80
+ }
81
+ }
82
+ return value;
83
+ }
84
+ // Handle project.structure
85
+ if (path === 'project.structure') {
86
+ return detected.structure.type;
87
+ }
88
+ // Handle infrastructure.{name}.{field} pattern
89
+ const infraMatch = path.match(/^infrastructure\.([^.]+)\.(.+)$/);
90
+ if (infraMatch && detected.infrastructure) {
91
+ const [, infraName, field] = infraMatch;
92
+ const infra = detected.infrastructure.find(i => i.name === infraName);
93
+ if (infra) {
94
+ return infra[field];
95
+ }
96
+ }
97
+ // Handle git fields
98
+ if (path.startsWith('git.') && detected.git) {
99
+ const field = path.replace('git.', '');
100
+ return detected.git[field];
101
+ }
102
+ // Handle runtime fields
103
+ const runtimeMatch = path.match(/^runtimes\[(\d+)\]\.(.+)$/);
104
+ if (runtimeMatch && detected.runtimes) {
105
+ const [, index, field] = runtimeMatch;
106
+ const runtime = detected.runtimes[parseInt(index, 10)];
107
+ if (runtime) {
108
+ return runtime[field];
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * Get a list of all $detect markers in a config
115
+ */
116
+ function findDetectMarkers(config) {
117
+ const markers = [];
118
+ function walk(obj, path = '') {
119
+ if (obj === '$detect') {
120
+ markers.push(path);
121
+ return;
122
+ }
123
+ if (obj && typeof obj === 'object') {
124
+ if (Array.isArray(obj)) {
125
+ obj.forEach((item, index) => {
126
+ walk(item, `${path}[${index}]`);
127
+ });
128
+ }
129
+ else {
130
+ for (const [key, value] of Object.entries(obj)) {
131
+ const newPath = path ? `${path}.${key}` : key;
132
+ walk(value, newPath);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ walk(config);
138
+ return markers;
139
+ }
140
+ /**
141
+ * Check if a config has any unresolved $detect markers
142
+ */
143
+ function hasUnresolvedMarkers(config) {
144
+ return findDetectMarkers(config).length > 0;
145
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,11 @@ const restore_db_1 = require("./commands/restore-db");
24
24
  const help_1 = require("./commands/help");
25
25
  const profiles_1 = require("./commands/profiles");
26
26
  const db_sync_1 = require("./commands/db-sync");
27
+ const config_1 = require("./commands/config");
28
+ const scan_1 = require("./commands/scan");
29
+ const resolve_1 = require("./commands/resolve");
30
+ const validate_1 = require("./commands/validate");
31
+ const migrate_1 = require("./commands/migrate");
27
32
  program
28
33
  .addCommand(init_1.initCommand)
29
34
  .addCommand(create_1.createCommand)
@@ -39,5 +44,11 @@ program
39
44
  .addCommand(restore_db_1.restoreDbCommand)
40
45
  .addCommand(help_1.helpCommand)
41
46
  .addCommand(profiles_1.profilesCommand)
42
- .addCommand(db_sync_1.dbSyncCommand);
47
+ .addCommand(db_sync_1.dbSyncCommand)
48
+ .addCommand(config_1.configCommand)
49
+ .addCommand(scan_1.scanCommand)
50
+ .addCommand(resolve_1.resolveCommand)
51
+ .addCommand(validate_1.validateCommand)
52
+ .addCommand(migrate_1.migrateCommand)
53
+ .addCommand(migrate_1.deprecationsCommand);
43
54
  program.parse(process.argv);