genbox 1.0.64 → 1.0.66

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.
@@ -43,15 +43,15 @@ const chalk_1 = __importDefault(require("chalk"));
43
43
  const ora_1 = __importDefault(require("ora"));
44
44
  const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
- const os = __importStar(require("os"));
46
+ const child_process_1 = require("child_process");
47
47
  const config_loader_1 = require("../config-loader");
48
48
  const profile_resolver_1 = require("../profile-resolver");
49
49
  const api_1 = require("../api");
50
50
  const ssh_config_1 = require("../ssh-config");
51
51
  const schema_v4_1 = require("../schema-v4");
52
- const child_process_1 = require("child_process");
53
52
  const random_name_1 = require("../random-name");
54
53
  const db_utils_1 = require("../db-utils");
54
+ const utils_1 = require("../utils");
55
55
  // Credits consumed per hour for each size (matches API billing.config.ts)
56
56
  const CREDITS_PER_HOUR = {
57
57
  cx22: 1,
@@ -104,55 +104,6 @@ async function provisionGenbox(payload) {
104
104
  body: JSON.stringify(payload),
105
105
  });
106
106
  }
107
- function getPublicSshKey() {
108
- const home = os.homedir();
109
- const potentialKeys = [
110
- path.join(home, '.ssh', 'id_ed25519.pub'),
111
- path.join(home, '.ssh', 'id_rsa.pub'),
112
- ];
113
- for (const keyPath of potentialKeys) {
114
- if (fs.existsSync(keyPath)) {
115
- const content = fs.readFileSync(keyPath, 'utf-8').trim();
116
- if (content)
117
- return content;
118
- }
119
- }
120
- throw new Error('No public SSH key found in ~/.ssh/');
121
- }
122
- function getPrivateSshKey() {
123
- const home = os.homedir();
124
- const potentialKeys = [
125
- path.join(home, '.ssh', 'id_ed25519'),
126
- path.join(home, '.ssh', 'id_rsa'),
127
- ];
128
- for (const keyPath of potentialKeys) {
129
- if (fs.existsSync(keyPath)) {
130
- return fs.readFileSync(keyPath, 'utf-8');
131
- }
132
- }
133
- return undefined;
134
- }
135
- /**
136
- * Get local git config for commits on genbox
137
- */
138
- function getGitConfig() {
139
- const { execSync } = require('child_process');
140
- let userName;
141
- let userEmail;
142
- try {
143
- userName = execSync('git config --global user.name', { encoding: 'utf-8' }).trim();
144
- }
145
- catch {
146
- // Git config not set
147
- }
148
- try {
149
- userEmail = execSync('git config --global user.email', { encoding: 'utf-8' }).trim();
150
- }
151
- catch {
152
- // Git config not set
153
- }
154
- return { userName, userEmail };
155
- }
156
107
  /**
157
108
  * Prompt user for environment name
158
109
  */
@@ -343,7 +294,7 @@ exports.createCommand = new commander_1.Command('create')
343
294
  // Interactive branch selection if no branch options were specified
344
295
  // Skip if: -b (existing branch), -f (new branch from source), -n (explicit new branch name), or -y (skip prompts)
345
296
  if (!options.branch && !options.fromBranch && !options.newBranch && !options.yes && resolved.repos.length > 0) {
346
- resolved = await promptForBranchOptions(resolved, config, name);
297
+ resolved = await (0, utils_1.promptForBranchOptionsCreate)(resolved, config, name);
347
298
  }
348
299
  // Default behavior when -y (non-interactive) and no branch options: create new branch from configured default
349
300
  if (!options.branch && !options.fromBranch && !options.newBranch && options.yes && resolved.repos.length > 0) {
@@ -412,7 +363,7 @@ exports.createCommand = new commander_1.Command('create')
412
363
  }
413
364
  }
414
365
  // Get SSH keys
415
- const publicKey = getPublicSshKey();
366
+ const publicKey = (0, utils_1.getPublicSshKey)();
416
367
  // Check if SSH auth is needed for git
417
368
  let privateKeyContent;
418
369
  const v3Config = config;
@@ -424,7 +375,7 @@ exports.createCommand = new commander_1.Command('create')
424
375
  default: true,
425
376
  });
426
377
  if (injectKey) {
427
- privateKeyContent = getPrivateSshKey();
378
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
428
379
  if (privateKeyContent) {
429
380
  console.log(chalk_1.default.dim(' Using local SSH private key'));
430
381
  }
@@ -432,7 +383,7 @@ exports.createCommand = new commander_1.Command('create')
432
383
  }
433
384
  // Validate git credentials and warn if missing
434
385
  const envVarsForValidation = configLoader.loadEnvVars(process.cwd());
435
- const gitValidation = validateGitCredentials(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
386
+ const gitValidation = (0, utils_1.validateGitCredentials)(resolved.repos.map(r => ({ url: r.url, name: r.name })), envVarsForValidation.GIT_TOKEN, privateKeyContent);
436
387
  if (gitValidation.warnings.length > 0) {
437
388
  console.log('');
438
389
  console.log(chalk_1.default.yellow('⚠ Git Authentication Warnings:'));
@@ -736,225 +687,6 @@ function displayResolvedConfig(resolved) {
736
687
  console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
737
688
  console.log('');
738
689
  }
739
- /**
740
- * Prompt for branch options interactively
741
- * Default behavior: create new branch from configured default (or 'main') with branch name = environment name
742
- */
743
- async function promptForBranchOptions(resolved, config, envName) {
744
- // Get the default/base branch from config (used as source for new branches)
745
- const baseBranch = config.defaults?.branch || 'main';
746
- console.log(chalk_1.default.blue('=== Branch Configuration ==='));
747
- console.log('');
748
- const branchChoice = await prompts.select({
749
- message: 'Branch option:',
750
- choices: [
751
- {
752
- name: `${chalk_1.default.green('Create new branch')} '${chalk_1.default.cyan(envName)}' from '${baseBranch}' ${chalk_1.default.dim('(recommended)')}`,
753
- value: 'new-from-default',
754
- },
755
- {
756
- name: `Create new branch from a different source`,
757
- value: 'new-custom',
758
- },
759
- {
760
- name: 'Use an existing branch',
761
- value: 'existing',
762
- },
763
- {
764
- name: `Use '${baseBranch}' directly without creating new branch`,
765
- value: 'default',
766
- },
767
- ],
768
- default: 'new-from-default',
769
- });
770
- if (branchChoice === 'new-from-default') {
771
- // Create new branch from configured default with name = environment name
772
- return {
773
- ...resolved,
774
- repos: resolved.repos.map(repo => ({
775
- ...repo,
776
- branch: envName,
777
- newBranch: envName,
778
- sourceBranch: baseBranch,
779
- })),
780
- };
781
- }
782
- if (branchChoice === 'new-custom') {
783
- const newBranchName = await prompts.input({
784
- message: 'New branch name:',
785
- default: envName,
786
- validate: (value) => {
787
- if (!value.trim())
788
- return 'Branch name is required';
789
- if (!/^[\w\-./]+$/.test(value))
790
- return 'Invalid branch name (use letters, numbers, -, _, /, .)';
791
- return true;
792
- },
793
- });
794
- const sourceBranch = await prompts.input({
795
- message: 'Create from branch:',
796
- default: 'main',
797
- validate: (value) => {
798
- if (!value.trim())
799
- return 'Source branch is required';
800
- return true;
801
- },
802
- });
803
- // Update all repos with new branch info
804
- return {
805
- ...resolved,
806
- repos: resolved.repos.map(repo => ({
807
- ...repo,
808
- branch: newBranchName.trim(),
809
- newBranch: newBranchName.trim(),
810
- sourceBranch: sourceBranch.trim(),
811
- })),
812
- };
813
- }
814
- if (branchChoice === 'existing') {
815
- const branchName = await prompts.input({
816
- message: 'Enter branch name:',
817
- default: baseBranch,
818
- validate: (value) => {
819
- if (!value.trim())
820
- return 'Branch name is required';
821
- return true;
822
- },
823
- });
824
- // Update all repos with the selected branch
825
- return {
826
- ...resolved,
827
- repos: resolved.repos.map(repo => ({
828
- ...repo,
829
- branch: branchName.trim(),
830
- newBranch: undefined,
831
- sourceBranch: undefined,
832
- })),
833
- };
834
- }
835
- if (branchChoice === 'default') {
836
- // Keep resolved repos as-is (no new branch)
837
- return resolved;
838
- }
839
- return resolved;
840
- }
841
- /**
842
- * Parse .env.genbox file into segregated sections
843
- */
844
- function parseEnvGenboxSections(content) {
845
- const sections = new Map();
846
- let currentSection = 'GLOBAL';
847
- let currentContent = [];
848
- for (const line of content.split('\n')) {
849
- const sectionMatch = line.match(/^# === ([^=]+) ===$/);
850
- if (sectionMatch) {
851
- // Save previous section
852
- if (currentContent.length > 0) {
853
- sections.set(currentSection, currentContent.join('\n').trim());
854
- }
855
- currentSection = sectionMatch[1].trim();
856
- currentContent = [];
857
- }
858
- else if (currentSection !== 'END') {
859
- currentContent.push(line);
860
- }
861
- }
862
- // Save last section
863
- if (currentContent.length > 0 && currentSection !== 'END') {
864
- sections.set(currentSection, currentContent.join('\n').trim());
865
- }
866
- return sections;
867
- }
868
- /**
869
- * Build a map of service URL variables based on connection type
870
- * e.g., if connectTo=staging: GATEWAY_URL → STAGING_GATEWAY_URL value
871
- */
872
- function buildServiceUrlMap(envVarsFromFile, connectTo) {
873
- const urlMap = {};
874
- const prefix = connectTo ? `${connectTo.toUpperCase()}_` : 'LOCAL_';
875
- // Find all service URL variables (LOCAL_*_URL and STAGING_*_URL patterns)
876
- const serviceNames = new Set();
877
- for (const key of Object.keys(envVarsFromFile)) {
878
- const match = key.match(/^(LOCAL|STAGING|PRODUCTION)_(.+_URL)$/);
879
- if (match) {
880
- serviceNames.add(match[2]);
881
- }
882
- }
883
- // Build mapping: VARNAME → value from appropriate prefix
884
- for (const serviceName of serviceNames) {
885
- const prefixedKey = `${prefix}${serviceName}`;
886
- const localKey = `LOCAL_${serviceName}`;
887
- // Use prefixed value if available, otherwise fall back to local
888
- const value = envVarsFromFile[prefixedKey] || envVarsFromFile[localKey];
889
- if (value) {
890
- urlMap[serviceName] = value;
891
- }
892
- }
893
- // Also handle legacy API_URL for backwards compatibility
894
- if (!urlMap['API_URL']) {
895
- const apiUrl = envVarsFromFile[`${prefix}API_URL`] ||
896
- envVarsFromFile['LOCAL_API_URL'] ||
897
- envVarsFromFile['STAGING_API_URL'];
898
- if (apiUrl) {
899
- urlMap['API_URL'] = apiUrl;
900
- }
901
- }
902
- return urlMap;
903
- }
904
- /**
905
- * Build env content for a specific app by combining GLOBAL + app-specific sections
906
- */
907
- function buildAppEnvContent(sections, appName, serviceUrlMap) {
908
- const parts = [];
909
- // Always include GLOBAL section
910
- const globalSection = sections.get('GLOBAL');
911
- if (globalSection) {
912
- parts.push(globalSection);
913
- }
914
- // Include app-specific section if exists
915
- const appSection = sections.get(appName);
916
- if (appSection) {
917
- parts.push(appSection);
918
- }
919
- let envContent = parts.join('\n\n');
920
- // Expand all ${VARNAME} references using the service URL map
921
- for (const [varName, value] of Object.entries(serviceUrlMap)) {
922
- const pattern = new RegExp(`\\$\\{${varName}\\}`, 'g');
923
- envContent = envContent.replace(pattern, value);
924
- }
925
- // Keep only actual env vars (filter out pure comment lines but keep var definitions)
926
- envContent = envContent
927
- .split('\n')
928
- .filter(line => {
929
- const trimmed = line.trim();
930
- // Keep empty lines, lines with = (even if commented), and non-comment lines
931
- return trimmed === '' || trimmed.includes('=') || !trimmed.startsWith('#');
932
- })
933
- .join('\n')
934
- .replace(/\n{3,}/g, '\n\n')
935
- .trim();
936
- return envContent;
937
- }
938
- /**
939
- * Validate git configuration and warn about missing credentials
940
- */
941
- function validateGitCredentials(repos, gitToken, privateKey) {
942
- const warnings = [];
943
- const errors = [];
944
- for (const repo of repos) {
945
- const isHttps = repo.url.startsWith('https://');
946
- const isSsh = repo.url.startsWith('git@') || repo.url.includes('ssh://');
947
- const isPrivateRepo = repo.url.includes('github.com') && !repo.url.includes('/public/');
948
- if (isHttps && !gitToken && isPrivateRepo) {
949
- warnings.push(`Repository '${repo.name}' uses HTTPS URL but GIT_TOKEN is not set.`);
950
- warnings.push(` Add GIT_TOKEN=<your-github-token> to .env.genbox for private repos.`);
951
- }
952
- if (isSsh && !privateKey) {
953
- warnings.push(`Repository '${repo.name}' uses SSH URL but no SSH key was injected.`);
954
- }
955
- }
956
- return { warnings, errors };
957
- }
958
690
  /**
959
691
  * Build API payload from resolved config
960
692
  */
@@ -977,22 +709,10 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
977
709
  if (fs.existsSync(envGenboxPath)) {
978
710
  const rawEnvContent = fs.readFileSync(envGenboxPath, 'utf-8');
979
711
  // Parse into sections
980
- const sections = parseEnvGenboxSections(rawEnvContent);
712
+ const sections = (0, utils_1.parseEnvGenboxSections)(rawEnvContent);
981
713
  // Parse GLOBAL section to get API URL values
982
714
  const globalSection = sections.get('GLOBAL') || '';
983
- const envVarsFromFile = {};
984
- for (const line of globalSection.split('\n')) {
985
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
986
- if (match) {
987
- let value = match[2].trim();
988
- // Remove quotes if present
989
- if ((value.startsWith('"') && value.endsWith('"')) ||
990
- (value.startsWith("'") && value.endsWith("'"))) {
991
- value = value.slice(1, -1);
992
- }
993
- envVarsFromFile[match[1]] = value;
994
- }
995
- }
715
+ const envVarsFromFile = (0, utils_1.parseEnvVarsFromSection)(globalSection);
996
716
  // Determine connection type from profile's connect_to (v3) or default_connection (v4)
997
717
  let connectTo;
998
718
  if (resolved.profile && config.profiles?.[resolved.profile]) {
@@ -1002,7 +722,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1002
722
  // Build service URL map for variable expansion
1003
723
  // This maps GATEWAY_URL → STAGING_GATEWAY_URL value (if connectTo=staging)
1004
724
  // or GATEWAY_URL → LOCAL_GATEWAY_URL value (if local)
1005
- const serviceUrlMap = buildServiceUrlMap(envVarsFromFile, connectTo);
725
+ const serviceUrlMap = (0, utils_1.buildServiceUrlMap)(envVarsFromFile, connectTo);
1006
726
  // Log what's being expanded for debugging
1007
727
  if (connectTo && Object.keys(serviceUrlMap).length > 0) {
1008
728
  console.log(chalk_1.default.dim(` Using ${connectTo} URLs for variable expansion`));
@@ -1019,7 +739,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1019
739
  for (const serviceSectionName of servicesSections) {
1020
740
  const serviceName = serviceSectionName.split('/')[1];
1021
741
  // Build service-specific env content (GLOBAL + service section)
1022
- const serviceEnvContent = buildAppEnvContent(sections, serviceSectionName, serviceUrlMap);
742
+ const serviceEnvContent = (0, utils_1.buildAppEnvContent)(sections, serviceSectionName, serviceUrlMap);
1023
743
  const stagingName = `${app.name}-${serviceName}.env`;
1024
744
  const targetPath = `${repoPath}/apps/${serviceName}/.env`;
1025
745
  files.push({
@@ -1032,7 +752,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1032
752
  }
1033
753
  else {
1034
754
  // Regular app - build app-specific env content (GLOBAL + app section)
1035
- const appEnvContent = buildAppEnvContent(sections, app.name, serviceUrlMap);
755
+ const appEnvContent = (0, utils_1.buildAppEnvContent)(sections, app.name, serviceUrlMap);
1036
756
  files.push({
1037
757
  path: `/home/dev/.env-staging/${app.name}.env`,
1038
758
  content: appEnvContent,
@@ -1071,7 +791,7 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
1071
791
  };
1072
792
  }
1073
793
  // Get local git config for commits
1074
- const gitConfig = getGitConfig();
794
+ const gitConfig = (0, utils_1.getGitConfig)();
1075
795
  // Load project cache to get project ID
1076
796
  const projectCache = loadProjectCache(process.cwd());
1077
797
  return {
@@ -1211,7 +931,7 @@ async function createLegacy(name, options) {
1211
931
  const { loadConfig, loadEnvVars } = await Promise.resolve().then(() => __importStar(require('../config')));
1212
932
  const config = loadConfig();
1213
933
  const size = options.size || config.system?.server_size || 'small';
1214
- const publicKey = getPublicSshKey();
934
+ const publicKey = (0, utils_1.getPublicSshKey)();
1215
935
  // Check if SSH auth needed
1216
936
  let privateKeyContent;
1217
937
  const usesSSH = config.git_auth?.method === 'ssh' ||
@@ -1222,7 +942,7 @@ async function createLegacy(name, options) {
1222
942
  default: true,
1223
943
  });
1224
944
  if (injectKey) {
1225
- privateKeyContent = getPrivateSshKey();
945
+ privateKeyContent = (0, utils_1.getPrivateSshKey)();
1226
946
  }
1227
947
  }
1228
948
  const spinner = (0, ora_1.default)(`Creating Genbox '${name}'...`).start();
@@ -147,7 +147,34 @@ exports.dbSyncCommand
147
147
  if (snapshotChoice === 'existing') {
148
148
  snapshotId = existingSnapshot._id;
149
149
  snapshotS3Key = existingSnapshot.s3Key;
150
- console.log(chalk_1.default.green(` ✓ Using existing snapshot`));
150
+ // Download the existing snapshot from S3
151
+ const downloadSpinner = (0, ora_1.default)('Downloading existing snapshot...').start();
152
+ try {
153
+ const downloadUrlResponse = await (0, api_1.getSnapshotDownloadUrl)(snapshotId);
154
+ const downloadResult = await (0, db_utils_1.downloadSnapshotFromS3)(downloadUrlResponse.downloadUrl, {
155
+ onProgress: (msg) => downloadSpinner.text = msg,
156
+ });
157
+ if (!downloadResult.success) {
158
+ downloadSpinner.fail(chalk_1.default.red('Failed to download snapshot'));
159
+ console.log(chalk_1.default.dim(` Error: ${downloadResult.error}`));
160
+ console.log(chalk_1.default.dim(' Creating fresh snapshot instead...'));
161
+ // Reset to create fresh
162
+ snapshotId = undefined;
163
+ snapshotS3Key = undefined;
164
+ }
165
+ else {
166
+ downloadSpinner.succeed(chalk_1.default.green('Snapshot downloaded'));
167
+ localDumpPath = downloadResult.dumpPath;
168
+ }
169
+ }
170
+ catch (error) {
171
+ downloadSpinner.fail(chalk_1.default.red('Failed to download snapshot'));
172
+ console.log(chalk_1.default.dim(` Error: ${error.message}`));
173
+ console.log(chalk_1.default.dim(' Creating fresh snapshot instead...'));
174
+ // Reset to create fresh
175
+ snapshotId = undefined;
176
+ snapshotS3Key = undefined;
177
+ }
151
178
  }
152
179
  }
153
180
  }
@@ -215,20 +242,37 @@ exports.dbSyncCommand
215
242
  }
216
243
  }
217
244
  }
218
- // Trigger restore on genbox
219
- if (!snapshotId || !snapshotS3Key) {
220
- console.log(chalk_1.default.red('No snapshot available to restore'));
245
+ // Restore directly via SCP/SSH (user's own SSH key, more reliable than API async job)
246
+ if (!localDumpPath) {
247
+ console.log(chalk_1.default.red('No dump file available to restore'));
221
248
  return;
222
249
  }
223
- const restoreSpinner = (0, ora_1.default)('Restoring database on genbox...').start();
250
+ const ipAddress = genbox.ipAddress;
251
+ if (!ipAddress) {
252
+ console.log(chalk_1.default.red('Genbox has no IP address'));
253
+ return;
254
+ }
255
+ const restoreSpinner = (0, ora_1.default)('Uploading dump to genbox...').start();
224
256
  try {
225
- await (0, api_1.fetchApi)(`/genboxes/${genboxId}/db/restore`, {
226
- method: 'POST',
227
- body: JSON.stringify({
228
- snapshotId,
229
- s3Key: snapshotS3Key,
230
- }),
257
+ // SCP dump file to genbox
258
+ const uploadResult = await (0, db_utils_1.uploadDumpToGenbox)(localDumpPath, ipAddress, {
259
+ onProgress: (msg) => restoreSpinner.text = msg,
231
260
  });
261
+ if (!uploadResult.success) {
262
+ restoreSpinner.fail(chalk_1.default.red('Upload failed'));
263
+ console.error(chalk_1.default.red(` Error: ${uploadResult.error}`));
264
+ return;
265
+ }
266
+ restoreSpinner.text = 'Restoring database...';
267
+ // SSH to run mongorestore with dynamic port detection
268
+ const restoreResult = await (0, db_utils_1.runRemoteMongoRestoreDynamic)(ipAddress, {
269
+ onProgress: (msg) => restoreSpinner.text = msg,
270
+ });
271
+ if (!restoreResult.success) {
272
+ restoreSpinner.fail(chalk_1.default.red('Restore failed'));
273
+ console.error(chalk_1.default.red(` Error: ${restoreResult.error}`));
274
+ return;
275
+ }
232
276
  restoreSpinner.succeed(chalk_1.default.green('Database sync completed!'));
233
277
  console.log('');
234
278
  console.log(chalk_1.default.dim(` Database has been restored from ${source} snapshot.`));
@@ -236,9 +280,6 @@ exports.dbSyncCommand
236
280
  catch (error) {
237
281
  restoreSpinner.fail(chalk_1.default.red('Database restore failed'));
238
282
  console.error(chalk_1.default.red(` Error: ${error.message}`));
239
- if (error instanceof api_1.AuthenticationError) {
240
- console.log(chalk_1.default.yellow('\nRun: genbox login'));
241
- }
242
283
  }
243
284
  }
244
285
  catch (error) {