genbox 1.0.14 → 1.0.16

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.
@@ -44,6 +44,7 @@ const yaml = __importStar(require("js-yaml"));
44
44
  const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
46
  const config_loader_1 = require("../config-loader");
47
+ const schema_v4_1 = require("../schema-v4");
47
48
  exports.profilesCommand = new commander_1.Command('profiles')
48
49
  .description('List and manage profiles')
49
50
  .option('--json', 'Output as JSON')
@@ -51,9 +52,15 @@ exports.profilesCommand = new commander_1.Command('profiles')
51
52
  try {
52
53
  const configLoader = new config_loader_1.ConfigLoader();
53
54
  const loadResult = await configLoader.load();
54
- if (!loadResult.config || loadResult.config.version !== '3.0') {
55
- console.log(chalk_1.default.yellow('Profiles require genbox.yaml v3.0'));
56
- console.log(chalk_1.default.dim('Run "genbox init" to create a v3 configuration'));
55
+ if (!loadResult.config) {
56
+ console.log(chalk_1.default.yellow('No genbox.yaml found'));
57
+ console.log(chalk_1.default.dim('Run "genbox init" to create a configuration'));
58
+ return;
59
+ }
60
+ const version = (0, schema_v4_1.getConfigVersion)(loadResult.config);
61
+ if (version === 'unknown') {
62
+ console.log(chalk_1.default.yellow('Unknown config version'));
63
+ console.log(chalk_1.default.dim('Run "genbox init" to create a v4 configuration'));
57
64
  return;
58
65
  }
59
66
  const config = loadResult.config;
@@ -105,8 +112,13 @@ exports.profilesCommand
105
112
  try {
106
113
  const configLoader = new config_loader_1.ConfigLoader();
107
114
  const loadResult = await configLoader.load();
108
- if (!loadResult.config || loadResult.config.version !== '3.0') {
109
- console.log(chalk_1.default.yellow('Profiles require genbox.yaml v3.0'));
115
+ if (!loadResult.config) {
116
+ console.log(chalk_1.default.yellow('No genbox.yaml found'));
117
+ return;
118
+ }
119
+ const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
120
+ if (configVersion === 'unknown') {
121
+ console.log(chalk_1.default.yellow('Unknown config version'));
110
122
  return;
111
123
  }
112
124
  const config = loadResult.config;
@@ -137,8 +149,10 @@ exports.profilesCommand
137
149
  }
138
150
  }
139
151
  }
140
- if (profile.connect_to) {
141
- console.log(` ${chalk_1.default.bold('Connect to:')} ${profile.connect_to} environment`);
152
+ // Show connection (v4: default_connection, v3: connect_to)
153
+ const connection = (0, config_loader_1.getProfileConnection)(profile);
154
+ if (connection) {
155
+ console.log(` ${chalk_1.default.bold('Connect to:')} ${connection} environment`);
142
156
  }
143
157
  if (profile.database) {
144
158
  console.log(` ${chalk_1.default.bold('Database:')}`);
@@ -147,9 +161,21 @@ exports.profilesCommand
147
161
  console.log(` Source: ${profile.database.source}`);
148
162
  }
149
163
  }
150
- if (profile.infrastructure) {
164
+ // v4 uses connections, v3 uses infrastructure
165
+ const v4Profile = profile;
166
+ const v3Profile = profile;
167
+ if (v4Profile.connections) {
168
+ console.log(` ${chalk_1.default.bold('Connections:')}`);
169
+ for (const [appName, appConns] of Object.entries(v4Profile.connections)) {
170
+ console.log(` ${appName}:`);
171
+ for (const [targetName, mode] of Object.entries(appConns)) {
172
+ console.log(` ${targetName}: ${mode}`);
173
+ }
174
+ }
175
+ }
176
+ else if (v3Profile.infrastructure) {
151
177
  console.log(` ${chalk_1.default.bold('Infrastructure:')}`);
152
- for (const [name, mode] of Object.entries(profile.infrastructure)) {
178
+ for (const [name, mode] of Object.entries(v3Profile.infrastructure)) {
153
179
  console.log(` ${name}: ${mode}`);
154
180
  }
155
181
  }
@@ -169,8 +195,13 @@ exports.profilesCommand
169
195
  try {
170
196
  const configLoader = new config_loader_1.ConfigLoader();
171
197
  const loadResult = await configLoader.load();
172
- if (!loadResult.config || loadResult.config.version !== '3.0') {
173
- console.log(chalk_1.default.yellow('Profiles require genbox.yaml v3.0'));
198
+ if (!loadResult.config) {
199
+ console.log(chalk_1.default.yellow('No genbox.yaml found'));
200
+ return;
201
+ }
202
+ const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
203
+ if (configVersion === 'unknown') {
204
+ console.log(chalk_1.default.yellow('Unknown config version'));
174
205
  return;
175
206
  }
176
207
  const config = loadResult.config;
@@ -286,8 +317,13 @@ exports.profilesCommand
286
317
  try {
287
318
  const configLoader = new config_loader_1.ConfigLoader();
288
319
  const loadResult = await configLoader.load();
289
- if (!loadResult.config || loadResult.config.version !== '3.0') {
290
- console.log(chalk_1.default.yellow('Profiles require genbox.yaml v3.0'));
320
+ if (!loadResult.config) {
321
+ console.log(chalk_1.default.yellow('No genbox.yaml found'));
322
+ return;
323
+ }
324
+ const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
325
+ if (configVersion === 'unknown') {
326
+ console.log(chalk_1.default.yellow('Unknown config version'));
291
327
  return;
292
328
  }
293
329
  const config = loadResult.config;
@@ -12,6 +12,7 @@
12
12
  * genbox scan # Output to .genbox/detected.yaml
13
13
  * genbox scan --stdout # Output to stdout
14
14
  * genbox scan --json # Output as JSON
15
+ * genbox scan -i # Interactive mode (select apps)
15
16
  */
16
17
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
18
  if (k2 === undefined) k2 = k;
@@ -52,22 +53,69 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
53
  Object.defineProperty(exports, "__esModule", { value: true });
53
54
  exports.scanCommand = void 0;
54
55
  const commander_1 = require("commander");
56
+ const prompts = __importStar(require("@inquirer/prompts"));
55
57
  const chalk_1 = __importDefault(require("chalk"));
56
58
  const fs = __importStar(require("fs"));
57
59
  const path = __importStar(require("path"));
58
60
  const yaml = __importStar(require("js-yaml"));
61
+ const child_process_1 = require("child_process");
59
62
  const scanner_1 = require("../scanner");
60
63
  // eslint-disable-next-line @typescript-eslint/no-var-requires
61
64
  const { version } = require('../../package.json');
65
+ /**
66
+ * Detect git repository info for a specific directory
67
+ */
68
+ function detectGitForDirectory(dir) {
69
+ const gitDir = path.join(dir, '.git');
70
+ if (!fs.existsSync(gitDir))
71
+ return undefined;
72
+ try {
73
+ const remote = (0, child_process_1.execSync)('git remote get-url origin', {
74
+ cwd: dir,
75
+ stdio: 'pipe',
76
+ encoding: 'utf8',
77
+ }).trim();
78
+ if (!remote)
79
+ return undefined;
80
+ const isSSH = remote.startsWith('git@') || remote.startsWith('ssh://');
81
+ let provider = 'other';
82
+ if (remote.includes('github.com'))
83
+ provider = 'github';
84
+ else if (remote.includes('gitlab.com'))
85
+ provider = 'gitlab';
86
+ else if (remote.includes('bitbucket.org'))
87
+ provider = 'bitbucket';
88
+ let branch = 'main';
89
+ try {
90
+ branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
91
+ cwd: dir,
92
+ stdio: 'pipe',
93
+ encoding: 'utf8',
94
+ }).trim();
95
+ }
96
+ catch { }
97
+ return {
98
+ remote,
99
+ type: isSSH ? 'ssh' : 'https',
100
+ provider,
101
+ branch,
102
+ };
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
62
108
  exports.scanCommand = new commander_1.Command('scan')
63
109
  .description('Analyze project structure and output detected configuration')
64
110
  .option('--stdout', 'Output to stdout instead of .genbox/detected.yaml')
65
111
  .option('--json', 'Output as JSON instead of YAML')
66
112
  .option('--no-infra', 'Skip infrastructure detection (docker-compose)')
67
113
  .option('--no-scripts', 'Skip script detection')
114
+ .option('-i, --interactive', 'Interactive mode - select apps before writing')
68
115
  .option('-e, --exclude <patterns>', 'Comma-separated patterns to exclude', '')
69
116
  .action(async (options) => {
70
117
  const cwd = process.cwd();
118
+ const isInteractive = options.interactive && !options.stdout && process.stdin.isTTY;
71
119
  console.log(chalk_1.default.cyan('\nšŸ” Scanning project...\n'));
72
120
  try {
73
121
  // Run the scanner
@@ -78,7 +126,21 @@ exports.scanCommand = new commander_1.Command('scan')
78
126
  skipScripts: !options.scripts,
79
127
  });
80
128
  // Convert scan result to DetectedConfig format
81
- const detected = convertScanToDetected(scan, cwd);
129
+ let detected = convertScanToDetected(scan, cwd);
130
+ // Interactive mode: let user select apps
131
+ if (isInteractive && Object.keys(detected.apps).length > 0) {
132
+ detected = await interactiveAppSelection(detected);
133
+ }
134
+ // Scan env files for service URLs (only for selected frontend apps)
135
+ const frontendApps = Object.entries(detected.apps)
136
+ .filter(([, app]) => app.type === 'frontend')
137
+ .map(([name]) => name);
138
+ if (frontendApps.length > 0) {
139
+ const serviceUrls = scanEnvFilesForUrls(detected.apps, cwd);
140
+ if (serviceUrls.length > 0) {
141
+ detected.service_urls = serviceUrls;
142
+ }
143
+ }
82
144
  // Output
83
145
  if (options.stdout) {
84
146
  outputToStdout(detected, options.json);
@@ -89,10 +151,156 @@ exports.scanCommand = new commander_1.Command('scan')
89
151
  }
90
152
  }
91
153
  catch (error) {
154
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
155
+ console.log('\n' + chalk_1.default.dim('Cancelled.'));
156
+ process.exit(0);
157
+ }
92
158
  console.error(chalk_1.default.red('Scan failed:'), error);
93
159
  process.exit(1);
94
160
  }
95
161
  });
162
+ /**
163
+ * Interactive app selection
164
+ */
165
+ async function interactiveAppSelection(detected) {
166
+ const appEntries = Object.entries(detected.apps);
167
+ console.log(chalk_1.default.blue('=== Detected Apps ===\n'));
168
+ // Show detected apps
169
+ for (const [name, app] of appEntries) {
170
+ const parts = [
171
+ chalk_1.default.cyan(name),
172
+ app.type ? `(${app.type})` : '',
173
+ app.framework ? `[${app.framework}]` : '',
174
+ app.port ? `port:${app.port}` : '',
175
+ ].filter(Boolean);
176
+ console.log(` ${parts.join(' ')}`);
177
+ if (app.git) {
178
+ console.log(chalk_1.default.dim(` └─ ${app.git.remote}`));
179
+ }
180
+ }
181
+ console.log();
182
+ // Let user select which apps to include
183
+ const choices = appEntries.map(([name, app]) => ({
184
+ name: `${name} (${app.type || 'unknown'}${app.framework ? `, ${app.framework}` : ''})`,
185
+ value: name,
186
+ checked: app.type !== 'library', // Default: include non-libraries
187
+ }));
188
+ const selectedApps = await prompts.checkbox({
189
+ message: 'Select apps to include in detected.yaml:',
190
+ choices,
191
+ });
192
+ // Filter apps to only selected ones
193
+ const filteredApps = {};
194
+ for (const appName of selectedApps) {
195
+ filteredApps[appName] = detected.apps[appName];
196
+ }
197
+ return {
198
+ ...detected,
199
+ apps: filteredApps,
200
+ };
201
+ }
202
+ /**
203
+ * Scan env files in app directories for service URLs
204
+ */
205
+ function scanEnvFilesForUrls(apps, rootDir) {
206
+ const serviceUrls = new Map();
207
+ const envPatterns = ['.env', '.env.local', '.env.development'];
208
+ for (const [appName, app] of Object.entries(apps)) {
209
+ // Only scan frontend apps
210
+ if (app.type !== 'frontend')
211
+ continue;
212
+ const appDir = path.join(rootDir, app.path);
213
+ // Find env file
214
+ let envContent;
215
+ let envSource;
216
+ for (const pattern of envPatterns) {
217
+ const envPath = path.join(appDir, pattern);
218
+ if (fs.existsSync(envPath)) {
219
+ envContent = fs.readFileSync(envPath, 'utf8');
220
+ envSource = pattern;
221
+ break;
222
+ }
223
+ }
224
+ if (!envContent)
225
+ continue;
226
+ // Find all HTTP URLs
227
+ const urlRegex = /^([A-Z_][A-Z0-9_]*)=["']?(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?[^"'\s]*)["']?/gm;
228
+ let match;
229
+ while ((match = urlRegex.exec(envContent)) !== null) {
230
+ const varName = match[1];
231
+ const fullUrl = match[2];
232
+ // Extract hostname
233
+ const hostMatch = fullUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)/);
234
+ if (!hostMatch)
235
+ continue;
236
+ const hostname = hostMatch[1];
237
+ // Only include local URLs (localhost, Docker internal names, IPs)
238
+ const isLocalUrl = hostname === 'localhost' ||
239
+ !hostname.includes('.') ||
240
+ /^\d+\.\d+\.\d+\.\d+$/.test(hostname);
241
+ if (!isLocalUrl)
242
+ continue;
243
+ // Extract base URL
244
+ const baseMatch = fullUrl.match(/^(https?:\/\/[a-zA-Z0-9_.-]+(?::\d+)?)/);
245
+ if (!baseMatch)
246
+ continue;
247
+ const baseUrl = baseMatch[1];
248
+ // Add to map
249
+ if (!serviceUrls.has(baseUrl)) {
250
+ serviceUrls.set(baseUrl, { vars: new Set(), apps: new Set() });
251
+ }
252
+ serviceUrls.get(baseUrl).vars.add(varName);
253
+ serviceUrls.get(baseUrl).apps.add(appName);
254
+ }
255
+ }
256
+ // Convert to DetectedServiceUrl array
257
+ const result = [];
258
+ for (const [baseUrl, { vars, apps: appNames }] of serviceUrls) {
259
+ const serviceInfo = getServiceInfoFromUrl(baseUrl);
260
+ result.push({
261
+ base_url: baseUrl,
262
+ var_name: serviceInfo.varName,
263
+ description: serviceInfo.description,
264
+ used_by: Array.from(vars),
265
+ apps: Array.from(appNames),
266
+ source: 'env files',
267
+ });
268
+ }
269
+ // Sort by port for consistent output
270
+ result.sort((a, b) => {
271
+ const portA = parseInt(a.base_url.match(/:(\d+)/)?.[1] || '0');
272
+ const portB = parseInt(b.base_url.match(/:(\d+)/)?.[1] || '0');
273
+ return portA - portB;
274
+ });
275
+ return result;
276
+ }
277
+ /**
278
+ * Get service info from URL
279
+ */
280
+ function getServiceInfoFromUrl(baseUrl) {
281
+ const urlMatch = baseUrl.match(/^https?:\/\/([a-zA-Z0-9_.-]+)(?::(\d+))?/);
282
+ if (!urlMatch) {
283
+ return { varName: 'UNKNOWN_URL', description: 'Unknown service' };
284
+ }
285
+ const hostname = urlMatch[1];
286
+ const port = urlMatch[2] ? parseInt(urlMatch[2]) : undefined;
287
+ // Generate from hostname if not localhost
288
+ if (hostname !== 'localhost') {
289
+ const varName = hostname.toUpperCase().replace(/-/g, '_') + '_URL';
290
+ return {
291
+ varName,
292
+ description: `${hostname} service`,
293
+ };
294
+ }
295
+ // Generate from port for localhost
296
+ if (port) {
297
+ return {
298
+ varName: `PORT_${port}_URL`,
299
+ description: `localhost:${port}`,
300
+ };
301
+ }
302
+ return { varName: 'LOCALHOST_URL', description: 'localhost' };
303
+ }
96
304
  /**
97
305
  * Convert ProjectScan to DetectedConfig
98
306
  */
@@ -117,10 +325,17 @@ function convertScanToDetected(scan, root) {
117
325
  })),
118
326
  apps: {},
119
327
  };
120
- // Convert apps
328
+ // Convert apps (detect git info for each app in multi-repo workspaces)
329
+ const isMultiRepo = scan.structure.type === 'hybrid';
121
330
  for (const app of scan.apps) {
122
331
  // Map scanner AppType to DetectedApp type
123
332
  const mappedType = mapAppType(app.type);
333
+ // Detect git info for this app (for multi-repo workspaces)
334
+ let appGit;
335
+ if (isMultiRepo) {
336
+ const appDir = path.join(root, app.path);
337
+ appGit = detectGitForDirectory(appDir);
338
+ }
124
339
  detected.apps[app.name] = {
125
340
  path: app.path,
126
341
  type: mappedType,
@@ -135,6 +350,7 @@ function convertScanToDetected(scan, root) {
135
350
  start: app.scripts.start,
136
351
  } : undefined,
137
352
  dependencies: app.dependencies,
353
+ git: appGit,
138
354
  };
139
355
  }
140
356
  // Convert infrastructure
@@ -341,11 +557,30 @@ function showSummary(detected) {
341
557
  console.log(` ${chalk_1.default.cyan(infra.name)}: ${infra.type} (${infra.image})`);
342
558
  }
343
559
  }
344
- // Git
560
+ // Git (root level)
345
561
  if (detected.git?.remote) {
346
562
  console.log(`\n Git: ${detected.git.provider || 'git'} (${detected.git.type})`);
347
563
  console.log(chalk_1.default.dim(` Branch: ${detected.git.branch || 'unknown'}`));
348
564
  }
565
+ // Per-app git repos (for multi-repo workspaces)
566
+ const appsWithGit = Object.entries(detected.apps).filter(([, app]) => app.git);
567
+ if (appsWithGit.length > 0) {
568
+ console.log(`\n App Repositories (${appsWithGit.length}):`);
569
+ for (const [name, app] of appsWithGit) {
570
+ const git = app.git;
571
+ console.log(` ${chalk_1.default.cyan(name)}: ${git.provider} (${git.type})`);
572
+ console.log(chalk_1.default.dim(` ${git.remote}`));
573
+ }
574
+ }
575
+ // Service URLs (for staging URL configuration)
576
+ if (detected.service_urls && detected.service_urls.length > 0) {
577
+ console.log(`\n Service URLs (${detected.service_urls.length}):`);
578
+ for (const svc of detected.service_urls) {
579
+ console.log(` ${chalk_1.default.cyan(svc.var_name)}: ${svc.base_url}`);
580
+ console.log(chalk_1.default.dim(` Used by: ${svc.used_by.slice(0, 3).join(', ')}${svc.used_by.length > 3 ? ` +${svc.used_by.length - 3} more` : ''}`));
581
+ }
582
+ console.log(chalk_1.default.dim('\n These URLs will need staging equivalents in init.'));
583
+ }
349
584
  console.log(chalk_1.default.bold('\nšŸ“ Next steps:\n'));
350
585
  console.log(' 1. Review the detected configuration in .genbox/detected.yaml');
351
586
  console.log(' 2. Run ' + chalk_1.default.cyan('genbox init --from-scan') + ' to create genbox.yaml');
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sshSetupCommand = void 0;
4
+ const commander_1 = require("commander");
5
+ const api_1 = require("../api");
6
+ const ssh_config_1 = require("../ssh-config");
7
+ /**
8
+ * Internal command to poll for IP and add SSH config in background
9
+ * Called by: genbox ssh-setup <genboxId> <name>
10
+ */
11
+ exports.sshSetupCommand = new commander_1.Command('ssh-setup')
12
+ .argument('<genboxId>', 'Genbox ID')
13
+ .argument('<name>', 'Genbox name')
14
+ .option('--max-attempts <n>', 'Max polling attempts', '90')
15
+ .option('--delay <ms>', 'Delay between attempts in ms', '2000')
16
+ .action(async (genboxId, name, options) => {
17
+ const maxAttempts = parseInt(options.maxAttempts, 10);
18
+ const delayMs = parseInt(options.delay, 10);
19
+ for (let i = 0; i < maxAttempts; i++) {
20
+ try {
21
+ const genbox = await (0, api_1.fetchApi)(`/genboxes/${genboxId}`);
22
+ if (genbox.ipAddress) {
23
+ (0, ssh_config_1.addSshConfigEntry)({ name, ipAddress: genbox.ipAddress });
24
+ process.exit(0);
25
+ }
26
+ }
27
+ catch {
28
+ // Ignore errors during polling
29
+ }
30
+ await new Promise(resolve => setTimeout(resolve, delayMs));
31
+ }
32
+ // Timed out - exit silently
33
+ process.exit(0);
34
+ });
@@ -200,7 +200,8 @@ class ConfigExplainer {
200
200
  if (!config)
201
201
  return warnings;
202
202
  // Check each app for implicit type/port/framework
203
- for (const [appName, appConfig] of Object.entries(config.apps || {})) {
203
+ for (const [appName, rawAppConfig] of Object.entries(config.apps || {})) {
204
+ const appConfig = rawAppConfig;
204
205
  // Check type - was it inferred?
205
206
  if (appConfig.type) {
206
207
  const typeExplanation = this.explain(`apps.${appName}.type`);