genbox 1.0.92 → 1.0.94

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.
@@ -796,11 +796,15 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
796
796
  const appPath = config.apps[app.name]?.path || app.name;
797
797
  const repoPath = resolved.repos.find(r => r.name === app.name)?.path ||
798
798
  (resolved.repos[0]?.path ? `${resolved.repos[0].path}/${appPath}` : `/home/dev/${config.project.name}/${appPath}`);
799
+ // Get runner type to determine infrastructure URL prefix (docker vs host)
800
+ const runner = config.apps[app.name]?.runner || 'pm2';
801
+ const infraUrlMap = (0, utils_1.buildInfraUrlMap)(envVarsFromFile, runner);
802
+ const combinedUrlMap = { ...serviceUrlMap, ...infraUrlMap };
799
803
  const servicesSections = Array.from(sections.keys()).filter(s => s.startsWith(`${app.name}/`));
800
804
  if (servicesSections.length > 0) {
801
805
  for (const serviceSectionName of servicesSections) {
802
806
  const serviceName = serviceSectionName.split('/')[1];
803
- const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
807
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, combinedUrlMap);
804
808
  const stagingName = `${app.name}-${serviceName}.env`;
805
809
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
806
810
  files.push({
@@ -812,7 +816,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
812
816
  }
813
817
  }
814
818
  else {
815
- const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
819
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, combinedUrlMap);
816
820
  files.push({
817
821
  path: `/home/dev/.env-staging/${app.name}.env`,
818
822
  content: appEnvContent,
@@ -934,10 +938,39 @@ function generateSetupScript(resolved, config, envFilesToMove = []) {
934
938
  lines.push(' \' "$file" | grep -v "^$" | head -n -1');
935
939
  lines.push(' }');
936
940
  lines.push('');
941
+ lines.push(' # Function to expand infrastructure URL placeholders based on runner type');
942
+ lines.push(' # Docker apps use DOCKER_* values, PM2/host apps use HOST_* values');
943
+ lines.push(' expand_infra_urls() {');
944
+ lines.push(' local envfile="$1"');
945
+ lines.push(' local runner="$2"');
946
+ lines.push(' if [ "$runner" = "docker" ]; then');
947
+ lines.push(' # For docker apps, replace ${VAR} with DOCKER_VAR value');
948
+ lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
949
+ lines.push(' docker_val=$(grep "^DOCKER_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
950
+ lines.push(' if [ -n "$docker_val" ]; then');
951
+ lines.push(' sed -i "s|\\${${var}}|${docker_val}|g" "$envfile"');
952
+ lines.push(' fi');
953
+ lines.push(' done');
954
+ lines.push(' else');
955
+ lines.push(' # For PM2/host apps, replace ${VAR} with HOST_VAR value');
956
+ lines.push(' for var in MONGODB_URI MONGO_URI REDIS_URL REDIS_URI RABBITMQ_URL AMQP_URL DATABASE_URL DATABASE_URI; do');
957
+ lines.push(' host_val=$(grep "^HOST_${var}=" /home/dev/.env.genbox 2>/dev/null | cut -d= -f2-)');
958
+ lines.push(' if [ -n "$host_val" ]; then');
959
+ lines.push(' sed -i "s|\\${${var}}|${host_val}|g" "$envfile"');
960
+ lines.push(' fi');
961
+ lines.push(' done');
962
+ lines.push(' fi');
963
+ lines.push(' }');
964
+ lines.push('');
937
965
  for (const { stagingName, targetPath } of envFilesToMove) {
938
- lines.push(` # Create .env for ${stagingName}`);
966
+ // Determine runner type for this app
967
+ // stagingName could be "appName" or "appName/serviceName" for service sections
968
+ const appName = stagingName.includes('/') ? stagingName.split('/')[0] : stagingName;
969
+ const runner = config.apps[appName]?.runner || 'pm2';
970
+ lines.push(` # Create .env for ${stagingName} (runner: ${runner})`);
939
971
  lines.push(` mkdir -p "$(dirname "${targetPath}")"`);
940
972
  lines.push(` extract_section "${stagingName}" /home/dev/.env.genbox > "${targetPath}"`);
973
+ lines.push(` expand_infra_urls "${targetPath}" "${runner}"`);
941
974
  lines.push(` echo " Created ${targetPath}"`);
942
975
  }
943
976
  lines.push('');
@@ -162,12 +162,19 @@ function convertScanToDetected(scan, root) {
162
162
  }
163
163
  }
164
164
  }
165
+ // Map to store git info for directories with docker apps (to preserve repo info)
166
+ const dockerDirGitInfo = new Map();
165
167
  // Convert apps from package.json (skip if docker app builds from same directory)
166
168
  const isMultiRepo = scan.structure.type === 'hybrid';
167
169
  for (const app of scan.apps) {
168
170
  // Skip if there's a docker app building from this directory
169
171
  const normalizedPath = app.path.replace(/^\.?\/?/, '').replace(/\/$/, '');
170
172
  if (dockerBuildContexts.has(normalizedPath)) {
173
+ // Save git info for this directory so docker apps can use it
174
+ const gitInfo = detectGitForDirectory(path_1.default.join(root, app.path));
175
+ if (gitInfo) {
176
+ dockerDirGitInfo.set(normalizedPath, gitInfo);
177
+ }
171
178
  continue; // Docker app takes precedence
172
179
  }
173
180
  const mappedType = mapAppType(app.type);
@@ -187,14 +194,17 @@ function convertScanToDetected(scan, root) {
187
194
  framework: app.framework,
188
195
  framework_source: app.framework ? 'package.json dependencies' : undefined,
189
196
  commands: app.scripts ? {
190
- dev: app.scripts.dev,
191
- build: app.scripts.build,
192
- start: app.scripts.start,
197
+ // Use script NAMES (not raw commands) - PM2 runs `pnpm run <name>`
198
+ dev: app.scripts.dev ? 'dev' : undefined,
199
+ build: app.scripts.build ? 'build' : undefined,
200
+ start: app.scripts.start ? 'start' : undefined,
193
201
  } : undefined,
194
202
  dependencies: app.dependencies,
195
203
  git: appGit,
196
204
  };
197
205
  }
206
+ // Store dockerDirGitInfo for use when adding docker apps
207
+ const _dockerDirGitInfo = dockerDirGitInfo;
198
208
  // Convert infrastructure (keep all, no deduplication - user selects by source file)
199
209
  if (scan.compose) {
200
210
  detected.infrastructure = [];
@@ -205,6 +215,7 @@ function convertScanToDetected(scan, root) {
205
215
  image: db.image || 'unknown',
206
216
  port: db.ports?.[0]?.host || 0,
207
217
  source: db.sourceFile || 'docker-compose.yml',
218
+ ...(db.replicaSet && { replicaSet: db.replicaSet }),
208
219
  });
209
220
  }
210
221
  for (const cache of scan.compose.caches || []) {
@@ -263,6 +274,9 @@ function convertScanToDetected(scan, root) {
263
274
  continue; // Same source, skip duplicate
264
275
  }
265
276
  }
277
+ // Look up git info for this docker app's build context
278
+ const buildContext = dockerApp.build?.context?.replace(/^\.?\/?/, '').replace(/\/$/, '') || '';
279
+ const dockerAppGit = _dockerDirGitInfo.get(buildContext);
266
280
  detected.apps[detectedAppKey] = {
267
281
  path: dockerApp.build?.context || '.',
268
282
  type: mappedType,
@@ -278,6 +292,7 @@ function convertScanToDetected(scan, root) {
278
292
  port: dockerApp.ports?.[0]?.host,
279
293
  port_source: `${dockerApp.sourceFile || 'docker-compose.yml'} ports`,
280
294
  source: dockerApp.sourceFile || 'docker-compose',
295
+ git: dockerAppGit,
281
296
  };
282
297
  }
283
298
  }
@@ -1408,6 +1423,36 @@ function generateEnvFile(projectName, detected, envVars, serviceUrlMappings) {
1408
1423
  }
1409
1424
  }
1410
1425
  }
1426
+ // Add infrastructure URL mappings (HOST_ vs DOCKER_ prefixes)
1427
+ // These are used to differentiate between PM2 apps (use HOST_*) and Docker apps (use DOCKER_*)
1428
+ if (detected.infrastructure && detected.infrastructure.length > 0) {
1429
+ content += `\n# Infrastructure URL Configuration\n`;
1430
+ content += `# HOST_* = for PM2/host-based apps (uses localhost)\n`;
1431
+ content += `# DOCKER_* = for Docker apps (uses docker network hostnames)\n\n`;
1432
+ for (const infra of detected.infrastructure) {
1433
+ const name = infra.name.toLowerCase();
1434
+ const port = infra.port;
1435
+ if (infra.type === 'database' && infra.image?.includes('mongo')) {
1436
+ // MongoDB - use project name as database name
1437
+ const dbName = projectName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
1438
+ content += `HOST_MONGODB_URI=mongodb://localhost:${port}/${dbName}\n`;
1439
+ content += `DOCKER_MONGODB_URI=mongodb://${name}:27017/${dbName}\n`;
1440
+ content += `MONGODB_URI=\${MONGODB_URI}\n\n`;
1441
+ }
1442
+ else if (infra.type === 'cache' && infra.image?.includes('redis')) {
1443
+ // Redis
1444
+ content += `HOST_REDIS_URL=redis://localhost:${port}\n`;
1445
+ content += `DOCKER_REDIS_URL=redis://${name}:6379\n`;
1446
+ content += `REDIS_URL=\${REDIS_URL}\n\n`;
1447
+ }
1448
+ else if (infra.type === 'queue' && infra.image?.includes('rabbitmq')) {
1449
+ // RabbitMQ
1450
+ content += `HOST_RABBITMQ_URL=amqp://localhost:${port}\n`;
1451
+ content += `DOCKER_RABBITMQ_URL=amqp://${name}:5672\n`;
1452
+ content += `RABBITMQ_URL=\${RABBITMQ_URL}\n\n`;
1453
+ }
1454
+ }
1455
+ }
1411
1456
  // Add GIT_TOKEN placeholder if not present
1412
1457
  if (!envVars['GIT_TOKEN']) {
1413
1458
  content += `\n# Git authentication\n# GIT_TOKEN=ghp_xxxxxxxxxxxx\n`;
@@ -98,8 +98,8 @@ class ProfileResolver {
98
98
  // Step 3: Resolve dependencies
99
99
  const { apps, infrastructure, dependencyWarnings } = await this.resolveDependencies(config, selectedApps, options, profile);
100
100
  warnings.push(...dependencyWarnings);
101
- // Step 4: Determine database mode
102
- const database = await this.resolveDatabaseMode(config, options, profile);
101
+ // Step 4: Determine database mode (pass infrastructure for replicaSet detection)
102
+ const database = await this.resolveDatabaseMode(config, options, profile, infrastructure);
103
103
  // Step 5: Determine size
104
104
  const size = options.size || profile.size || this.inferSize(apps, infrastructure);
105
105
  // Step 6: Build resolved config
@@ -320,7 +320,7 @@ class ProfileResolver {
320
320
  /**
321
321
  * Resolve database mode
322
322
  */
323
- async resolveDatabaseMode(config, options, profile) {
323
+ async resolveDatabaseMode(config, options, profile, infrastructure) {
324
324
  // Load env vars from .env.genbox (where init stores MongoDB URLs)
325
325
  const envVars = this.configLoader.loadEnvVars(process.cwd());
326
326
  // Helper to get MongoDB URL from .env.genbox
@@ -335,6 +335,9 @@ class ProfileResolver {
335
335
  // Custom environments: {NAME}_MONGODB_URL
336
336
  return envVars[`${source.toUpperCase()}_MONGODB_URL`];
337
337
  };
338
+ // Extract replicaSet from MongoDB infrastructure config if present
339
+ const mongoInfra = infrastructure.find(i => i.type === 'database' && i.image?.toLowerCase().includes('mongo'));
340
+ const replicaSet = mongoInfra?.replicaSet;
338
341
  // CLI flag takes precedence
339
342
  if (options.db) {
340
343
  // Normalize 'fresh' to 'local' (fresh is the UI name, local is internal)
@@ -344,6 +347,7 @@ class ProfileResolver {
344
347
  mode: dbMode,
345
348
  source,
346
349
  url: dbMode === 'copy' ? getMongoUrl(source) : undefined,
350
+ replicaSet,
347
351
  };
348
352
  }
349
353
  // Profile setting
@@ -354,6 +358,7 @@ class ProfileResolver {
354
358
  url: profile.database.mode === 'copy' && profile.database.source
355
359
  ? getMongoUrl(profile.database.source)
356
360
  : undefined,
361
+ replicaSet,
357
362
  };
358
363
  }
359
364
  // If default_connection is set, use remote
@@ -365,11 +370,13 @@ class ProfileResolver {
365
370
  mode: 'remote',
366
371
  source: profileConnection,
367
372
  url: mongoUrl,
373
+ replicaSet,
368
374
  };
369
375
  }
370
376
  // Interactive mode
371
377
  if (!options.yes) {
372
- return await this.selectDatabaseModeInteractive(config);
378
+ const interactiveResult = await this.selectDatabaseModeInteractive(config);
379
+ return { ...interactiveResult, replicaSet };
373
380
  }
374
381
  // Default
375
382
  const defaultMode = config.defaults?.database?.mode || 'local';
@@ -378,6 +385,7 @@ class ProfileResolver {
378
385
  mode: defaultMode,
379
386
  source: defaultSource,
380
387
  url: defaultMode === 'copy' && defaultSource ? getMongoUrl(defaultSource) : undefined,
388
+ replicaSet,
381
389
  };
382
390
  }
383
391
  /**
@@ -194,21 +194,46 @@ class ComposeParser {
194
194
  return files;
195
195
  }
196
196
  normalizeService(name, config, root, composeDir, sourceFile) {
197
+ const command = this.normalizeCommand(config.command);
198
+ const environment = this.normalizeEnvironment(config.environment);
199
+ const image = config.image;
197
200
  return {
198
201
  name,
199
- image: config.image,
202
+ image,
200
203
  build: this.normalizeBuild(config.build, root, composeDir),
201
204
  ports: this.normalizePorts(config.ports),
202
- environment: this.normalizeEnvironment(config.environment),
205
+ environment,
203
206
  envFile: this.normalizeEnvFile(config.env_file),
204
207
  dependsOn: this.normalizeDependsOn(config.depends_on),
205
208
  volumes: this.normalizeVolumes(config.volumes),
206
209
  healthcheck: this.normalizeHealthcheck(config.healthcheck),
207
- command: this.normalizeCommand(config.command),
210
+ command,
208
211
  labels: this.normalizeLabels(config.labels),
209
212
  sourceFile,
213
+ replicaSet: this.detectReplicaSet(image, command, environment),
210
214
  };
211
215
  }
216
+ /**
217
+ * Detect MongoDB replica set configuration from command or environment
218
+ */
219
+ detectReplicaSet(image, command, environment) {
220
+ // Only check MongoDB services
221
+ if (!image || !DATABASE_PATTERNS[0].test(image)) {
222
+ return undefined;
223
+ }
224
+ // Check command for --replSet flag
225
+ if (command) {
226
+ const replSetMatch = command.match(/--replSet[=\s]+(\S+)/);
227
+ if (replSetMatch) {
228
+ return replSetMatch[1];
229
+ }
230
+ }
231
+ // Check environment for MONGO_REPLICA_SET
232
+ if (environment.MONGO_REPLICA_SET) {
233
+ return environment.MONGO_REPLICA_SET;
234
+ }
235
+ return undefined;
236
+ }
212
237
  normalizeBuild(build, root, composeDir) {
213
238
  if (!build)
214
239
  return undefined;
@@ -133,6 +133,9 @@ class ProjectScanner {
133
133
  type: 'backend', // Will be refined by framework detection
134
134
  scripts,
135
135
  });
136
+ // Also scan first-level subdirectories for additional apps
137
+ const subApps = await this.discoverMultiRepoApps(root, exclude);
138
+ apps.push(...subApps);
136
139
  }
137
140
  return apps;
138
141
  }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseEnvGenboxSections = parseEnvGenboxSections;
8
8
  exports.buildServiceUrlMap = buildServiceUrlMap;
9
9
  exports.buildAppEnvContent = buildAppEnvContent;
10
+ exports.buildInfraUrlMap = buildInfraUrlMap;
10
11
  exports.parseEnvVarsFromSection = parseEnvVarsFromSection;
11
12
  /**
12
13
  * Parse .env.genbox file into segregated sections
@@ -106,6 +107,61 @@ function buildAppEnvContent(sections, appName, serviceUrlMap) {
106
107
  .trim();
107
108
  return envContent;
108
109
  }
110
+ /**
111
+ * Infrastructure variable patterns that need HOST_ vs DOCKER_ prefixes
112
+ * These are database/cache/queue connection strings that differ between
113
+ * host-based apps (PM2) and containerized apps (Docker)
114
+ */
115
+ const INFRA_VAR_PATTERNS = [
116
+ /^(MONGODB|MONGO)_URI$/i,
117
+ /^(MONGODB|MONGO)_URL$/i,
118
+ /^(REDIS)_URL$/i,
119
+ /^(REDIS)_URI$/i,
120
+ /^(RABBITMQ|RABBIT|AMQP)_URL$/i,
121
+ /^(RABBITMQ|RABBIT|AMQP)_URI$/i,
122
+ /^(POSTGRES|POSTGRESQL|PG)_URL$/i,
123
+ /^(POSTGRES|POSTGRESQL|PG)_URI$/i,
124
+ /^DATABASE_URL$/i,
125
+ /^DATABASE_URI$/i,
126
+ ];
127
+ /**
128
+ * Build a map of infrastructure URL variables based on runner type
129
+ * For docker apps: use DOCKER_* prefixed values (e.g., mongodb://mongodb:27017)
130
+ * For host/PM2 apps: use HOST_* prefixed values (e.g., mongodb://localhost:27018)
131
+ *
132
+ * Example .env.genbox:
133
+ * HOST_MONGODB_URI=mongodb://localhost:27018/myapp
134
+ * DOCKER_MONGODB_URI=mongodb://mongodb:27017/myapp
135
+ * MONGODB_URI=${MONGODB_URI} # Placeholder that gets expanded
136
+ */
137
+ function buildInfraUrlMap(envVarsFromFile, runner) {
138
+ const urlMap = {};
139
+ const useDocker = runner === 'docker';
140
+ const prefix = useDocker ? 'DOCKER_' : 'HOST_';
141
+ // Find all infrastructure variables that have HOST_ or DOCKER_ prefixes
142
+ const infraVarNames = new Set();
143
+ for (const key of Object.keys(envVarsFromFile)) {
144
+ const match = key.match(/^(HOST|DOCKER)_(.+)$/);
145
+ if (match) {
146
+ const varName = match[2];
147
+ // Check if it's an infrastructure variable
148
+ if (INFRA_VAR_PATTERNS.some(pattern => pattern.test(varName))) {
149
+ infraVarNames.add(varName);
150
+ }
151
+ }
152
+ }
153
+ // Build mapping: VARNAME → value from appropriate prefix
154
+ for (const varName of infraVarNames) {
155
+ const prefixedKey = `${prefix}${varName}`;
156
+ const fallbackKey = useDocker ? `HOST_${varName}` : `DOCKER_${varName}`;
157
+ // Use prefixed value if available, otherwise fall back
158
+ const value = envVarsFromFile[prefixedKey] || envVarsFromFile[fallbackKey];
159
+ if (value) {
160
+ urlMap[varName] = value;
161
+ }
162
+ }
163
+ return urlMap;
164
+ }
109
165
  /**
110
166
  * Parse env vars from a section content string
111
167
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.92",
3
+ "version": "1.0.94",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {