genbox 1.0.3 → 1.0.5

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
+ * Multi-Level Configuration Loader
4
+ *
5
+ * Resolution order (highest priority first):
6
+ * 1. CLI flags
7
+ * 2. User profiles (~/.genbox/profiles.yaml)
8
+ * 3. Project config (./genbox.yaml)
9
+ * 4. Workspace config (../genbox.yaml)
10
+ * 5. User defaults (~/.genbox/config.json)
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.configLoader = exports.ConfigLoader = void 0;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const yaml = __importStar(require("js-yaml"));
50
+ const os = __importStar(require("os"));
51
+ const CONFIG_FILENAME = 'genbox.yaml';
52
+ const ENV_FILENAME = '.env.genbox';
53
+ const USER_CONFIG_DIR = path.join(os.homedir(), '.genbox');
54
+ const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
55
+ const USER_PROFILES_FILE = path.join(USER_CONFIG_DIR, 'profiles.yaml');
56
+ class ConfigLoader {
57
+ /**
58
+ * Load configuration from all levels
59
+ */
60
+ async load(cwd = process.cwd()) {
61
+ const sources = [];
62
+ const warnings = [];
63
+ // 1. Find and load workspace config (parent directories)
64
+ const workspaceConfig = this.findWorkspaceConfig(cwd);
65
+ if (workspaceConfig) {
66
+ sources.push(workspaceConfig);
67
+ }
68
+ // 2. Load project config (current directory or git root)
69
+ const projectConfig = this.loadProjectConfig(cwd);
70
+ if (projectConfig) {
71
+ sources.push(projectConfig);
72
+ }
73
+ // 3. Load user config
74
+ const userConfig = this.loadUserConfig();
75
+ // Determine if this is a workspace setup
76
+ const isWorkspace = workspaceConfig !== null &&
77
+ (projectConfig === null || workspaceConfig.path !== projectConfig.path);
78
+ // Merge configurations
79
+ const mergedConfig = this.mergeConfigs(sources, userConfig);
80
+ // Validate
81
+ const validation = this.validate(mergedConfig);
82
+ if (!validation.valid) {
83
+ warnings.push(...validation.errors.map(e => e.message));
84
+ }
85
+ warnings.push(...validation.warnings.map(w => w.message));
86
+ // Determine root directory
87
+ const root = workspaceConfig?.path
88
+ ? path.dirname(workspaceConfig.path)
89
+ : projectConfig?.path
90
+ ? path.dirname(projectConfig.path)
91
+ : cwd;
92
+ return {
93
+ found: sources.length > 0,
94
+ config: sources.length > 0 ? mergedConfig : null,
95
+ sources,
96
+ warnings,
97
+ isWorkspace,
98
+ root,
99
+ };
100
+ }
101
+ /**
102
+ * Find workspace config by searching parent directories
103
+ */
104
+ findWorkspaceConfig(startDir) {
105
+ let currentDir = path.resolve(startDir);
106
+ const root = path.parse(currentDir).root;
107
+ // Go up to 5 levels max
108
+ for (let i = 0; i < 5; i++) {
109
+ const parentDir = path.dirname(currentDir);
110
+ // Stop at filesystem root
111
+ if (parentDir === currentDir || parentDir === root) {
112
+ break;
113
+ }
114
+ const configPath = path.join(parentDir, CONFIG_FILENAME);
115
+ if (fs.existsSync(configPath)) {
116
+ try {
117
+ const content = fs.readFileSync(configPath, 'utf8');
118
+ const config = yaml.load(content);
119
+ // Check if it's a workspace config (has structure: 'workspace' or multiple repos)
120
+ if (config.project?.structure === 'workspace' || config.repos) {
121
+ return {
122
+ type: 'workspace',
123
+ path: configPath,
124
+ config,
125
+ };
126
+ }
127
+ }
128
+ catch (err) {
129
+ console.warn(`Warning: Could not parse ${configPath}:`, err);
130
+ }
131
+ }
132
+ currentDir = parentDir;
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Load project config from current directory
138
+ */
139
+ loadProjectConfig(cwd) {
140
+ const configPath = path.join(cwd, CONFIG_FILENAME);
141
+ if (!fs.existsSync(configPath)) {
142
+ return null;
143
+ }
144
+ try {
145
+ const content = fs.readFileSync(configPath, 'utf8');
146
+ const config = yaml.load(content);
147
+ return {
148
+ type: 'project',
149
+ path: configPath,
150
+ config,
151
+ };
152
+ }
153
+ catch (err) {
154
+ console.warn(`Warning: Could not parse ${configPath}:`, err);
155
+ return null;
156
+ }
157
+ }
158
+ /**
159
+ * Load user configuration
160
+ */
161
+ loadUserConfig() {
162
+ if (!fs.existsSync(USER_CONFIG_FILE)) {
163
+ return null;
164
+ }
165
+ try {
166
+ const content = fs.readFileSync(USER_CONFIG_FILE, 'utf8');
167
+ return JSON.parse(content);
168
+ }
169
+ catch (err) {
170
+ console.warn('Warning: Could not parse user config:', err);
171
+ return null;
172
+ }
173
+ }
174
+ /**
175
+ * Load user profiles
176
+ */
177
+ loadUserProfiles() {
178
+ if (!fs.existsSync(USER_PROFILES_FILE)) {
179
+ return null;
180
+ }
181
+ try {
182
+ const content = fs.readFileSync(USER_PROFILES_FILE, 'utf8');
183
+ return yaml.load(content);
184
+ }
185
+ catch (err) {
186
+ console.warn('Warning: Could not parse user profiles:', err);
187
+ return null;
188
+ }
189
+ }
190
+ /**
191
+ * Merge configurations from all sources
192
+ */
193
+ mergeConfigs(sources, userConfig) {
194
+ // Start with default structure
195
+ const merged = {
196
+ version: '3.0',
197
+ project: {
198
+ name: 'unnamed',
199
+ structure: 'single-app',
200
+ },
201
+ apps: {},
202
+ };
203
+ // Merge sources in order (workspace first, then project)
204
+ for (const source of sources) {
205
+ this.deepMerge(merged, source.config);
206
+ }
207
+ // Apply user defaults
208
+ if (userConfig?.defaults) {
209
+ if (!merged.defaults) {
210
+ merged.defaults = {};
211
+ }
212
+ if (userConfig.defaults.size && !merged.defaults.size) {
213
+ merged.defaults.size = userConfig.defaults.size;
214
+ }
215
+ if (userConfig.defaults.db_source && !merged.defaults.database?.source) {
216
+ if (!merged.defaults.database) {
217
+ merged.defaults.database = { mode: 'copy' };
218
+ }
219
+ merged.defaults.database.source = userConfig.defaults.db_source;
220
+ }
221
+ }
222
+ return merged;
223
+ }
224
+ /**
225
+ * Deep merge objects
226
+ */
227
+ deepMerge(target, source) {
228
+ for (const key of Object.keys(source)) {
229
+ const sourceValue = source[key];
230
+ const targetValue = target[key];
231
+ if (sourceValue === undefined)
232
+ continue;
233
+ if (typeof sourceValue === 'object' &&
234
+ sourceValue !== null &&
235
+ !Array.isArray(sourceValue) &&
236
+ typeof targetValue === 'object' &&
237
+ targetValue !== null &&
238
+ !Array.isArray(targetValue)) {
239
+ this.deepMerge(targetValue, sourceValue);
240
+ }
241
+ else {
242
+ target[key] = sourceValue;
243
+ }
244
+ }
245
+ }
246
+ /**
247
+ * Validate configuration
248
+ */
249
+ validate(config) {
250
+ const errors = [];
251
+ const warnings = [];
252
+ // Required fields
253
+ if (!config.project?.name) {
254
+ errors.push({
255
+ path: 'project.name',
256
+ message: 'Project name is required',
257
+ severity: 'error',
258
+ });
259
+ }
260
+ if (!config.apps || Object.keys(config.apps).length === 0) {
261
+ warnings.push({
262
+ path: 'apps',
263
+ message: 'No apps defined in configuration',
264
+ severity: 'warning',
265
+ });
266
+ }
267
+ // Validate apps
268
+ for (const [name, app] of Object.entries(config.apps || {})) {
269
+ if (!app.path) {
270
+ errors.push({
271
+ path: `apps.${name}.path`,
272
+ message: `App '${name}' is missing required 'path' field`,
273
+ severity: 'error',
274
+ });
275
+ }
276
+ if (!app.type) {
277
+ warnings.push({
278
+ path: `apps.${name}.type`,
279
+ message: `App '${name}' is missing 'type' field, defaulting to 'backend'`,
280
+ severity: 'warning',
281
+ });
282
+ }
283
+ // Validate dependencies reference existing apps/infrastructure
284
+ if (app.requires) {
285
+ for (const dep of Object.keys(app.requires)) {
286
+ const isApp = config.apps?.[dep];
287
+ const isInfra = config.infrastructure?.[dep];
288
+ if (!isApp && !isInfra) {
289
+ warnings.push({
290
+ path: `apps.${name}.requires.${dep}`,
291
+ message: `App '${name}' requires '${dep}' which is not defined`,
292
+ severity: 'warning',
293
+ });
294
+ }
295
+ }
296
+ }
297
+ }
298
+ // Validate profiles
299
+ for (const [name, profile] of Object.entries(config.profiles || {})) {
300
+ // Check apps exist
301
+ for (const appName of profile.apps || []) {
302
+ if (!config.apps?.[appName]) {
303
+ errors.push({
304
+ path: `profiles.${name}.apps`,
305
+ message: `Profile '${name}' references undefined app '${appName}'`,
306
+ severity: 'error',
307
+ });
308
+ }
309
+ }
310
+ // Check extends
311
+ if (profile.extends && !config.profiles?.[profile.extends]) {
312
+ errors.push({
313
+ path: `profiles.${name}.extends`,
314
+ message: `Profile '${name}' extends undefined profile '${profile.extends}'`,
315
+ severity: 'error',
316
+ });
317
+ }
318
+ // Check connect_to
319
+ if (profile.connect_to && !config.environments?.[profile.connect_to]) {
320
+ warnings.push({
321
+ path: `profiles.${name}.connect_to`,
322
+ message: `Profile '${name}' connects to undefined environment '${profile.connect_to}'`,
323
+ severity: 'warning',
324
+ });
325
+ }
326
+ }
327
+ // Validate environments
328
+ for (const [name, env] of Object.entries(config.environments || {})) {
329
+ // Check for unresolved variable references
330
+ const checkValue = (value, path) => {
331
+ if (typeof value === 'string' && value.includes('${')) {
332
+ const match = value.match(/\$\{([^}]+)\}/);
333
+ if (match) {
334
+ warnings.push({
335
+ path,
336
+ message: `Environment '${name}' has variable reference '${match[1]}' - ensure it's defined in .env.genbox`,
337
+ severity: 'warning',
338
+ });
339
+ }
340
+ }
341
+ };
342
+ if (env.mongodb?.url)
343
+ checkValue(env.mongodb.url, `environments.${name}.mongodb.url`);
344
+ if (env.redis?.url)
345
+ checkValue(env.redis.url, `environments.${name}.redis.url`);
346
+ if (env.rabbitmq?.url)
347
+ checkValue(env.rabbitmq.url, `environments.${name}.rabbitmq.url`);
348
+ }
349
+ return {
350
+ valid: errors.length === 0,
351
+ errors,
352
+ warnings,
353
+ };
354
+ }
355
+ /**
356
+ * Get a specific profile, resolving extends
357
+ */
358
+ getProfile(config, profileName) {
359
+ const profile = config.profiles?.[profileName];
360
+ if (!profile) {
361
+ // Check user profiles
362
+ const userProfiles = this.loadUserProfiles();
363
+ if (userProfiles?.profiles?.[profileName]) {
364
+ return this.resolveProfile(config, userProfiles.profiles[profileName], userProfiles);
365
+ }
366
+ return null;
367
+ }
368
+ return this.resolveProfile(config, profile);
369
+ }
370
+ /**
371
+ * Resolve profile inheritance (extends)
372
+ */
373
+ resolveProfile(config, profile, userProfiles) {
374
+ if (!profile.extends) {
375
+ return profile;
376
+ }
377
+ // Find parent profile
378
+ let parent = config.profiles?.[profile.extends];
379
+ if (!parent && userProfiles) {
380
+ parent = userProfiles.profiles?.[profile.extends];
381
+ }
382
+ if (!parent) {
383
+ console.warn(`Warning: Profile extends '${profile.extends}' which doesn't exist`);
384
+ return profile;
385
+ }
386
+ // Resolve parent first (recursive)
387
+ const resolvedParent = this.resolveProfile(config, parent, userProfiles);
388
+ // Merge parent into child
389
+ return {
390
+ ...resolvedParent,
391
+ ...profile,
392
+ // Arrays are replaced, not merged
393
+ apps: profile.apps || resolvedParent.apps,
394
+ // Objects are merged
395
+ infrastructure: {
396
+ ...resolvedParent.infrastructure,
397
+ ...profile.infrastructure,
398
+ },
399
+ env: {
400
+ ...resolvedParent.env,
401
+ ...profile.env,
402
+ },
403
+ };
404
+ }
405
+ /**
406
+ * List all available profiles
407
+ */
408
+ listProfiles(config) {
409
+ const profiles = [];
410
+ // Project profiles
411
+ for (const [name, profile] of Object.entries(config.profiles || {})) {
412
+ const resolved = this.getProfile(config, name);
413
+ profiles.push({
414
+ name,
415
+ description: profile.description,
416
+ source: 'project',
417
+ apps: resolved?.apps || [],
418
+ size: resolved?.size,
419
+ });
420
+ }
421
+ // User profiles
422
+ const userProfiles = this.loadUserProfiles();
423
+ if (userProfiles) {
424
+ for (const [name, profile] of Object.entries(userProfiles.profiles || {})) {
425
+ // Skip if already defined in project
426
+ if (config.profiles?.[name])
427
+ continue;
428
+ const resolved = this.getProfile(config, name);
429
+ profiles.push({
430
+ name,
431
+ description: profile.description,
432
+ source: 'user',
433
+ apps: resolved?.apps || [],
434
+ size: resolved?.size,
435
+ });
436
+ }
437
+ }
438
+ return profiles;
439
+ }
440
+ /**
441
+ * Save user profile
442
+ */
443
+ saveUserProfile(name, profile) {
444
+ // Ensure directory exists
445
+ if (!fs.existsSync(USER_CONFIG_DIR)) {
446
+ fs.mkdirSync(USER_CONFIG_DIR, { recursive: true });
447
+ }
448
+ // Load existing profiles
449
+ let profiles = { profiles: {} };
450
+ if (fs.existsSync(USER_PROFILES_FILE)) {
451
+ try {
452
+ const content = fs.readFileSync(USER_PROFILES_FILE, 'utf8');
453
+ profiles = yaml.load(content);
454
+ }
455
+ catch { }
456
+ }
457
+ // Add/update profile
458
+ profiles.profiles[name] = profile;
459
+ // Save
460
+ const content = yaml.dump(profiles, { lineWidth: 120 });
461
+ fs.writeFileSync(USER_PROFILES_FILE, content);
462
+ }
463
+ /**
464
+ * Load environment variables from .env.genbox files
465
+ */
466
+ loadEnvVars(root) {
467
+ const vars = {};
468
+ // Load from multiple locations
469
+ const envPaths = [
470
+ path.join(root, ENV_FILENAME),
471
+ path.join(path.dirname(root), ENV_FILENAME), // Workspace level
472
+ ];
473
+ for (const envPath of envPaths) {
474
+ if (fs.existsSync(envPath)) {
475
+ const content = fs.readFileSync(envPath, 'utf8');
476
+ const parsed = this.parseEnvFile(content);
477
+ Object.assign(vars, parsed);
478
+ }
479
+ }
480
+ return vars;
481
+ }
482
+ /**
483
+ * Parse .env file content
484
+ */
485
+ parseEnvFile(content) {
486
+ const vars = {};
487
+ const lines = content.split('\n');
488
+ for (const line of lines) {
489
+ const trimmed = line.trim();
490
+ // Skip comments and empty lines
491
+ if (!trimmed || trimmed.startsWith('#'))
492
+ continue;
493
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
494
+ if (match) {
495
+ let [, key, value] = match;
496
+ // Remove quotes
497
+ if ((value.startsWith('"') && value.endsWith('"')) ||
498
+ (value.startsWith("'") && value.endsWith("'"))) {
499
+ value = value.slice(1, -1);
500
+ }
501
+ vars[key] = value;
502
+ }
503
+ }
504
+ return vars;
505
+ }
506
+ /**
507
+ * Resolve variable references in a string
508
+ */
509
+ resolveVariables(value, vars) {
510
+ return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
511
+ return vars[varName] || match;
512
+ });
513
+ }
514
+ /**
515
+ * Check if a config file exists
516
+ */
517
+ hasConfig(cwd = process.cwd()) {
518
+ return fs.existsSync(path.join(cwd, CONFIG_FILENAME));
519
+ }
520
+ /**
521
+ * Get the config file path
522
+ */
523
+ getConfigPath(cwd = process.cwd()) {
524
+ return path.join(cwd, CONFIG_FILENAME);
525
+ }
526
+ }
527
+ exports.ConfigLoader = ConfigLoader;
528
+ // Export singleton instance
529
+ exports.configLoader = new ConfigLoader();
package/dist/index.js CHANGED
@@ -22,6 +22,8 @@ const status_1 = require("./commands/status");
22
22
  const forward_1 = require("./commands/forward");
23
23
  const restore_db_1 = require("./commands/restore-db");
24
24
  const help_1 = require("./commands/help");
25
+ const profiles_1 = require("./commands/profiles");
26
+ const db_sync_1 = require("./commands/db-sync");
25
27
  program
26
28
  .addCommand(init_1.initCommand)
27
29
  .addCommand(create_1.createCommand)
@@ -35,5 +37,7 @@ program
35
37
  .addCommand(status_1.statusCommand)
36
38
  .addCommand(forward_1.forwardCommand)
37
39
  .addCommand(restore_db_1.restoreDbCommand)
38
- .addCommand(help_1.helpCommand);
40
+ .addCommand(help_1.helpCommand)
41
+ .addCommand(profiles_1.profilesCommand)
42
+ .addCommand(db_sync_1.dbSyncCommand);
39
43
  program.parse(process.argv);