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 +62 -0
- package/dist/commands/create.js +189 -9
- package/dist/commands/db-sync.js +1 -1
- package/dist/commands/help.js +15 -13
- package/dist/commands/init.js +103 -1
- package/dist/commands/migrate.js +18 -215
- package/dist/commands/profiles.js +5 -11
- package/dist/commands/push.js +70 -32
- package/dist/commands/rebuild.js +3 -8
- package/dist/commands/validate.js +13 -13
- package/dist/config-explainer.js +1 -1
- package/dist/config-loader.js +74 -150
- package/dist/db-utils.js +416 -0
- package/dist/index.js +1 -1
- package/dist/profile-resolver.js +35 -34
- package/dist/schema-v4.js +35 -35
- package/dist/strict-mode.js +57 -126
- package/package.json +3 -2
- package/dist/migration.js +0 -335
- package/dist/schema-v3.js +0 -48
package/dist/db-utils.js
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Utilities
|
|
4
|
+
*
|
|
5
|
+
* Handles local mongodump/mongorestore operations and
|
|
6
|
+
* uploading database dumps to genboxes.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.isMongoDumpAvailable = isMongoDumpAvailable;
|
|
46
|
+
exports.getMongoDumpInstallInstructions = getMongoDumpInstallInstructions;
|
|
47
|
+
exports.runLocalMongoDump = runLocalMongoDump;
|
|
48
|
+
exports.uploadDumpToGenbox = uploadDumpToGenbox;
|
|
49
|
+
exports.runRemoteMongoRestore = runRemoteMongoRestore;
|
|
50
|
+
exports.cleanupDump = cleanupDump;
|
|
51
|
+
exports.formatBytes = formatBytes;
|
|
52
|
+
exports.waitForSshAccess = waitForSshAccess;
|
|
53
|
+
exports.uploadDumpToS3 = uploadDumpToS3;
|
|
54
|
+
exports.createAndUploadSnapshot = createAndUploadSnapshot;
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const os = __importStar(require("os"));
|
|
59
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
60
|
+
/**
|
|
61
|
+
* Check if mongodump is available locally
|
|
62
|
+
*/
|
|
63
|
+
function isMongoDumpAvailable() {
|
|
64
|
+
try {
|
|
65
|
+
(0, child_process_1.execSync)('mongodump --version', { stdio: 'ignore' });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get instructions for installing mongodb-database-tools
|
|
74
|
+
*/
|
|
75
|
+
function getMongoDumpInstallInstructions() {
|
|
76
|
+
const platform = process.platform;
|
|
77
|
+
if (platform === 'darwin') {
|
|
78
|
+
return `Install MongoDB Database Tools:
|
|
79
|
+
${chalk_1.default.cyan('brew tap mongodb/brew')}
|
|
80
|
+
${chalk_1.default.cyan('brew install mongodb-database-tools')}`;
|
|
81
|
+
}
|
|
82
|
+
else if (platform === 'linux') {
|
|
83
|
+
return `Install MongoDB Database Tools:
|
|
84
|
+
${chalk_1.default.cyan('# For Ubuntu/Debian:')}
|
|
85
|
+
${chalk_1.default.cyan('wget -qO- https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg')}
|
|
86
|
+
${chalk_1.default.cyan('echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list')}
|
|
87
|
+
${chalk_1.default.cyan('sudo apt-get update && sudo apt-get install -y mongodb-database-tools')}`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return `Install MongoDB Database Tools from:
|
|
91
|
+
${chalk_1.default.cyan('https://www.mongodb.com/try/download/database-tools')}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run mongodump locally
|
|
96
|
+
*/
|
|
97
|
+
async function runLocalMongoDump(sourceUrl, options = {}) {
|
|
98
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'genbox-dbdump-'));
|
|
99
|
+
const dumpPath = path.join(tempDir, 'dump.gz');
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const args = [
|
|
102
|
+
`--uri=${sourceUrl}`,
|
|
103
|
+
`--archive=${dumpPath}`,
|
|
104
|
+
'--gzip',
|
|
105
|
+
];
|
|
106
|
+
// Add collection filters
|
|
107
|
+
if (options.collections && options.collections.length > 0) {
|
|
108
|
+
for (const col of options.collections) {
|
|
109
|
+
args.push(`--collection=${col}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (options.excludeCollections && options.excludeCollections.length > 0) {
|
|
113
|
+
for (const col of options.excludeCollections) {
|
|
114
|
+
args.push(`--excludeCollection=${col}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
options.onProgress?.('Starting mongodump...');
|
|
118
|
+
const proc = (0, child_process_1.spawn)('mongodump', args, {
|
|
119
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
|
+
});
|
|
121
|
+
let stderr = '';
|
|
122
|
+
proc.stderr?.on('data', (data) => {
|
|
123
|
+
const line = data.toString();
|
|
124
|
+
stderr += line;
|
|
125
|
+
// Parse progress from mongodump output
|
|
126
|
+
if (line.includes('done dumping')) {
|
|
127
|
+
options.onProgress?.(line.trim());
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
proc.on('close', (code) => {
|
|
131
|
+
if (code === 0 && fs.existsSync(dumpPath)) {
|
|
132
|
+
const stats = fs.statSync(dumpPath);
|
|
133
|
+
resolve({
|
|
134
|
+
success: true,
|
|
135
|
+
dumpPath,
|
|
136
|
+
sizeBytes: stats.size,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Cleanup on failure
|
|
141
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
142
|
+
// Parse common errors
|
|
143
|
+
let errorMessage = 'mongodump failed';
|
|
144
|
+
if (stderr.includes('authentication failed')) {
|
|
145
|
+
errorMessage = 'Authentication failed - check your database credentials';
|
|
146
|
+
}
|
|
147
|
+
else if (stderr.includes('connection refused') || stderr.includes('no reachable servers')) {
|
|
148
|
+
errorMessage = 'Connection failed - check if the database URL is correct and accessible';
|
|
149
|
+
}
|
|
150
|
+
else if (stderr.includes('timed out')) {
|
|
151
|
+
errorMessage = 'Connection timed out - the database may not be accessible from your network';
|
|
152
|
+
}
|
|
153
|
+
else if (stderr) {
|
|
154
|
+
errorMessage = stderr.split('\n')[0] || errorMessage;
|
|
155
|
+
}
|
|
156
|
+
resolve({
|
|
157
|
+
success: false,
|
|
158
|
+
error: errorMessage,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
proc.on('error', (err) => {
|
|
163
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
164
|
+
resolve({
|
|
165
|
+
success: false,
|
|
166
|
+
error: `Failed to run mongodump: ${err.message}`,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Upload dump file to genbox via SCP
|
|
173
|
+
*/
|
|
174
|
+
async function uploadDumpToGenbox(dumpPath, ipAddress, options = {}) {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const remotePath = '/home/dev/.db-dump.gz';
|
|
177
|
+
options.onProgress?.(`Uploading dump to genbox (${formatBytes(fs.statSync(dumpPath).size)})...`);
|
|
178
|
+
const proc = (0, child_process_1.spawn)('scp', [
|
|
179
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
180
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
181
|
+
'-o', 'ConnectTimeout=30',
|
|
182
|
+
dumpPath,
|
|
183
|
+
`dev@${ipAddress}:${remotePath}`,
|
|
184
|
+
], {
|
|
185
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
186
|
+
});
|
|
187
|
+
let stderr = '';
|
|
188
|
+
proc.stderr?.on('data', (data) => {
|
|
189
|
+
stderr += data.toString();
|
|
190
|
+
});
|
|
191
|
+
proc.on('close', (code) => {
|
|
192
|
+
if (code === 0) {
|
|
193
|
+
resolve({ success: true });
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
let errorMessage = 'SCP upload failed';
|
|
197
|
+
if (stderr.includes('Connection refused') || stderr.includes('Connection timed out')) {
|
|
198
|
+
errorMessage = 'Cannot connect to genbox - it may still be provisioning';
|
|
199
|
+
}
|
|
200
|
+
else if (stderr.includes('Permission denied')) {
|
|
201
|
+
errorMessage = 'Permission denied - SSH key may not be configured';
|
|
202
|
+
}
|
|
203
|
+
else if (stderr) {
|
|
204
|
+
errorMessage = stderr.split('\n')[0] || errorMessage;
|
|
205
|
+
}
|
|
206
|
+
resolve({ success: false, error: errorMessage });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
proc.on('error', (err) => {
|
|
210
|
+
resolve({ success: false, error: `Failed to run scp: ${err.message}` });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Run mongorestore on genbox via SSH
|
|
216
|
+
*/
|
|
217
|
+
async function runRemoteMongoRestore(ipAddress, dbName, options = {}) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
options.onProgress?.('Restoring database on genbox...');
|
|
220
|
+
// The restore command - uses the uploaded dump file
|
|
221
|
+
const restoreCmd = `
|
|
222
|
+
# Wait for MongoDB to be ready
|
|
223
|
+
for i in {1..30}; do
|
|
224
|
+
if docker exec goodpass-mongodb mongosh --quiet --eval "db.runCommand({ping:1})" 2>/dev/null; then
|
|
225
|
+
break
|
|
226
|
+
fi
|
|
227
|
+
echo "Waiting for MongoDB... ($i/30)"
|
|
228
|
+
sleep 2
|
|
229
|
+
done
|
|
230
|
+
|
|
231
|
+
# Restore the database
|
|
232
|
+
if [ -f /home/dev/.db-dump.gz ]; then
|
|
233
|
+
echo "Restoring database..."
|
|
234
|
+
mongorestore --uri="mongodb://localhost:27017" --db="${dbName}" --archive=/home/dev/.db-dump.gz --gzip --drop
|
|
235
|
+
rm -f /home/dev/.db-dump.gz
|
|
236
|
+
echo "Database restored successfully"
|
|
237
|
+
else
|
|
238
|
+
echo "Error: Dump file not found"
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
`;
|
|
242
|
+
const proc = (0, child_process_1.spawn)('ssh', [
|
|
243
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
244
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
245
|
+
'-o', 'ConnectTimeout=30',
|
|
246
|
+
`dev@${ipAddress}`,
|
|
247
|
+
'bash', '-c', `'${restoreCmd.replace(/'/g, "'\\''")}'`,
|
|
248
|
+
], {
|
|
249
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
250
|
+
});
|
|
251
|
+
let stdout = '';
|
|
252
|
+
let stderr = '';
|
|
253
|
+
proc.stdout?.on('data', (data) => {
|
|
254
|
+
const line = data.toString();
|
|
255
|
+
stdout += line;
|
|
256
|
+
if (line.includes('Restoring') || line.includes('restored')) {
|
|
257
|
+
options.onProgress?.(line.trim());
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
proc.stderr?.on('data', (data) => {
|
|
261
|
+
stderr += data.toString();
|
|
262
|
+
});
|
|
263
|
+
proc.on('close', (code) => {
|
|
264
|
+
if (code === 0) {
|
|
265
|
+
resolve({ success: true });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
resolve({
|
|
269
|
+
success: false,
|
|
270
|
+
error: stderr || stdout || 'Restore failed',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
proc.on('error', (err) => {
|
|
275
|
+
resolve({ success: false, error: `Failed to run ssh: ${err.message}` });
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Clean up temporary dump files
|
|
281
|
+
*/
|
|
282
|
+
function cleanupDump(dumpPath) {
|
|
283
|
+
try {
|
|
284
|
+
const dir = path.dirname(dumpPath);
|
|
285
|
+
if (dir.includes('genbox-dbdump-')) {
|
|
286
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
fs.rmSync(dumpPath, { force: true });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Ignore cleanup errors
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Format bytes to human readable
|
|
298
|
+
*/
|
|
299
|
+
function formatBytes(bytes) {
|
|
300
|
+
if (bytes < 1024)
|
|
301
|
+
return `${bytes} B`;
|
|
302
|
+
if (bytes < 1024 * 1024)
|
|
303
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
304
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
305
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
306
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Wait for genbox to be accessible via SSH
|
|
310
|
+
*/
|
|
311
|
+
async function waitForSshAccess(ipAddress, maxWaitSeconds = 300, onProgress) {
|
|
312
|
+
const startTime = Date.now();
|
|
313
|
+
const checkInterval = 5000; // 5 seconds
|
|
314
|
+
while (Date.now() - startTime < maxWaitSeconds * 1000) {
|
|
315
|
+
try {
|
|
316
|
+
(0, child_process_1.execSync)(`ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 dev@${ipAddress} echo ok`, { stdio: 'ignore' });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
321
|
+
onProgress?.(`Waiting for genbox to be ready (${elapsed}s)...`);
|
|
322
|
+
await new Promise((r) => setTimeout(r, checkInterval));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Upload dump file to S3 using pre-signed URL
|
|
329
|
+
*/
|
|
330
|
+
async function uploadDumpToS3(dumpPath, uploadUrl, options = {}) {
|
|
331
|
+
const fileSize = fs.statSync(dumpPath).size;
|
|
332
|
+
options.onProgress?.(`Uploading snapshot to cloud (${formatBytes(fileSize)})...`, 0);
|
|
333
|
+
try {
|
|
334
|
+
// Read file as buffer
|
|
335
|
+
const fileBuffer = fs.readFileSync(dumpPath);
|
|
336
|
+
// Upload to S3 using pre-signed URL
|
|
337
|
+
const response = await fetch(uploadUrl, {
|
|
338
|
+
method: 'PUT',
|
|
339
|
+
headers: {
|
|
340
|
+
'Content-Type': 'application/gzip',
|
|
341
|
+
'Content-Length': fileSize.toString(),
|
|
342
|
+
},
|
|
343
|
+
body: fileBuffer,
|
|
344
|
+
});
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
const errorText = await response.text();
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
error: `Upload failed: ${response.status} ${response.statusText} - ${errorText}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
options.onProgress?.('Upload complete', 100);
|
|
353
|
+
return { success: true };
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: `Upload failed: ${error.message}`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function createAndUploadSnapshot(dumpPath, projectId, source, options = {}) {
|
|
363
|
+
// Import API functions dynamically to avoid circular deps
|
|
364
|
+
const { initiateSnapshotUpload, completeSnapshotUpload, failSnapshotUpload, } = await Promise.resolve().then(() => __importStar(require('./api')));
|
|
365
|
+
const fileSize = fs.statSync(dumpPath).size;
|
|
366
|
+
const snapshotName = options.name || `${source}-${new Date().toISOString().split('T')[0]}`;
|
|
367
|
+
let snapshotId;
|
|
368
|
+
try {
|
|
369
|
+
// Step 1: Initiate upload to get pre-signed URL
|
|
370
|
+
options.onProgress?.('Preparing snapshot upload...');
|
|
371
|
+
const initResult = await initiateSnapshotUpload({
|
|
372
|
+
name: snapshotName,
|
|
373
|
+
projectId,
|
|
374
|
+
source,
|
|
375
|
+
sizeBytes: fileSize,
|
|
376
|
+
contentType: 'application/gzip',
|
|
377
|
+
});
|
|
378
|
+
snapshotId = initResult.snapshotId;
|
|
379
|
+
// Step 2: Upload to S3
|
|
380
|
+
const uploadResult = await uploadDumpToS3(dumpPath, initResult.uploadUrl, {
|
|
381
|
+
onProgress: options.onProgress,
|
|
382
|
+
});
|
|
383
|
+
if (!uploadResult.success) {
|
|
384
|
+
// Mark as failed
|
|
385
|
+
await failSnapshotUpload(snapshotId, uploadResult.error || 'Upload failed');
|
|
386
|
+
return { success: false, error: uploadResult.error };
|
|
387
|
+
}
|
|
388
|
+
// Step 3: Complete the upload
|
|
389
|
+
options.onProgress?.('Finalizing snapshot...');
|
|
390
|
+
await completeSnapshotUpload(snapshotId, {
|
|
391
|
+
sizeBytes: fileSize,
|
|
392
|
+
compressedSizeBytes: fileSize, // Already gzipped
|
|
393
|
+
sourceUrl: options.sourceUrl,
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
success: true,
|
|
397
|
+
snapshotId,
|
|
398
|
+
s3Key: initResult.s3Key,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
// Try to mark as failed if we have a snapshot ID
|
|
403
|
+
if (snapshotId) {
|
|
404
|
+
try {
|
|
405
|
+
await failSnapshotUpload(snapshotId, error.message);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Ignore cleanup error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
success: false,
|
|
413
|
+
error: error.message,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const program = new commander_1.Command();
|
|
|
8
8
|
const { version } = require('../package.json');
|
|
9
9
|
program
|
|
10
10
|
.name('genbox')
|
|
11
|
-
.description('Genbox CLI - AI-Powered Development Environments')
|
|
11
|
+
.description('Genbox CLI - AI-Powered Development Environments\n\nTIP: Use "gb" as a shorthand (e.g., gb list, gb create)')
|
|
12
12
|
.version(version, '-v, --version', 'Output the current version');
|
|
13
13
|
const create_1 = require("./commands/create");
|
|
14
14
|
const list_1 = require("./commands/list");
|
package/dist/profile-resolver.js
CHANGED
|
@@ -60,7 +60,6 @@ class ProfileResolver {
|
|
|
60
60
|
*/
|
|
61
61
|
async resolve(config, options) {
|
|
62
62
|
const warnings = [];
|
|
63
|
-
const isV4Config = (0, config_loader_1.isV4)(config);
|
|
64
63
|
// Step 1: Get base profile (if specified)
|
|
65
64
|
let profile = {};
|
|
66
65
|
if (options.profile) {
|
|
@@ -193,12 +192,13 @@ class ProfileResolver {
|
|
|
193
192
|
const dependencies = {};
|
|
194
193
|
const infrastructure = (0, config_loader_1.getInfrastructure)(config);
|
|
195
194
|
// Process each dependency (v3: requires, v4: connects_to)
|
|
196
|
-
const appDeps =
|
|
195
|
+
const appDeps = true
|
|
197
196
|
? Object.entries(appConfig.connects_to || {})
|
|
198
|
-
: Object.entries(appConfig.
|
|
197
|
+
: Object.entries(appConfig.connects_to || {});
|
|
199
198
|
for (const [depName, requirement] of appDeps) {
|
|
200
|
-
//
|
|
201
|
-
|
|
199
|
+
// Skip if not required
|
|
200
|
+
const reqObj = requirement;
|
|
201
|
+
if (!reqObj.required)
|
|
202
202
|
continue;
|
|
203
203
|
// Check if dependency is already in selected apps
|
|
204
204
|
const isLocalApp = selectedApps.includes(depName);
|
|
@@ -218,12 +218,12 @@ class ProfileResolver {
|
|
|
218
218
|
url,
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
|
-
else if (
|
|
221
|
+
else if (reqObj.required && !options.yes) {
|
|
222
222
|
// Prompt user for resolution
|
|
223
223
|
const resolution = await this.promptDependencyResolution(config, appName, depName, isInfra ? 'infrastructure' : 'app');
|
|
224
224
|
dependencies[depName] = resolution;
|
|
225
225
|
}
|
|
226
|
-
else if (
|
|
226
|
+
else if (!reqObj.required) {
|
|
227
227
|
// Skip optional dependencies if not explicitly included
|
|
228
228
|
continue;
|
|
229
229
|
}
|
|
@@ -250,13 +250,13 @@ class ProfileResolver {
|
|
|
250
250
|
type,
|
|
251
251
|
framework,
|
|
252
252
|
port,
|
|
253
|
-
services: appConfig.services,
|
|
254
253
|
commands: appConfig.commands,
|
|
255
254
|
env: appConfig.env,
|
|
256
255
|
runner,
|
|
257
256
|
docker,
|
|
258
257
|
healthcheck,
|
|
259
258
|
dependsOn,
|
|
259
|
+
connections: [], // Will be populated later if needed
|
|
260
260
|
dependencies,
|
|
261
261
|
};
|
|
262
262
|
}
|
|
@@ -314,26 +314,25 @@ class ProfileResolver {
|
|
|
314
314
|
* Get URL for a dependency from environment config
|
|
315
315
|
*/
|
|
316
316
|
getUrlForDependency(depName, envConfig) {
|
|
317
|
-
// Check
|
|
318
|
-
|
|
319
|
-
// Check common fields: url, gateway, api, or first string value
|
|
320
|
-
const apiConfig = envConfig.api;
|
|
321
|
-
return apiConfig.url || apiConfig.gateway || apiConfig.api ||
|
|
322
|
-
Object.values(apiConfig).find(v => typeof v === 'string' && v.startsWith('http'));
|
|
323
|
-
}
|
|
324
|
-
// Check infrastructure
|
|
325
|
-
const infraConfig = envConfig[depName];
|
|
326
|
-
return infraConfig?.url;
|
|
317
|
+
// Check the urls map in the environment config
|
|
318
|
+
return envConfig.urls?.[depName];
|
|
327
319
|
}
|
|
328
320
|
/**
|
|
329
321
|
* Resolve database mode
|
|
330
322
|
*/
|
|
331
323
|
async resolveDatabaseMode(config, options, profile) {
|
|
324
|
+
// Helper to get MongoDB URL for an environment
|
|
325
|
+
const getMongoUrl = (source) => {
|
|
326
|
+
const envConfig = config.environments?.[source];
|
|
327
|
+
return envConfig?.urls?.mongodb;
|
|
328
|
+
};
|
|
332
329
|
// CLI flag takes precedence
|
|
333
330
|
if (options.db) {
|
|
331
|
+
const source = options.dbSource || profile.database?.source || 'staging';
|
|
334
332
|
return {
|
|
335
333
|
mode: options.db,
|
|
336
|
-
source
|
|
334
|
+
source,
|
|
335
|
+
url: options.db === 'copy' ? getMongoUrl(source) : undefined,
|
|
337
336
|
};
|
|
338
337
|
}
|
|
339
338
|
// Profile setting
|
|
@@ -341,16 +340,16 @@ class ProfileResolver {
|
|
|
341
340
|
return {
|
|
342
341
|
mode: profile.database.mode,
|
|
343
342
|
source: profile.database.source,
|
|
343
|
+
url: profile.database.mode === 'copy' && profile.database.source
|
|
344
|
+
? getMongoUrl(profile.database.source)
|
|
345
|
+
: undefined,
|
|
344
346
|
};
|
|
345
347
|
}
|
|
346
|
-
// If
|
|
348
|
+
// If default_connection is set, use remote
|
|
347
349
|
const profileConnection = (0, config_loader_1.getProfileConnection)(profile);
|
|
348
350
|
if (profileConnection) {
|
|
349
351
|
const envConfig = config.environments?.[profileConnection];
|
|
350
|
-
|
|
351
|
-
const mongoUrl = (0, config_loader_1.isV4)(config)
|
|
352
|
-
? envConfig?.urls?.mongodb
|
|
353
|
-
: envConfig?.mongodb?.url;
|
|
352
|
+
const mongoUrl = envConfig?.urls?.mongodb;
|
|
354
353
|
return {
|
|
355
354
|
mode: 'remote',
|
|
356
355
|
source: profileConnection,
|
|
@@ -362,9 +361,12 @@ class ProfileResolver {
|
|
|
362
361
|
return await this.selectDatabaseModeInteractive(config);
|
|
363
362
|
}
|
|
364
363
|
// Default
|
|
364
|
+
const defaultMode = config.defaults?.database?.mode || 'local';
|
|
365
|
+
const defaultSource = config.defaults?.database?.source;
|
|
365
366
|
return {
|
|
366
|
-
mode:
|
|
367
|
-
source:
|
|
367
|
+
mode: defaultMode,
|
|
368
|
+
source: defaultSource,
|
|
369
|
+
url: defaultMode === 'copy' && defaultSource ? getMongoUrl(defaultSource) : undefined,
|
|
368
370
|
};
|
|
369
371
|
}
|
|
370
372
|
/**
|
|
@@ -403,15 +405,14 @@ class ProfileResolver {
|
|
|
403
405
|
}
|
|
404
406
|
else if (answer.startsWith('copy-')) {
|
|
405
407
|
const source = answer.replace('copy-', '');
|
|
406
|
-
|
|
408
|
+
const envConfig = config.environments?.[source];
|
|
409
|
+
const mongoUrl = envConfig?.urls?.mongodb;
|
|
410
|
+
return { mode: 'copy', source, url: mongoUrl };
|
|
407
411
|
}
|
|
408
412
|
else if (answer.startsWith('remote-')) {
|
|
409
413
|
const source = answer.replace('remote-', '');
|
|
410
414
|
const envConfig = config.environments?.[source];
|
|
411
|
-
|
|
412
|
-
const mongoUrl = (0, config_loader_1.isV4)(config)
|
|
413
|
-
? envConfig?.urls?.mongodb
|
|
414
|
-
: envConfig?.mongodb?.url;
|
|
415
|
+
const mongoUrl = envConfig?.urls?.mongodb;
|
|
415
416
|
return {
|
|
416
417
|
mode: 'remote',
|
|
417
418
|
source,
|
|
@@ -427,9 +428,9 @@ class ProfileResolver {
|
|
|
427
428
|
const localApps = apps.length;
|
|
428
429
|
const localInfra = infrastructure.filter(i => i.mode === 'local').length;
|
|
429
430
|
const total = localApps + localInfra;
|
|
430
|
-
// Check for heavy
|
|
431
|
-
const
|
|
432
|
-
if (total > 8 ||
|
|
431
|
+
// Check for heavy apps (many dependencies)
|
|
432
|
+
const hasHeavyApp = apps.some(a => Object.keys(a.dependencies).length > 3);
|
|
433
|
+
if (total > 8 || hasHeavyApp)
|
|
433
434
|
return 'xl';
|
|
434
435
|
if (total > 5)
|
|
435
436
|
return 'large';
|
package/dist/schema-v4.js
CHANGED
|
@@ -2,53 +2,53 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Genbox Configuration Schema v4
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - `provides` section for infrastructure definitions
|
|
8
|
-
* - `$detect` markers for opt-in auto-detection
|
|
9
|
-
* - Strict mode by default (no implicit inference)
|
|
10
|
-
* - Better separation of concerns
|
|
5
|
+
* This is the ONLY supported schema version.
|
|
6
|
+
* All legacy v2/v3 support has been removed.
|
|
11
7
|
*/
|
|
12
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.
|
|
14
|
-
exports.
|
|
9
|
+
exports.DETECTABLE_APP_FIELDS = exports.DETECT_MARKER = void 0;
|
|
10
|
+
exports.isDetectMarker = isDetectMarker;
|
|
11
|
+
exports.isValidConfig = isValidConfig;
|
|
15
12
|
exports.getConfigVersion = getConfigVersion;
|
|
16
13
|
// ============================================
|
|
17
|
-
//
|
|
14
|
+
// $detect Marker Support
|
|
18
15
|
// ============================================
|
|
19
16
|
/**
|
|
20
|
-
*
|
|
17
|
+
* Special marker indicating a value should be auto-detected.
|
|
21
18
|
*/
|
|
22
|
-
|
|
19
|
+
exports.DETECT_MARKER = '$detect';
|
|
20
|
+
/**
|
|
21
|
+
* List of fields that support $detect markers in AppConfig
|
|
22
|
+
*/
|
|
23
|
+
exports.DETECTABLE_APP_FIELDS = [
|
|
24
|
+
'type',
|
|
25
|
+
'framework',
|
|
26
|
+
'port',
|
|
27
|
+
'commands.install',
|
|
28
|
+
'commands.dev',
|
|
29
|
+
'commands.build',
|
|
30
|
+
'commands.start',
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Check if a value is a $detect marker
|
|
34
|
+
*/
|
|
35
|
+
function isDetectMarker(value) {
|
|
36
|
+
return value === exports.DETECT_MARKER;
|
|
37
|
+
}
|
|
38
|
+
// ============================================
|
|
39
|
+
// Helper Functions
|
|
40
|
+
// ============================================
|
|
41
|
+
/**
|
|
42
|
+
* Check if a config is valid v4
|
|
43
|
+
*/
|
|
44
|
+
function isValidConfig(config) {
|
|
23
45
|
return config.version === 4;
|
|
24
46
|
}
|
|
25
47
|
/**
|
|
26
|
-
* Get the version of a config
|
|
48
|
+
* Get the version of a config (only v4 is valid now)
|
|
27
49
|
*/
|
|
28
50
|
function getConfigVersion(config) {
|
|
29
51
|
if (config.version === 4)
|
|
30
52
|
return 4;
|
|
31
|
-
|
|
32
|
-
return 3;
|
|
33
|
-
return 'unknown';
|
|
53
|
+
return 'invalid';
|
|
34
54
|
}
|
|
35
|
-
/**
|
|
36
|
-
* List of deprecated fields from v3
|
|
37
|
-
*/
|
|
38
|
-
exports.DEPRECATED_V3_FIELDS = [
|
|
39
|
-
{
|
|
40
|
-
path: 'apps.*.requires',
|
|
41
|
-
replacement: 'apps.*.connects_to',
|
|
42
|
-
message: 'Use connects_to with explicit connection modes',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
path: 'infrastructure',
|
|
46
|
-
replacement: 'provides',
|
|
47
|
-
message: 'Renamed to provides for clarity',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
path: 'profiles.*.connect_to',
|
|
51
|
-
replacement: 'profiles.*.default_connection',
|
|
52
|
-
message: 'Use default_connection or per-app connections override',
|
|
53
|
-
},
|
|
54
|
-
];
|