genbox 1.0.47 → 1.0.49

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.
package/dist/api.js CHANGED
@@ -8,6 +8,14 @@ exports.handleApiError = handleApiError;
8
8
  exports.isAuthError = isAuthError;
9
9
  exports.fetchApi = fetchApi;
10
10
  exports.checkNameAvailability = checkNameAvailability;
11
+ exports.syncProject = syncProject;
12
+ exports.getProjectByName = getProjectByName;
13
+ exports.initiateSnapshotUpload = initiateSnapshotUpload;
14
+ exports.completeSnapshotUpload = completeSnapshotUpload;
15
+ exports.failSnapshotUpload = failSnapshotUpload;
16
+ exports.listProjectSnapshots = listProjectSnapshots;
17
+ exports.getLatestSnapshot = getLatestSnapshot;
18
+ exports.getSnapshotDownloadUrl = getSnapshotDownloadUrl;
11
19
  const chalk_1 = __importDefault(require("chalk"));
12
20
  const config_store_1 = require("./config-store");
13
21
  const API_URL = process.env.GENBOX_API_URL || 'https://api.genbox.dev';
@@ -97,3 +105,57 @@ async function fetchApi(endpoint, options = {}) {
97
105
  async function checkNameAvailability(name, workspace) {
98
106
  return fetchApi(`/genboxes/check-name?name=${encodeURIComponent(name)}&workspace=${encodeURIComponent(workspace)}`);
99
107
  }
108
+ async function syncProject(payload) {
109
+ return fetchApi('/projects', {
110
+ method: 'POST',
111
+ body: JSON.stringify(payload),
112
+ });
113
+ }
114
+ async function getProjectByName(name) {
115
+ return fetchApi(`/projects/by-name/${encodeURIComponent(name)}`);
116
+ }
117
+ /**
118
+ * Initiate a snapshot upload - get pre-signed URL
119
+ */
120
+ async function initiateSnapshotUpload(payload) {
121
+ return fetchApi('/database-snapshots/initiate-upload', {
122
+ method: 'POST',
123
+ body: JSON.stringify(payload),
124
+ });
125
+ }
126
+ /**
127
+ * Complete a snapshot upload after file is uploaded to S3
128
+ */
129
+ async function completeSnapshotUpload(snapshotId, payload) {
130
+ return fetchApi(`/database-snapshots/${snapshotId}/complete`, {
131
+ method: 'POST',
132
+ body: JSON.stringify(payload),
133
+ });
134
+ }
135
+ /**
136
+ * Mark snapshot upload as failed
137
+ */
138
+ async function failSnapshotUpload(snapshotId, error) {
139
+ await fetchApi(`/database-snapshots/${snapshotId}/fail`, {
140
+ method: 'POST',
141
+ body: JSON.stringify({ error }),
142
+ });
143
+ }
144
+ /**
145
+ * List snapshots for a project
146
+ */
147
+ async function listProjectSnapshots(projectId) {
148
+ return fetchApi(`/database-snapshots/project/${projectId}`);
149
+ }
150
+ /**
151
+ * Get latest snapshot for a project by source
152
+ */
153
+ async function getLatestSnapshot(projectId, source) {
154
+ return fetchApi(`/database-snapshots/project/${projectId}/latest?source=${source}`);
155
+ }
156
+ /**
157
+ * Get download URL for a snapshot
158
+ */
159
+ async function getSnapshotDownloadUrl(snapshotId) {
160
+ return fetchApi(`/database-snapshots/${snapshotId}/download`);
161
+ }
@@ -51,6 +51,7 @@ const ssh_config_1 = require("../ssh-config");
51
51
  const schema_v4_1 = require("../schema-v4");
52
52
  const child_process_1 = require("child_process");
53
53
  const random_name_1 = require("../random-name");
54
+ const db_utils_1 = require("../db-utils");
54
55
  // Credits consumed per hour for each size (matches API billing.config.ts)
55
56
  const CREDITS_PER_HOUR = {
56
57
  cx22: 1,
@@ -80,6 +81,23 @@ function spawnSshConfigSetup(genboxId, name) {
80
81
  // Allow parent to exit independently
81
82
  child.unref();
82
83
  }
84
+ const DETECTED_DIR = '.genbox';
85
+ const PROJECT_CACHE_FILENAME = 'project.json';
86
+ /**
87
+ * Load project cache from local storage
88
+ */
89
+ function loadProjectCache(rootDir) {
90
+ const cachePath = path.join(rootDir, DETECTED_DIR, PROJECT_CACHE_FILENAME);
91
+ if (!fs.existsSync(cachePath))
92
+ return null;
93
+ try {
94
+ const content = fs.readFileSync(cachePath, 'utf8');
95
+ return JSON.parse(content);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
83
101
  async function provisionGenbox(payload) {
84
102
  return (0, api_1.fetchApi)('/genboxes', {
85
103
  method: 'POST',
@@ -220,6 +238,8 @@ exports.createCommand = new commander_1.Command('create')
220
238
  .option('--api <mode>', 'API mode: local, staging, production')
221
239
  .option('--db <mode>', 'Database mode: none, local, copy, remote')
222
240
  .option('--db-source <source>', 'Database source: staging, production')
241
+ .option('--db-dump <path>', 'Use existing mongodump file instead of creating one')
242
+ .option('--db-copy-remote', 'Copy database on the server (requires publicly accessible DB)')
223
243
  .option('-s, --size <size>', 'Server size: small, medium, large, xl')
224
244
  .option('-b, --branch <branch>', 'Use existing git branch (skips new branch creation)')
225
245
  .option('-n, --new-branch <name>', 'Create a new branch with this name (defaults to env name)')
@@ -238,7 +258,7 @@ exports.createCommand = new commander_1.Command('create')
238
258
  return;
239
259
  }
240
260
  const configVersion = (0, schema_v4_1.getConfigVersion)(loadResult.config);
241
- if (configVersion === 'unknown') {
261
+ if (configVersion === 'invalid') {
242
262
  console.log(chalk_1.default.red('Unknown config version'));
243
263
  console.log(chalk_1.default.dim('Run "genbox init" to create a new configuration.'));
244
264
  return;
@@ -410,8 +430,102 @@ exports.createCommand = new commander_1.Command('create')
410
430
  }
411
431
  }
412
432
  }
433
+ // Handle database copy - dump locally and upload to S3
434
+ let snapshotId;
435
+ let snapshotS3Key;
436
+ let localDumpPath;
437
+ const projectCache = loadProjectCache(process.cwd());
438
+ const needsLocalDbCopy = resolved.database.mode === 'copy' &&
439
+ resolved.database.url &&
440
+ !options.dbCopyRemote;
441
+ if (needsLocalDbCopy) {
442
+ // Check for user-provided dump file
443
+ if (options.dbDump) {
444
+ if (!fs.existsSync(options.dbDump)) {
445
+ console.log(chalk_1.default.red(`Database dump file not found: ${options.dbDump}`));
446
+ return;
447
+ }
448
+ localDumpPath = options.dbDump;
449
+ console.log(chalk_1.default.dim(` Using provided dump file: ${options.dbDump}`));
450
+ }
451
+ else {
452
+ // Need to run mongodump locally
453
+ if (!(0, db_utils_1.isMongoDumpAvailable)()) {
454
+ console.log(chalk_1.default.red('mongodump not found. Required for database copy.'));
455
+ console.log('');
456
+ console.log((0, db_utils_1.getMongoDumpInstallInstructions)());
457
+ console.log('');
458
+ console.log(chalk_1.default.dim('Alternatively:'));
459
+ console.log(chalk_1.default.dim(' • Use --db-dump <path> to provide an existing dump file'));
460
+ console.log(chalk_1.default.dim(' • Use --db-copy-remote if your database is publicly accessible'));
461
+ return;
462
+ }
463
+ const dbUrl = resolved.database.url;
464
+ console.log('');
465
+ console.log(chalk_1.default.blue('=== Database Copy ==='));
466
+ console.log(chalk_1.default.dim(` Source: ${resolved.database.source}`));
467
+ console.log(chalk_1.default.dim(` URL: ${dbUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@')}`));
468
+ const dumpSpinner = (0, ora_1.default)('Creating database dump...').start();
469
+ const dumpResult = await (0, db_utils_1.runLocalMongoDump)(dbUrl, {
470
+ onProgress: (msg) => dumpSpinner.text = msg,
471
+ });
472
+ if (!dumpResult.success) {
473
+ dumpSpinner.fail(chalk_1.default.red('Database dump failed'));
474
+ console.log(chalk_1.default.red(` ${dumpResult.error}`));
475
+ console.log('');
476
+ console.log(chalk_1.default.dim('You can:'));
477
+ console.log(chalk_1.default.dim(' • Fix the database connection and try again'));
478
+ console.log(chalk_1.default.dim(' • Use --db local to start with an empty database'));
479
+ console.log(chalk_1.default.dim(' • Use --db-dump <path> to provide an existing dump'));
480
+ return;
481
+ }
482
+ dumpSpinner.succeed(chalk_1.default.green(`Database dump created (${(0, db_utils_1.formatBytes)(dumpResult.sizeBytes || 0)})`));
483
+ localDumpPath = dumpResult.dumpPath;
484
+ }
485
+ // Upload to S3 if we have a project ID
486
+ if (localDumpPath && projectCache?._id) {
487
+ const uploadSpinner = (0, ora_1.default)('Uploading database snapshot...').start();
488
+ const snapshotSource = (resolved.database.source === 'staging' ? 'staging' :
489
+ resolved.database.source === 'production' ? 'production' : 'local');
490
+ const snapshotResult = await (0, db_utils_1.createAndUploadSnapshot)(localDumpPath, projectCache._id, snapshotSource, {
491
+ sourceUrl: resolved.database.url?.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'),
492
+ onProgress: (msg) => uploadSpinner.text = msg,
493
+ });
494
+ if (snapshotResult.success) {
495
+ uploadSpinner.succeed(chalk_1.default.green('Database snapshot uploaded'));
496
+ snapshotId = snapshotResult.snapshotId;
497
+ snapshotS3Key = snapshotResult.s3Key;
498
+ // Cleanup local dump since it's now in S3
499
+ (0, db_utils_1.cleanupDump)(localDumpPath);
500
+ localDumpPath = undefined;
501
+ }
502
+ else {
503
+ uploadSpinner.warn(chalk_1.default.yellow(`Snapshot upload failed: ${snapshotResult.error}`));
504
+ console.log(chalk_1.default.dim(' Will fall back to direct SCP upload after genbox creation'));
505
+ }
506
+ }
507
+ }
413
508
  // Build payload
414
- const payload = buildPayload(resolved, config, publicKey, privateKeyContent, configLoader);
509
+ const payloadResolved = { ...resolved };
510
+ if (snapshotId && snapshotS3Key) {
511
+ // Use S3 snapshot mode
512
+ payloadResolved.database = {
513
+ ...resolved.database,
514
+ url: undefined,
515
+ mode: 'snapshot',
516
+ snapshotId,
517
+ s3Key: snapshotS3Key,
518
+ };
519
+ }
520
+ else if (localDumpPath) {
521
+ // Fall back to SCP upload mode (no project or S3 upload failed)
522
+ payloadResolved.database = {
523
+ ...resolved.database,
524
+ url: undefined,
525
+ mode: 'copy-local',
526
+ };
527
+ }
528
+ const payload = buildPayload(payloadResolved, config, publicKey, privateKeyContent, configLoader);
415
529
  // Create genbox
416
530
  const spinner = (0, ora_1.default)(`Creating Genbox '${name}'...`).start();
417
531
  try {
@@ -431,12 +545,69 @@ exports.createCommand = new commander_1.Command('create')
431
545
  }
432
546
  // Display results
433
547
  displayGenboxInfo(genbox, resolved);
548
+ // Handle database upload if we have a local dump
549
+ if (localDumpPath && genbox.ipAddress) {
550
+ console.log('');
551
+ console.log(chalk_1.default.blue('=== Database Restore ==='));
552
+ // Wait for SSH access
553
+ const sshSpinner = (0, ora_1.default)('Waiting for SSH access...').start();
554
+ const sshReady = await (0, db_utils_1.waitForSshAccess)(genbox.ipAddress, 300, (msg) => {
555
+ sshSpinner.text = msg;
556
+ });
557
+ if (!sshReady) {
558
+ sshSpinner.fail(chalk_1.default.yellow('SSH not ready - database will need manual restore'));
559
+ console.log(chalk_1.default.dim(' Run `genbox db sync` after the server is ready'));
560
+ (0, db_utils_1.cleanupDump)(localDumpPath);
561
+ }
562
+ else {
563
+ sshSpinner.succeed('SSH connected');
564
+ // Upload dump
565
+ const uploadSpinner = (0, ora_1.default)('Uploading database dump...').start();
566
+ const uploadResult = await (0, db_utils_1.uploadDumpToGenbox)(localDumpPath, genbox.ipAddress, {
567
+ onProgress: (msg) => uploadSpinner.text = msg,
568
+ });
569
+ if (!uploadResult.success) {
570
+ uploadSpinner.fail(chalk_1.default.yellow('Upload failed - database will need manual restore'));
571
+ console.log(chalk_1.default.dim(` ${uploadResult.error}`));
572
+ console.log(chalk_1.default.dim(' Run `genbox db sync` after the server is ready'));
573
+ (0, db_utils_1.cleanupDump)(localDumpPath);
574
+ }
575
+ else {
576
+ uploadSpinner.succeed('Dump uploaded');
577
+ // Trigger restore
578
+ const restoreSpinner = (0, ora_1.default)('Restoring database...').start();
579
+ const restoreResult = await (0, db_utils_1.runRemoteMongoRestore)(genbox.ipAddress, config.project.name, { onProgress: (msg) => restoreSpinner.text = msg });
580
+ if (!restoreResult.success) {
581
+ restoreSpinner.fail(chalk_1.default.yellow('Restore failed'));
582
+ console.log(chalk_1.default.dim(` ${restoreResult.error}`));
583
+ console.log(chalk_1.default.dim(' The dump file is on the server - you can restore manually'));
584
+ }
585
+ else {
586
+ restoreSpinner.succeed(chalk_1.default.green('Database restored successfully!'));
587
+ }
588
+ // Cleanup local dump
589
+ (0, db_utils_1.cleanupDump)(localDumpPath);
590
+ }
591
+ }
592
+ }
593
+ else if (localDumpPath) {
594
+ // No IP yet - inform user to run db sync later
595
+ console.log('');
596
+ console.log(chalk_1.default.yellow('Database dump created but IP not yet assigned.'));
597
+ console.log(chalk_1.default.dim(' Run `genbox db sync` once the server is ready to restore the database.'));
598
+ // Keep the dump file - user might want to use it
599
+ console.log(chalk_1.default.dim(` Dump file: ${localDumpPath}`));
600
+ }
434
601
  // Inform user about server provisioning
435
602
  console.log('');
436
603
  console.log(chalk_1.default.dim('Server is provisioning. Run `genbox connect` once ready.'));
437
604
  }
438
605
  catch (error) {
439
606
  spinner.fail(chalk_1.default.red(`Failed to create Genbox: ${error.message}`));
607
+ // Cleanup dump on failure
608
+ if (localDumpPath) {
609
+ (0, db_utils_1.cleanupDump)(localDumpPath);
610
+ }
440
611
  if (error instanceof api_1.AuthenticationError) {
441
612
  console.log('');
442
613
  console.log(chalk_1.default.yellow(' Please authenticate first:'));
@@ -509,6 +680,16 @@ function displayResolvedConfig(resolved) {
509
680
  if (hasBackendApps && resolved.database.mode !== 'none') {
510
681
  console.log('');
511
682
  console.log(` ${chalk_1.default.bold('Database:')} ${resolved.database.mode}${resolved.database.source ? ` (from ${resolved.database.source})` : ''}`);
683
+ // Show info for database copy mode
684
+ if (resolved.database.mode === 'copy' && resolved.database.source) {
685
+ if (resolved.database.url) {
686
+ console.log(chalk_1.default.dim(` Source URL: ${resolved.database.url.replace(/\/\/[^:]+:[^@]+@/, '//***:***@')}`));
687
+ console.log(chalk_1.default.dim(` Database will be dumped locally and uploaded to the genbox`));
688
+ }
689
+ else {
690
+ console.log(chalk_1.default.yellow(` ⚠ No database URL found in environments.${resolved.database.source}.urls.mongodb`));
691
+ }
692
+ }
512
693
  }
513
694
  if (Object.keys(resolved.env).length > 0) {
514
695
  console.log('');
@@ -735,13 +916,8 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
735
916
  // Build services map
736
917
  const services = {};
737
918
  for (const app of resolved.apps) {
738
- if (app.services) {
739
- for (const [name, svc] of Object.entries(app.services)) {
740
- services[name] = { port: svc.port, healthcheck: svc.healthcheck };
741
- }
742
- }
743
- else if (app.port) {
744
- services[app.name] = { port: app.port };
919
+ if (app.port) {
920
+ services[app.name] = { port: app.port, healthcheck: app.healthcheck };
745
921
  }
746
922
  }
747
923
  // Build files bundle
@@ -848,6 +1024,8 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
848
1024
  }
849
1025
  // Get local git config for commits
850
1026
  const gitConfig = getGitConfig();
1027
+ // Load project cache to get project ID
1028
+ const projectCache = loadProjectCache(process.cwd());
851
1029
  return {
852
1030
  name: resolved.name,
853
1031
  size: resolved.size,
@@ -887,6 +1065,8 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
887
1065
  branch: resolved.repos[0]?.branch,
888
1066
  newBranch: resolved.repos[0]?.newBranch,
889
1067
  sourceBranch: resolved.repos[0]?.sourceBranch,
1068
+ // Project reference
1069
+ project: projectCache?._id,
890
1070
  };
891
1071
  }
892
1072
  /**
@@ -78,7 +78,7 @@ exports.dbSyncCommand
78
78
  const config = loadResult.config;
79
79
  const source = options.source;
80
80
  // Validate source environment exists
81
- if (config.version === '3.0' && !config.environments?.[source]) {
81
+ if (config.version === 4 && !config.environments?.[source]) {
82
82
  console.log(chalk_1.default.yellow(`Environment '${source}' not configured in genbox.yaml`));
83
83
  console.log(chalk_1.default.dim('Available environments:'));
84
84
  for (const env of Object.keys(config.environments || {})) {
@@ -14,6 +14,8 @@ exports.helpCommand = new commander_1.Command('help')
14
14
  console.log(chalk_1.default.bold('Genbox CLI - AI-Powered Development Environments'));
15
15
  console.log(chalk_1.default.dim('Create isolated cloud development environments on demand'));
16
16
  console.log('');
17
+ console.log(chalk_1.default.green('TIP:') + ' Use ' + chalk_1.default.cyan('gb') + ' as a shorthand for ' + chalk_1.default.cyan('genbox') + ' (e.g., ' + chalk_1.default.dim('gb list') + ')');
18
+ console.log('');
17
19
  if (command) {
18
20
  showCommandHelp(command);
19
21
  }
@@ -45,34 +47,34 @@ function showGeneralHelp() {
45
47
  console.log(chalk_1.default.bold('QUICK START'));
46
48
  console.log('');
47
49
  console.log(' 1. Initialize your project:');
48
- console.log(chalk_1.default.dim(' $ genbox init'));
50
+ console.log(chalk_1.default.dim(' $ gb init'));
49
51
  console.log('');
50
52
  console.log(' 2. Create a development environment:');
51
- console.log(chalk_1.default.dim(' $ genbox create feature-auth'));
53
+ console.log(chalk_1.default.dim(' $ gb create feature-auth'));
52
54
  console.log('');
53
55
  console.log(' 3. Check progress (setup takes a few minutes):');
54
- console.log(chalk_1.default.dim(' $ genbox status feature-auth'));
56
+ console.log(chalk_1.default.dim(' $ gb status feature-auth'));
55
57
  console.log('');
56
58
  console.log(' 4. Connect and start coding:');
57
- console.log(chalk_1.default.dim(' $ genbox connect feature-auth'));
59
+ console.log(chalk_1.default.dim(' $ gb connect feature-auth'));
58
60
  console.log('');
59
61
  console.log(' 5. Clean up when done:');
60
- console.log(chalk_1.default.dim(' $ genbox destroy feature-auth'));
62
+ console.log(chalk_1.default.dim(' $ gb destroy feature-auth'));
61
63
  console.log('');
62
64
  console.log(chalk_1.default.bold('WORKFLOW EXAMPLES'));
63
65
  console.log('');
64
66
  console.log(chalk_1.default.yellow(' Feature Development:'));
65
- console.log(chalk_1.default.dim(' $ genbox create feat-new-checkout'));
66
- console.log(chalk_1.default.dim(' $ genbox status feat-new-checkout # Wait for setup'));
67
- console.log(chalk_1.default.dim(' $ genbox urls feat-new-checkout # Get service URLs'));
68
- console.log(chalk_1.default.dim(' $ genbox connect feat-new-checkout # SSH to code'));
67
+ console.log(chalk_1.default.dim(' $ gb create feat-new-checkout'));
68
+ console.log(chalk_1.default.dim(' $ gb status feat-new-checkout # Wait for setup'));
69
+ console.log(chalk_1.default.dim(' $ gb urls feat-new-checkout # Get service URLs'));
70
+ console.log(chalk_1.default.dim(' $ gb connect feat-new-checkout # SSH to code'));
69
71
  console.log('');
70
72
  console.log(chalk_1.default.yellow(' Local Debugging with Port Forwarding:'));
71
- console.log(chalk_1.default.dim(' $ genbox forward feat-new-checkout'));
73
+ console.log(chalk_1.default.dim(' $ gb forward feat-new-checkout'));
72
74
  console.log(chalk_1.default.dim(' # Now access remote services at localhost:3050, etc.'));
73
75
  console.log('');
74
76
  console.log(chalk_1.default.yellow(' Database Sync:'));
75
- console.log(chalk_1.default.dim(' $ genbox restore-db feat-new-checkout'));
77
+ console.log(chalk_1.default.dim(' $ gb restore-db feat-new-checkout'));
76
78
  console.log(chalk_1.default.dim(' # Copies your local MongoDB to the Genbox'));
77
79
  console.log('');
78
80
  console.log(chalk_1.default.bold('CONFIGURATION FILES'));
@@ -80,12 +82,12 @@ function showGeneralHelp() {
80
82
  console.log(` ${chalk_1.default.cyan('genbox.yaml')} Project configuration (services, ports, repos)`);
81
83
  console.log(` ${chalk_1.default.cyan('.env.genbox')} Environment variables (secrets, tokens)`);
82
84
  console.log('');
83
- console.log(chalk_1.default.dim(' Run `genbox init` to create these files interactively.'));
85
+ console.log(chalk_1.default.dim(' Run `gb init` to create these files interactively.'));
84
86
  console.log('');
85
87
  console.log(chalk_1.default.bold('MORE HELP'));
86
88
  console.log('');
87
89
  console.log(' Get help for a specific command:');
88
- console.log(chalk_1.default.dim(' $ genbox help <command>'));
90
+ console.log(chalk_1.default.dim(' $ gb help <command>'));
89
91
  console.log('');
90
92
  console.log(' Documentation: https://genbox.dev/docs');
91
93
  console.log(' Issues: https://github.com/goodpass-co/genbox/issues');
@@ -59,12 +59,63 @@ const fs_1 = __importDefault(require("fs"));
59
59
  const yaml = __importStar(require("js-yaml"));
60
60
  const process = __importStar(require("process"));
61
61
  const scanner_1 = require("../scanner");
62
+ const api_1 = require("../api");
63
+ const config_store_1 = require("../config-store");
62
64
  // eslint-disable-next-line @typescript-eslint/no-var-requires
63
65
  const { version } = require('../../package.json');
64
66
  const CONFIG_FILENAME = 'genbox.yaml';
65
67
  const ENV_FILENAME = '.env.genbox';
66
68
  const DETECTED_DIR = '.genbox';
67
69
  const DETECTED_FILENAME = 'detected.yaml';
70
+ const PROJECT_CACHE_FILENAME = 'project.json';
71
+ /**
72
+ * Save project ID and info to local cache
73
+ */
74
+ function saveProjectCache(rootDir, cache) {
75
+ const genboxDir = path_1.default.join(rootDir, DETECTED_DIR);
76
+ if (!fs_1.default.existsSync(genboxDir)) {
77
+ fs_1.default.mkdirSync(genboxDir, { recursive: true });
78
+ }
79
+ const cachePath = path_1.default.join(genboxDir, PROJECT_CACHE_FILENAME);
80
+ fs_1.default.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
81
+ }
82
+ /**
83
+ * Load project cache from local storage
84
+ */
85
+ function loadProjectCache(rootDir) {
86
+ const cachePath = path_1.default.join(rootDir, DETECTED_DIR, PROJECT_CACHE_FILENAME);
87
+ if (!fs_1.default.existsSync(cachePath))
88
+ return null;
89
+ try {
90
+ const content = fs_1.default.readFileSync(cachePath, 'utf8');
91
+ return JSON.parse(content);
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * Convert GenboxConfig to ProjectSyncPayload
99
+ */
100
+ function configToSyncPayload(config, envVars, privateKey) {
101
+ return {
102
+ name: config.project.name,
103
+ version: config.version,
104
+ project: config.project,
105
+ apps: config.apps,
106
+ provides: config.provides,
107
+ environments: config.environments,
108
+ profiles: config.profiles,
109
+ defaults: config.defaults,
110
+ repos: config.repos,
111
+ hooks: config.hooks,
112
+ scripts: config.scripts,
113
+ strict: config.strict,
114
+ git_auth: config.git_auth,
115
+ envVars,
116
+ privateKey,
117
+ };
118
+ }
68
119
  // =============================================================================
69
120
  // Scan Phase
70
121
  // =============================================================================
@@ -969,7 +1020,7 @@ function generateDefaultProfiles(detected, environments) {
969
1020
  // Config Generation
970
1021
  // =============================================================================
971
1022
  /**
972
- * Generate GenboxConfigV4 from detected config and user inputs
1023
+ * Generate GenboxConfig from detected config and user inputs
973
1024
  */
974
1025
  function generateConfig(detected, settings, repos, environments, profiles) {
975
1026
  // Convert apps
@@ -1554,6 +1605,25 @@ exports.initCommand = new commander_1.Command('init')
1554
1605
  const envContent = generateEnvFile(settings.projectName, detected, { ...gitEnvVars, LOCAL_API_URL: 'http://localhost:3050' }, []);
1555
1606
  fs_1.default.writeFileSync(path_1.default.join(rootDir, ENV_FILENAME), envContent);
1556
1607
  console.log(chalk_1.default.green(`✔ Created ${ENV_FILENAME}`));
1608
+ // Sync project to API if logged in
1609
+ if (config_store_1.ConfigStore.getToken()) {
1610
+ try {
1611
+ const payload = configToSyncPayload(config, { ...gitEnvVars, LOCAL_API_URL: 'http://localhost:3050' });
1612
+ const result = await (0, api_1.syncProject)(payload);
1613
+ saveProjectCache(rootDir, {
1614
+ _id: result._id,
1615
+ name: result.name,
1616
+ lastSyncedAt: new Date().toISOString(),
1617
+ action: result.action,
1618
+ });
1619
+ console.log(chalk_1.default.green(`✔ Project synced to Genbox (${result.action})`));
1620
+ }
1621
+ catch (error) {
1622
+ if (!(0, api_1.isAuthError)(error)) {
1623
+ console.log(chalk_1.default.yellow(` Warning: Could not sync project - ${error.message}`));
1624
+ }
1625
+ }
1626
+ }
1557
1627
  return;
1558
1628
  }
1559
1629
  // =========================================
@@ -1604,6 +1674,38 @@ exports.initCommand = new commander_1.Command('init')
1604
1674
  }
1605
1675
  }
1606
1676
  // =========================================
1677
+ // PHASE 8: Sync Project to API
1678
+ // =========================================
1679
+ if (config_store_1.ConfigStore.getToken()) {
1680
+ const syncSpinner = (0, ora_1.default)('Syncing project to Genbox...').start();
1681
+ try {
1682
+ const payload = configToSyncPayload(config, allEnvVars);
1683
+ const result = await (0, api_1.syncProject)(payload);
1684
+ saveProjectCache(rootDir, {
1685
+ _id: result._id,
1686
+ name: result.name,
1687
+ lastSyncedAt: new Date().toISOString(),
1688
+ action: result.action,
1689
+ });
1690
+ syncSpinner.succeed(`Project synced to Genbox (${result.action})`);
1691
+ if (result.changes && result.changes.length > 0) {
1692
+ console.log(chalk_1.default.dim(` Changes: ${result.changes.join(', ')}`));
1693
+ }
1694
+ }
1695
+ catch (error) {
1696
+ if ((0, api_1.isAuthError)(error)) {
1697
+ syncSpinner.warn('Not logged in - project not synced');
1698
+ console.log(chalk_1.default.dim(' Run `genbox login` to sync projects to your account'));
1699
+ }
1700
+ else {
1701
+ syncSpinner.warn(`Could not sync project - ${error.message}`);
1702
+ }
1703
+ }
1704
+ }
1705
+ else {
1706
+ console.log(chalk_1.default.dim(' Project not synced (not logged in). Run `genbox login` to sync.'));
1707
+ }
1708
+ // =========================================
1607
1709
  // Show Instructions
1608
1710
  // =========================================
1609
1711
  const hasBackend = Object.values(detected.apps).some(a => a.type === 'backend' || a.type === 'gateway');