genbox 1.0.203 → 1.0.204
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/commands/list.js +103 -365
- package/dist/commands/session/list.js +1 -1
- package/dist/commands/status.js +77 -1
- package/dist/lib/genbox-wizard.js +139 -19
- package/dist/lib/unified-session/list-sessions.js +95 -54
- package/dist/ssh-config.js +4 -0
- package/package.json +1 -1
package/dist/commands/list.js
CHANGED
|
@@ -114,138 +114,19 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
114
114
|
.action(async (options) => {
|
|
115
115
|
try {
|
|
116
116
|
const projectName = (0, genbox_selector_1.getProjectContext)();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// Cloud genboxes
|
|
120
|
-
(0, genbox_selector_1.getGenboxes)({
|
|
121
|
-
all: options.all,
|
|
122
|
-
includeTerminated: options.terminated,
|
|
123
|
-
}),
|
|
124
|
-
// Local genboxes from both LocalGenboxProvisioner and UnifiedSessionManager
|
|
125
|
-
Promise.resolve().then(() => {
|
|
126
|
-
const sessions = [];
|
|
127
|
-
const seenNames = new Set();
|
|
128
|
-
// Get sessions from LocalGenboxProvisioner (for legacy local genboxes)
|
|
129
|
-
try {
|
|
130
|
-
const provisioner = (0, local_genbox_provisioner_1.getLocalGenboxProvisioner)();
|
|
131
|
-
for (const s of provisioner.listSessions()) {
|
|
132
|
-
if (!seenNames.has(s.name)) {
|
|
133
|
-
sessions.push(s);
|
|
134
|
-
seenNames.add(s.name);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// Ignore errors
|
|
140
|
-
}
|
|
141
|
-
// Get local sessions from UnifiedSessionManager (multipass/docker VMs created via gb gemini)
|
|
142
|
-
try {
|
|
143
|
-
const sessionManager = (0, unified_session_1.getUnifiedSessionManager)();
|
|
144
|
-
const unifiedSessions = sessionManager.listSessions({
|
|
145
|
-
type: ['multipass', 'docker'],
|
|
146
|
-
});
|
|
147
|
-
for (const s of unifiedSessions) {
|
|
148
|
-
if (!seenNames.has(s.name)) {
|
|
149
|
-
// Check actual VM status for multipass sessions
|
|
150
|
-
let actualStatus = s.status === 'running' || s.status === 'active' ? 'running' : 'stopped';
|
|
151
|
-
let vmIp = s.infrastructure?.vmIpAddress;
|
|
152
|
-
if (s.type === 'multipass') {
|
|
153
|
-
// Use vmName or fall back to session name for older sessions
|
|
154
|
-
const vmName = s.infrastructure?.vmName || s.name;
|
|
155
|
-
try {
|
|
156
|
-
const info = (0, child_process_1.execSync)(`multipass info ${vmName} --format json 2>/dev/null`, { encoding: 'utf8' });
|
|
157
|
-
const vmInfo = JSON.parse(info);
|
|
158
|
-
const vm = vmInfo.info?.[vmName];
|
|
159
|
-
if (vm) {
|
|
160
|
-
actualStatus = vm.state === 'Running' ? 'running' : 'stopped';
|
|
161
|
-
vmIp = vm.ipv4?.[0] || vmIp;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// VM might not exist anymore
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else if (s.type === 'docker') {
|
|
169
|
-
// Check Docker container status
|
|
170
|
-
const containerName = s.infrastructure?.containerName || s.name;
|
|
171
|
-
try {
|
|
172
|
-
const status = (0, child_process_1.execSync)(`docker inspect --format='{{.State.Status}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
173
|
-
actualStatus = status === 'running' ? 'running' : 'stopped';
|
|
174
|
-
// Get container IP if running
|
|
175
|
-
if (actualStatus === 'running') {
|
|
176
|
-
const ip = (0, child_process_1.execSync)(`docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
177
|
-
if (ip)
|
|
178
|
-
vmIp = ip;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// Container might not exist anymore
|
|
183
|
-
actualStatus = 'stopped';
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Convert to LocalGenboxSession format for rendering
|
|
187
|
-
sessions.push({
|
|
188
|
-
id: s.id,
|
|
189
|
-
name: s.name,
|
|
190
|
-
projectName: s.projectPath?.split('/').pop() || '',
|
|
191
|
-
workdir: s.projectPath || '',
|
|
192
|
-
sessionDir: '',
|
|
193
|
-
createdAt: s.createdAt,
|
|
194
|
-
size: 'small',
|
|
195
|
-
isolation: s.type,
|
|
196
|
-
apps: [{
|
|
197
|
-
name: s.provider || 'unknown',
|
|
198
|
-
path: '',
|
|
199
|
-
status: actualStatus,
|
|
200
|
-
}],
|
|
201
|
-
infrastructure: [],
|
|
202
|
-
database: {
|
|
203
|
-
mode: 'none',
|
|
204
|
-
seeded: false,
|
|
205
|
-
},
|
|
206
|
-
vmName: s.infrastructure?.vmName || s.name,
|
|
207
|
-
vmIpAddress: vmIp,
|
|
208
|
-
});
|
|
209
|
-
seenNames.add(s.name);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// Ignore errors
|
|
215
|
-
}
|
|
216
|
-
// Filter by project if applicable
|
|
217
|
-
if (projectName && !options.all) {
|
|
218
|
-
return sessions.filter(s => s.projectName === projectName || !s.projectName);
|
|
219
|
-
}
|
|
220
|
-
return sessions;
|
|
221
|
-
}),
|
|
222
|
-
]);
|
|
223
|
-
// Extract results
|
|
224
|
-
const cloudGenboxes = cloudResult.status === 'fulfilled' ? cloudResult.value : [];
|
|
225
|
-
let localSessions = localResult.status === 'fulfilled' ? localResult.value : [];
|
|
226
|
-
// Show warning if cloud fetch failed (but continue with local)
|
|
227
|
-
if (cloudResult.status === 'rejected' && !options.json) {
|
|
228
|
-
const error = cloudResult.reason;
|
|
229
|
-
if (error instanceof api_1.AuthenticationError) {
|
|
230
|
-
(0, api_1.handleApiError)(error);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
console.log(chalk_1.default.yellow(`⚠ Could not fetch cloud genboxes: ${error.message}`));
|
|
234
|
-
}
|
|
235
|
-
// Detect orphaned VMs/containers (exist in Multipass/Docker but not tracked)
|
|
236
|
-
const trackedNames = new Set(localSessions.map(s => s.name));
|
|
237
|
-
// Also add vmName variants
|
|
238
|
-
for (const s of localSessions) {
|
|
239
|
-
if (s.vmName)
|
|
240
|
-
trackedNames.add(s.vmName);
|
|
241
|
-
}
|
|
242
|
-
const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
|
|
243
|
-
const totalCloud = cloudGenboxes.length;
|
|
244
|
-
const totalLocal = localSessions.length;
|
|
245
|
-
const totalOrphaned = orphanedGenboxes.length;
|
|
246
|
-
const totalAll = totalCloud + totalLocal + totalOrphaned;
|
|
247
|
-
// JSON output mode
|
|
117
|
+
const projectPath = (0, genbox_selector_1.isInProjectContext)() ? process.cwd() : undefined;
|
|
118
|
+
// JSON output mode - use legacy detailed format
|
|
248
119
|
if (options.json) {
|
|
120
|
+
const [cloudGenboxes, localSessions] = await Promise.all([
|
|
121
|
+
(0, genbox_selector_1.getGenboxes)({ all: options.all, includeTerminated: options.terminated }).catch(() => []),
|
|
122
|
+
getLocalGenboxesForJson(projectName, options.all),
|
|
123
|
+
]);
|
|
124
|
+
const trackedNames = new Set(localSessions.map(s => s.name));
|
|
125
|
+
for (const s of localSessions) {
|
|
126
|
+
if (s.vmName)
|
|
127
|
+
trackedNames.add(s.vmName);
|
|
128
|
+
}
|
|
129
|
+
const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
|
|
249
130
|
const cloudOutput = cloudGenboxes.map(g => ({
|
|
250
131
|
name: g.name,
|
|
251
132
|
type: 'cloud',
|
|
@@ -279,96 +160,34 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
279
160
|
console.log(JSON.stringify([...cloudOutput, ...localOutput, ...orphanedOutput], null, 2));
|
|
280
161
|
return;
|
|
281
162
|
}
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
294
|
-
// No genboxes at all
|
|
295
|
-
if (totalAll === 0) {
|
|
296
|
-
if (projectName && !options.all) {
|
|
297
|
-
console.log(chalk_1.default.yellow(`No genboxes found for project '${projectName}'.`));
|
|
298
|
-
console.log('');
|
|
299
|
-
console.log(chalk_1.default.dim(' Create one with:'));
|
|
300
|
-
console.log(chalk_1.default.cyan(' $ gb create <name> ') + chalk_1.default.dim('# Cloud genbox'));
|
|
301
|
-
console.log(chalk_1.default.cyan(' $ gb create -l <name> ') + chalk_1.default.dim('# Local genbox'));
|
|
302
|
-
console.log('');
|
|
303
|
-
console.log(chalk_1.default.dim(` Or see all genboxes with:`));
|
|
304
|
-
console.log(chalk_1.default.cyan(' $ gb list --all'));
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
console.log(chalk_1.default.yellow('No genboxes found.'));
|
|
308
|
-
console.log('');
|
|
309
|
-
console.log(chalk_1.default.dim(' Create one with:'));
|
|
310
|
-
console.log(chalk_1.default.cyan(' $ gb create <name> ') + chalk_1.default.dim('# Cloud genbox'));
|
|
311
|
-
console.log(chalk_1.default.cyan(' $ gb create -l <name> ') + chalk_1.default.dim('# Local genbox'));
|
|
312
|
-
}
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
// Calculate column widths
|
|
316
|
-
const cloudNameWidth = Math.max(12, ...cloudGenboxes.map(g => g.name.length + (options.all && g.project ? g.project.length + 3 : 0)));
|
|
317
|
-
const localNameWidth = Math.max(12, ...localSessions.map(s => s.name.length));
|
|
318
|
-
// Show CLOUD genboxes section
|
|
319
|
-
if (cloudGenboxes.length > 0) {
|
|
320
|
-
// Sort by createdAt (newest first)
|
|
321
|
-
cloudGenboxes.sort((a, b) => {
|
|
322
|
-
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
323
|
-
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
324
|
-
return dateB - dateA;
|
|
325
|
-
});
|
|
326
|
-
for (const genbox of cloudGenboxes) {
|
|
327
|
-
renderCloudGenbox(genbox, cloudNameWidth, options.all);
|
|
328
|
-
}
|
|
329
|
-
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
330
|
-
console.log(chalk_1.default.dim(` ${totalCloud} cloud genbox(es).`));
|
|
331
|
-
}
|
|
332
|
-
// Show LOCAL genboxes section (separate)
|
|
333
|
-
if (localSessions.length > 0) {
|
|
334
|
-
if (cloudGenboxes.length > 0) {
|
|
335
|
-
console.log('');
|
|
336
|
-
}
|
|
337
|
-
console.log(chalk_1.default.bold('Local Genboxes:'));
|
|
338
|
-
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
339
|
-
for (const session of localSessions) {
|
|
340
|
-
renderLocalSession(session, localNameWidth);
|
|
341
|
-
}
|
|
342
|
-
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
343
|
-
console.log(chalk_1.default.dim(` ${totalLocal} local genbox(es).`));
|
|
344
|
-
console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb session <name>')} to start or attach to an AI session.`));
|
|
345
|
-
}
|
|
346
|
-
// Show ORPHANED genboxes section
|
|
163
|
+
// Use unified display format
|
|
164
|
+
const result = await (0, unified_session_1.listAllSessions)({
|
|
165
|
+
projectPath: options.all ? undefined : projectPath,
|
|
166
|
+
includeEnded: options.terminated,
|
|
167
|
+
});
|
|
168
|
+
// Check for orphaned VMs/containers
|
|
169
|
+
const trackedNames = new Set(result.sessions.map(s => s.name));
|
|
170
|
+
const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
|
|
171
|
+
// Display unified format
|
|
172
|
+
(0, unified_session_1.displaySessions)(result, { terminology: 'genboxes' });
|
|
173
|
+
// Show ORPHANED genboxes section if any
|
|
347
174
|
if (orphanedGenboxes.length > 0) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
console.log(chalk_1.default.yellow.bold('Orphaned VMs/Containers:'));
|
|
352
|
-
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
175
|
+
console.log(chalk_1.default.yellow.bold(' Orphaned VMs/Containers:'));
|
|
176
|
+
console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
|
|
353
177
|
const orphanedNameWidth = Math.max(12, ...orphanedGenboxes.map(o => o.name.length));
|
|
354
178
|
for (const orphan of orphanedGenboxes) {
|
|
355
179
|
renderOrphanedGenbox(orphan, orphanedNameWidth);
|
|
356
180
|
}
|
|
357
|
-
console.log(
|
|
358
|
-
console.log(chalk_1.default.yellow(` ${
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk_1.default.yellow(` ${orphanedGenboxes.length} orphaned VM/container(s) found.`));
|
|
359
183
|
console.log(chalk_1.default.dim(` These exist in ${chalk_1.default.cyan('Multipass/Docker')} but aren't tracked by genbox.`));
|
|
360
184
|
console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb delete <name>')} to clean them up.`));
|
|
185
|
+
console.log('');
|
|
361
186
|
}
|
|
362
187
|
// Hint for --all
|
|
363
188
|
if (projectName && !options.all) {
|
|
364
|
-
console.log('');
|
|
365
189
|
console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('--all')} to see genboxes from all projects.`));
|
|
366
|
-
}
|
|
367
|
-
// Suggest cloud if only local genboxes exist
|
|
368
|
-
if (totalLocal > 0 && totalCloud === 0) {
|
|
369
190
|
console.log('');
|
|
370
|
-
console.log(chalk_1.default.dim(' Want more compute power? Create a cloud genbox:'));
|
|
371
|
-
console.log(chalk_1.default.cyan(' $ gb create <name>'));
|
|
372
191
|
}
|
|
373
192
|
// Force exit since fetch may leave connections open
|
|
374
193
|
process.exit(0);
|
|
@@ -383,172 +202,91 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
383
202
|
}
|
|
384
203
|
});
|
|
385
204
|
/**
|
|
386
|
-
*
|
|
205
|
+
* Get local genboxes for JSON output (preserves legacy format)
|
|
387
206
|
*/
|
|
388
|
-
function
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
let agePart = '';
|
|
399
|
-
if (session.createdAt) {
|
|
400
|
-
const age = Date.now() - new Date(session.createdAt).getTime();
|
|
401
|
-
const mins = Math.floor(age / 60000);
|
|
402
|
-
const hours = Math.floor(mins / 60);
|
|
403
|
-
if (hours > 0) {
|
|
404
|
-
agePart = chalk_1.default.dim(` ${hours}h ago`);
|
|
405
|
-
}
|
|
406
|
-
else if (mins > 0) {
|
|
407
|
-
agePart = chalk_1.default.dim(` ${mins}m ago`);
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
agePart = chalk_1.default.dim(' just now');
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
console.log(`${namePart} ${statusPart} ${typePart} ${sizePart}${agePart}`);
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Render a cloud genbox in the list
|
|
417
|
-
*/
|
|
418
|
-
function renderCloudGenbox(genbox, nameWidth, showAll) {
|
|
419
|
-
const statusColor = genbox.status === 'running' ? chalk_1.default.green :
|
|
420
|
-
genbox.status === 'terminated' ? chalk_1.default.red :
|
|
421
|
-
genbox.status === 'provisioning' ? chalk_1.default.cyan : chalk_1.default.yellow;
|
|
422
|
-
// Show project info when listing all
|
|
423
|
-
const projectSuffix = showAll && genbox.project ? ` [${genbox.project}]` : '';
|
|
424
|
-
const nameWithProject = genbox.name + projectSuffix;
|
|
425
|
-
// Determine auto-destroy status first (needed for hour end coloring)
|
|
426
|
-
const now = new Date();
|
|
427
|
-
// Calculate billing hour timing (matches backend billing.processor.ts logic)
|
|
428
|
-
let minutesIntoBillingHour = 0;
|
|
429
|
-
let wasActiveAfter50Min = false;
|
|
430
|
-
if (genbox.currentHourEnd) {
|
|
431
|
-
const currentHourEnd = new Date(genbox.currentHourEnd);
|
|
432
|
-
const currentHourStart = new Date(currentHourEnd.getTime() - 60 * 60 * 1000);
|
|
433
|
-
minutesIntoBillingHour = Math.floor((now.getTime() - currentHourStart.getTime()) / (60 * 1000));
|
|
434
|
-
// Check if there was activity after the 50 min mark (this is what prevents auto-destroy)
|
|
435
|
-
const fiftyMinMark = new Date(currentHourStart.getTime() + 50 * 60 * 1000);
|
|
436
|
-
const lastActivity = genbox.lastActivityAt ? new Date(genbox.lastActivityAt) : null;
|
|
437
|
-
wasActiveAfter50Min = lastActivity ? lastActivity.getTime() >= fiftyMinMark.getTime() : false;
|
|
438
|
-
}
|
|
439
|
-
// Check if auto-destroy will actually happen
|
|
440
|
-
const isProtectedPermanently = genbox.autoDestroyOnInactivity === false;
|
|
441
|
-
const isProtectedTemporarily = genbox.protectedUntil && new Date(genbox.protectedUntil).getTime() > now.getTime();
|
|
442
|
-
// Auto-destroy is paused if we're past 50min mark AND there was activity after 50min
|
|
443
|
-
const isAutoDestroyPaused = minutesIntoBillingHour >= 50 && wasActiveAfter50Min;
|
|
444
|
-
const willAutoDestroy = !isProtectedPermanently && !isProtectedTemporarily && !isAutoDestroyPaused;
|
|
445
|
-
// Format end hour as relative time (destroy happens at 58 min mark, not 60)
|
|
446
|
-
let endHourInfo = '';
|
|
447
|
-
if (genbox.currentHourEnd && genbox.status === 'running') {
|
|
448
|
-
const endTime = new Date(genbox.currentHourEnd);
|
|
449
|
-
// Subtract 2 minutes since auto-destroy happens at 58 min mark
|
|
450
|
-
const destroyTime = endTime.getTime() - (2 * 60 * 1000);
|
|
451
|
-
const diffMs = destroyTime - now.getTime();
|
|
452
|
-
if (diffMs > 0) {
|
|
453
|
-
const totalSeconds = Math.floor(diffMs / 1000);
|
|
454
|
-
const mins = Math.floor(totalSeconds / 60);
|
|
455
|
-
const secs = totalSeconds % 60;
|
|
456
|
-
const timeStr = mins > 0
|
|
457
|
-
? `${mins}m:${secs.toString().padStart(2, '0')}s`
|
|
458
|
-
: `${secs}s`;
|
|
459
|
-
// Red color only if less than 5 minutes AND auto-destroy will actually happen
|
|
460
|
-
if (mins < 5 && willAutoDestroy) {
|
|
461
|
-
endHourInfo = chalk_1.default.red(` hour ends in ${timeStr}`);
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
endHourInfo = chalk_1.default.dim(` hour ends in ${timeStr}`);
|
|
207
|
+
async function getLocalGenboxesForJson(projectName, showAll) {
|
|
208
|
+
const sessions = [];
|
|
209
|
+
const seenNames = new Set();
|
|
210
|
+
// Get sessions from LocalGenboxProvisioner (for legacy local genboxes)
|
|
211
|
+
try {
|
|
212
|
+
const provisioner = (0, local_genbox_provisioner_1.getLocalGenboxProvisioner)();
|
|
213
|
+
for (const s of provisioner.listSessions()) {
|
|
214
|
+
if (!seenNames.has(s.name)) {
|
|
215
|
+
sessions.push(s);
|
|
216
|
+
seenNames.add(s.name);
|
|
465
217
|
}
|
|
466
218
|
}
|
|
467
|
-
else if (willAutoDestroy) {
|
|
468
|
-
endHourInfo = chalk_1.default.red(' hour ending...');
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
endHourInfo = chalk_1.default.dim(' hour ending...');
|
|
472
|
-
}
|
|
473
219
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (genbox.status === 'stopped') {
|
|
477
|
-
// Show snapshot status if available
|
|
478
|
-
if (genbox.snapshot) {
|
|
479
|
-
if (genbox.snapshot.status === 'creating') {
|
|
480
|
-
const progress = genbox.snapshot.progress || 0;
|
|
481
|
-
protectedInfo = chalk_1.default.yellow(` (snapshot: ${progress}%)`);
|
|
482
|
-
}
|
|
483
|
-
else if (genbox.snapshot.status === 'ready') {
|
|
484
|
-
protectedInfo = chalk_1.default.dim(' → gb start to resume (instant)');
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
protectedInfo = chalk_1.default.dim(' → gb start to resume');
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
protectedInfo = chalk_1.default.dim(' → gb start to resume');
|
|
492
|
-
}
|
|
220
|
+
catch {
|
|
221
|
+
// Ignore errors
|
|
493
222
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
223
|
+
// Get local sessions from UnifiedSessionManager (multipass/docker VMs created via gb gemini)
|
|
224
|
+
try {
|
|
225
|
+
const sessionManager = (0, unified_session_1.getUnifiedSessionManager)();
|
|
226
|
+
const unifiedSessions = sessionManager.listSessions({
|
|
227
|
+
type: ['multipass', 'docker'],
|
|
228
|
+
});
|
|
229
|
+
for (const s of unifiedSessions) {
|
|
230
|
+
if (!seenNames.has(s.name)) {
|
|
231
|
+
let actualStatus = s.status === 'running' || s.status === 'active' ? 'running' : 'stopped';
|
|
232
|
+
let vmIp = s.infrastructure?.vmIpAddress;
|
|
233
|
+
if (s.type === 'multipass') {
|
|
234
|
+
const vmName = s.infrastructure?.vmName || s.name;
|
|
235
|
+
try {
|
|
236
|
+
const info = (0, child_process_1.execSync)(`multipass info ${vmName} --format json 2>/dev/null`, { encoding: 'utf8' });
|
|
237
|
+
const vmInfo = JSON.parse(info);
|
|
238
|
+
const vm = vmInfo.info?.[vmName];
|
|
239
|
+
if (vm) {
|
|
240
|
+
actualStatus = vm.state === 'Running' ? 'running' : 'stopped';
|
|
241
|
+
vmIp = vm.ipv4?.[0] || vmIp;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// VM might not exist anymore
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (s.type === 'docker') {
|
|
249
|
+
const containerName = s.infrastructure?.containerName || s.name;
|
|
250
|
+
try {
|
|
251
|
+
const status = (0, child_process_1.execSync)(`docker inspect --format='{{.State.Status}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
252
|
+
actualStatus = status === 'running' ? 'running' : 'stopped';
|
|
253
|
+
if (actualStatus === 'running') {
|
|
254
|
+
const ip = (0, child_process_1.execSync)(`docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
255
|
+
if (ip)
|
|
256
|
+
vmIp = ip;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
actualStatus = 'stopped';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
sessions.push({
|
|
264
|
+
id: s.id,
|
|
265
|
+
name: s.name,
|
|
266
|
+
projectName: s.projectPath?.split('/').pop() || '',
|
|
267
|
+
workdir: s.projectPath || '',
|
|
268
|
+
sessionDir: '',
|
|
269
|
+
createdAt: s.createdAt,
|
|
270
|
+
size: 'small',
|
|
271
|
+
isolation: s.type,
|
|
272
|
+
apps: [{ name: s.provider || 'unknown', path: '', status: actualStatus }],
|
|
273
|
+
infrastructure: [],
|
|
274
|
+
database: { mode: 'none', seeded: false },
|
|
275
|
+
vmName: s.infrastructure?.vmName || s.name,
|
|
276
|
+
vmIpAddress: vmIp,
|
|
277
|
+
});
|
|
278
|
+
seenNames.add(s.name);
|
|
504
279
|
}
|
|
505
280
|
}
|
|
506
|
-
else {
|
|
507
|
-
protectedInfo = chalk_1.default.cyan(' (provisioning...)');
|
|
508
|
-
}
|
|
509
281
|
}
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
}
|
|
513
|
-
else if (genbox.autoDestroyOnInactivity === false) {
|
|
514
|
-
protectedInfo = chalk_1.default.yellow(' [protected]');
|
|
515
|
-
}
|
|
516
|
-
else if (genbox.protectedUntil) {
|
|
517
|
-
const protectedUntil = new Date(genbox.protectedUntil);
|
|
518
|
-
const diffMs = protectedUntil.getTime() - now.getTime();
|
|
519
|
-
const hoursRemaining = Math.ceil(diffMs / (1000 * 60 * 60));
|
|
520
|
-
if (hoursRemaining > 0) {
|
|
521
|
-
protectedInfo = chalk_1.default.cyan(` [extended ${hoursRemaining}h]`);
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
protectedInfo = chalk_1.default.dim(' [auto-destroy]');
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
// Show auto-destroy paused only when we're past 50min mark AND activity saved it
|
|
529
|
-
if (isAutoDestroyPaused) {
|
|
530
|
-
protectedInfo = chalk_1.default.green(' [auto-destroy paused]');
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
protectedInfo = chalk_1.default.dim(' [auto-destroy]');
|
|
534
|
-
}
|
|
282
|
+
catch {
|
|
283
|
+
// Ignore errors
|
|
535
284
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let ipText = genbox.ipAddress || 'Pending IP';
|
|
540
|
-
if (!genbox.ipAddress) {
|
|
541
|
-
if (genbox.status === 'stopped') {
|
|
542
|
-
ipText = '(paused)';
|
|
543
|
-
}
|
|
544
|
-
else if (genbox.status === 'terminated') {
|
|
545
|
-
ipText = '-';
|
|
546
|
-
}
|
|
285
|
+
// Filter by project if applicable
|
|
286
|
+
if (projectName && !showAll) {
|
|
287
|
+
return sessions.filter(s => s.projectName === projectName || !s.projectName);
|
|
547
288
|
}
|
|
548
|
-
|
|
549
|
-
const sizePart = `(${genbox.size})`.padEnd(8);
|
|
550
|
-
const extraInfo = endHourInfo + protectedInfo;
|
|
551
|
-
console.log(`${namePart} ${statusPart} ${ipPart} ${sizePart}${extraInfo}`);
|
|
289
|
+
return sessions;
|
|
552
290
|
}
|
|
553
291
|
/**
|
|
554
292
|
* Render an orphaned VM/container in the list
|
|
@@ -51,7 +51,7 @@ exports.sessionListCommand = new commander_1.Command('list')
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
// Display formatted output
|
|
54
|
-
(0, unified_session_1.displaySessions)(result);
|
|
54
|
+
(0, unified_session_1.displaySessions)(result, { terminology: 'sessions' });
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
57
57
|
if (error instanceof api_1.AuthenticationError) {
|
package/dist/commands/status.js
CHANGED
|
@@ -465,7 +465,7 @@ async function displayFullLocalGenboxStatus(session) {
|
|
|
465
465
|
console.log(` Uptime: ${uptimeStr}`);
|
|
466
466
|
}
|
|
467
467
|
console.log('');
|
|
468
|
-
// === System Stats with progress bars
|
|
468
|
+
// === System Stats with progress bars ===
|
|
469
469
|
if (session.isolation === 'multipass') {
|
|
470
470
|
const vmName = session.vmName || `genbox-${session.name}`;
|
|
471
471
|
try {
|
|
@@ -507,6 +507,82 @@ async function displayFullLocalGenboxStatus(session) {
|
|
|
507
507
|
// VM info not available
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
|
+
else if (session.isolation === 'docker') {
|
|
511
|
+
// Docker container stats
|
|
512
|
+
const containerName = session.vmName || session.name;
|
|
513
|
+
try {
|
|
514
|
+
// Get container IP
|
|
515
|
+
const containerIp = session.vmIpAddress || (0, child_process_1.execSync)(`docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
516
|
+
// Get container stats (CPU, Memory)
|
|
517
|
+
const statsOutput = (0, child_process_1.execSync)(`docker stats --no-stream --format='{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
518
|
+
// Get container CPU limit from inspect
|
|
519
|
+
const cpuLimit = (0, child_process_1.execSync)(`docker inspect --format='{{.HostConfig.NanoCpus}}' ${containerName} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
520
|
+
const cpuCount = cpuLimit && parseInt(cpuLimit) > 0 ? Math.round(parseInt(cpuLimit) / 1e9) : null;
|
|
521
|
+
// Get disk usage inside container
|
|
522
|
+
const diskOutput = (0, child_process_1.execSync)(`docker exec ${containerName} df -B1 / 2>/dev/null | tail -1`, { encoding: 'utf8' }).trim();
|
|
523
|
+
console.log(chalk_1.default.blue('[INFO] === System Stats ==='));
|
|
524
|
+
// IP Address
|
|
525
|
+
if (containerIp) {
|
|
526
|
+
console.log(` IP: ${chalk_1.default.cyan(containerIp)}`);
|
|
527
|
+
}
|
|
528
|
+
// CPUs
|
|
529
|
+
if (cpuCount) {
|
|
530
|
+
console.log(` CPUs: ${cpuCount}`);
|
|
531
|
+
}
|
|
532
|
+
// Parse and display memory stats
|
|
533
|
+
if (statsOutput) {
|
|
534
|
+
const [cpuPerc, memUsage, memPerc] = statsOutput.split('\t');
|
|
535
|
+
// memUsage is like "412.5MiB / 3.842GiB"
|
|
536
|
+
// memPerc is like "10.48%"
|
|
537
|
+
const memPercent = parseFloat(memPerc?.replace('%', '') || '0');
|
|
538
|
+
const memBar = renderBar(Math.round(memPercent));
|
|
539
|
+
// Parse memUsage to extract used and total
|
|
540
|
+
const memMatch = memUsage?.match(/([\d.]+)([A-Za-z]+)\s*\/\s*([\d.]+)([A-Za-z]+)/);
|
|
541
|
+
if (memMatch) {
|
|
542
|
+
const [, usedVal, usedUnit, totalVal, totalUnit] = memMatch;
|
|
543
|
+
// Convert to GB for display
|
|
544
|
+
const toGb = (val, unit) => {
|
|
545
|
+
const num = parseFloat(val);
|
|
546
|
+
if (unit.toLowerCase().includes('gib') || unit.toLowerCase().includes('gb'))
|
|
547
|
+
return num;
|
|
548
|
+
if (unit.toLowerCase().includes('mib') || unit.toLowerCase().includes('mb'))
|
|
549
|
+
return num / 1024;
|
|
550
|
+
if (unit.toLowerCase().includes('kib') || unit.toLowerCase().includes('kb'))
|
|
551
|
+
return num / (1024 * 1024);
|
|
552
|
+
return num;
|
|
553
|
+
};
|
|
554
|
+
const usedGb = toGb(usedVal, usedUnit).toFixed(1);
|
|
555
|
+
const totalGb = toGb(totalVal, totalUnit).toFixed(1);
|
|
556
|
+
console.log(` Memory: ${memBar} ${Math.round(memPercent)}% (${usedGb}G/${totalGb}G)`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Parse and display disk stats
|
|
560
|
+
if (diskOutput) {
|
|
561
|
+
const diskParts = diskOutput.split(/\s+/);
|
|
562
|
+
// Format: Filesystem 1B-blocks Used Available Use% Mounted
|
|
563
|
+
if (diskParts.length >= 5) {
|
|
564
|
+
const diskTotal = parseInt(diskParts[1]) || 0;
|
|
565
|
+
const diskUsed = parseInt(diskParts[2]) || 0;
|
|
566
|
+
if (diskTotal > 0) {
|
|
567
|
+
const diskPercent = Math.round((diskUsed / diskTotal) * 100);
|
|
568
|
+
const diskBar = renderBar(diskPercent);
|
|
569
|
+
const diskUsedGb = (diskUsed / (1024 * 1024 * 1024)).toFixed(1);
|
|
570
|
+
const diskTotalGb = (diskTotal / (1024 * 1024 * 1024)).toFixed(1);
|
|
571
|
+
console.log(` Disk: ${diskBar} ${diskPercent}% (${diskUsedGb}G/${diskTotalGb}G)`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
console.log('');
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// Container stats not available - still show IP if we have it
|
|
579
|
+
if (session.vmIpAddress) {
|
|
580
|
+
console.log(chalk_1.default.blue('[INFO] === System Stats ==='));
|
|
581
|
+
console.log(` IP: ${chalk_1.default.cyan(session.vmIpAddress)}`);
|
|
582
|
+
console.log('');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
510
586
|
// === Infrastructure Services (similar to Docker Services) ===
|
|
511
587
|
if (session.infrastructure.length > 0) {
|
|
512
588
|
console.log(chalk_1.default.blue('[INFO] === Infrastructure Services ==='));
|
|
@@ -69,6 +69,7 @@ const config_loader_1 = require("../config-loader");
|
|
|
69
69
|
const profile_resolver_1 = require("../profile-resolver");
|
|
70
70
|
const utils_1 = require("../utils");
|
|
71
71
|
const random_name_1 = require("../random-name");
|
|
72
|
+
const ssh_config_1 = require("../ssh-config");
|
|
72
73
|
const VM_SIZE_REQUIREMENTS = {
|
|
73
74
|
small: {
|
|
74
75
|
cpus: 2,
|
|
@@ -827,12 +828,46 @@ function showProgress(status, elapsed, percent) {
|
|
|
827
828
|
if (percent !== undefined) {
|
|
828
829
|
statusLine += ` ${createProgressBar(percent)} ${percent}%`;
|
|
829
830
|
}
|
|
830
|
-
|
|
831
|
+
// Use ANSI escape sequence to clear entire line before writing
|
|
832
|
+
// \x1B[2K clears the line, \r moves cursor to start
|
|
833
|
+
process.stdout.write(`\x1B[2K\r${statusLine}`);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Clone repos inside a genbox via SSH
|
|
837
|
+
* Used by both Docker and VM genboxes to match cloud behavior
|
|
838
|
+
*/
|
|
839
|
+
async function cloneReposInGenbox(sshTarget, sshPortArgs, keyPath, repos, gitToken) {
|
|
840
|
+
for (const repo of repos) {
|
|
841
|
+
// Construct git URL with token for private repos
|
|
842
|
+
let gitUrl = repo.url;
|
|
843
|
+
if (gitToken && gitUrl.startsWith('github.com/')) {
|
|
844
|
+
gitUrl = `https://${gitToken}@${repo.url}`;
|
|
845
|
+
}
|
|
846
|
+
else if (!gitUrl.startsWith('http')) {
|
|
847
|
+
gitUrl = `https://${repo.url}`;
|
|
848
|
+
}
|
|
849
|
+
const branch = repo.branch || 'main';
|
|
850
|
+
const cloneCmd = `git clone --branch ${branch} ${gitUrl} ${repo.path} 2>&1 || echo "Clone may have failed"`;
|
|
851
|
+
try {
|
|
852
|
+
(0, child_process_1.execFileSync)('ssh', [
|
|
853
|
+
...sshPortArgs,
|
|
854
|
+
'-i', keyPath,
|
|
855
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
856
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
857
|
+
'-o', 'LogLevel=ERROR',
|
|
858
|
+
sshTarget,
|
|
859
|
+
cloneCmd,
|
|
860
|
+
], { timeout: 300000, stdio: 'pipe' });
|
|
861
|
+
}
|
|
862
|
+
catch (err) {
|
|
863
|
+
console.log(chalk_1.default.yellow(` Warning: Failed to clone ${repo.name}: ${err.message}`));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
831
866
|
}
|
|
832
867
|
/**
|
|
833
868
|
* Create a local Docker genbox
|
|
834
869
|
*/
|
|
835
|
-
async function createLocalDockerGenbox(name, provider = 'claude') {
|
|
870
|
+
async function createLocalDockerGenbox(name, provider = 'claude', localOptions = {}) {
|
|
836
871
|
// Get system resources
|
|
837
872
|
const resources = getSystemResources();
|
|
838
873
|
// Prompt for size selection
|
|
@@ -860,7 +895,6 @@ async function createLocalDockerGenbox(name, provider = 'claude') {
|
|
|
860
895
|
showProgress('Creating Docker container', 0, 5);
|
|
861
896
|
try {
|
|
862
897
|
const manager = (0, unified_session_1.getUnifiedSessionManager)();
|
|
863
|
-
const workdir = process.cwd();
|
|
864
898
|
showProgress('Pulling Ubuntu image', 0, 15);
|
|
865
899
|
// Pull Ubuntu image if not present
|
|
866
900
|
try {
|
|
@@ -903,6 +937,7 @@ mkdir -p /run/sshd
|
|
|
903
937
|
`.trim().replace(/\n/g, ' && ');
|
|
904
938
|
// Run Docker container with port forwarding for SSH
|
|
905
939
|
// On macOS, container IPs aren't accessible from host, so we use port mapping
|
|
940
|
+
// No mounting - repos are cloned inside the container (matching cloud behavior)
|
|
906
941
|
const dockerArgs = [
|
|
907
942
|
'run', '-d',
|
|
908
943
|
'--name', name,
|
|
@@ -910,7 +945,6 @@ mkdir -p /run/sshd
|
|
|
910
945
|
'--cpus', String(sizeReqs.cpus),
|
|
911
946
|
'--memory', `${sizeReqs.memoryGB}g`,
|
|
912
947
|
'-p', `${sshPort}:22`, // Map host port to container SSH
|
|
913
|
-
'-v', `${workdir}:/home/dev/workspace`,
|
|
914
948
|
'-w', '/home/dev/workspace',
|
|
915
949
|
'--restart', 'unless-stopped',
|
|
916
950
|
'ubuntu:24.04',
|
|
@@ -1069,13 +1103,35 @@ mkdir -p /run/sshd
|
|
|
1069
1103
|
// Non-fatal - credentials can be set up manually
|
|
1070
1104
|
console.log(chalk_1.default.yellow(`\n Warning: Failed to configure credentials automatically.`));
|
|
1071
1105
|
}
|
|
1106
|
+
// Always create workspace directory (no mounting)
|
|
1107
|
+
showProgress('Setting up workspace', 0, 92);
|
|
1108
|
+
try {
|
|
1109
|
+
(0, child_process_1.execFileSync)('ssh', [
|
|
1110
|
+
'-p', String(sshPort),
|
|
1111
|
+
'-i', keyPath,
|
|
1112
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
1113
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
1114
|
+
'-o', 'LogLevel=ERROR',
|
|
1115
|
+
'dev@localhost',
|
|
1116
|
+
'mkdir -p /home/dev/workspace',
|
|
1117
|
+
], { timeout: 10000, stdio: 'pipe' });
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
// Ignore - directory might already exist
|
|
1121
|
+
}
|
|
1122
|
+
// Clone repos if provided (for project-based genboxes)
|
|
1123
|
+
if (localOptions.repos && localOptions.repos.length > 0) {
|
|
1124
|
+
console.log(chalk_1.default.dim(`\n Cloning ${localOptions.repos.length} repository(ies)...`));
|
|
1125
|
+
await cloneReposInGenbox('dev@localhost', ['-p', String(sshPort)], keyPath, localOptions.repos, localOptions.gitToken);
|
|
1126
|
+
console.log(chalk_1.default.green(` ✓ Repositories cloned`));
|
|
1127
|
+
}
|
|
1072
1128
|
showProgress('Creating session', 0, 95);
|
|
1073
1129
|
// Create session record
|
|
1074
1130
|
const session = await manager.createSession({
|
|
1075
1131
|
type: 'docker',
|
|
1076
1132
|
provider,
|
|
1077
1133
|
name,
|
|
1078
|
-
projectPath:
|
|
1134
|
+
projectPath: '/home/dev/workspace',
|
|
1079
1135
|
syncEnabled: false,
|
|
1080
1136
|
infrastructure: {
|
|
1081
1137
|
containerId,
|
|
@@ -1085,6 +1141,12 @@ mkdir -p /run/sshd
|
|
|
1085
1141
|
});
|
|
1086
1142
|
// Mark session as running
|
|
1087
1143
|
await manager.markRunning(session.id);
|
|
1144
|
+
// Add SSH config entry for easy access: ssh genbox-{name}
|
|
1145
|
+
(0, ssh_config_1.addSshConfigEntry)({
|
|
1146
|
+
name,
|
|
1147
|
+
ipAddress: 'localhost',
|
|
1148
|
+
port: sshPort,
|
|
1149
|
+
});
|
|
1088
1150
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
1089
1151
|
const elapsedStr = elapsed >= 60 ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` : `${elapsed}s`;
|
|
1090
1152
|
process.stdout.write(`\r${chalk_1.default.green('●')} Ready! (${elapsedStr}) ${createProgressBar(100)} 100%`.padEnd(80) + '\n');
|
|
@@ -1095,7 +1157,7 @@ mkdir -p /run/sshd
|
|
|
1095
1157
|
console.log(` ${chalk_1.default.bold('Type:')} Docker container`);
|
|
1096
1158
|
console.log(` ${chalk_1.default.bold('Size:')} ${sizeReqs.label} (${sizeReqs.cpus} CPU, ${sizeReqs.memoryGB}GB RAM)`);
|
|
1097
1159
|
console.log(` ${chalk_1.default.bold('Provider:')} ${provider}`);
|
|
1098
|
-
console.log(` ${chalk_1.default.bold('SSH:')} ssh
|
|
1160
|
+
console.log(` ${chalk_1.default.bold('SSH:')} ssh genbox-${name}`);
|
|
1099
1161
|
console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.green('running')}`);
|
|
1100
1162
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
1101
1163
|
console.log('');
|
|
@@ -1107,7 +1169,7 @@ mkdir -p /run/sshd
|
|
|
1107
1169
|
id: session.id,
|
|
1108
1170
|
name: session.name,
|
|
1109
1171
|
status: 'running',
|
|
1110
|
-
projectPath:
|
|
1172
|
+
projectPath: '/home/dev/workspace',
|
|
1111
1173
|
_isLocal: true,
|
|
1112
1174
|
_localType: 'docker',
|
|
1113
1175
|
sshPort,
|
|
@@ -1123,7 +1185,7 @@ mkdir -p /run/sshd
|
|
|
1123
1185
|
/**
|
|
1124
1186
|
* Create a local VM genbox (Multipass)
|
|
1125
1187
|
*/
|
|
1126
|
-
async function createLocalVmGenbox(name, provider = 'claude') {
|
|
1188
|
+
async function createLocalVmGenbox(name, provider = 'claude', localOptions = {}) {
|
|
1127
1189
|
// Get system resources
|
|
1128
1190
|
const resources = getSystemResources();
|
|
1129
1191
|
// Prompt for size selection
|
|
@@ -1402,16 +1464,7 @@ ${credentialsScript}`;
|
|
|
1402
1464
|
catch {
|
|
1403
1465
|
// IP will be shown later
|
|
1404
1466
|
}
|
|
1405
|
-
//
|
|
1406
|
-
const workdir = process.cwd();
|
|
1407
|
-
try {
|
|
1408
|
-
(0, child_process_1.execSync)(`multipass mount "${workdir}" ${name}:/home/dev/workspace`, { stdio: 'pipe' });
|
|
1409
|
-
}
|
|
1410
|
-
catch (err) {
|
|
1411
|
-
// Non-fatal - mount can be done manually
|
|
1412
|
-
console.log(chalk_1.default.yellow(`\n Warning: Failed to mount workspace automatically.`));
|
|
1413
|
-
console.log(chalk_1.default.dim(` Run: multipass mount "${workdir}" ${name}:/home/dev/workspace`));
|
|
1414
|
-
}
|
|
1467
|
+
// No mounting - repos are cloned inside the VM (matching cloud behavior)
|
|
1415
1468
|
// Install provider CLI via SSH
|
|
1416
1469
|
if (vmIp) {
|
|
1417
1470
|
showProgress(`Installing ${provider} CLI`, 0, 90);
|
|
@@ -1426,6 +1479,21 @@ ${credentialsScript}`;
|
|
|
1426
1479
|
console.log(chalk_1.default.yellow(`\n Warning: Failed to install ${provider} CLI automatically.`));
|
|
1427
1480
|
console.log(chalk_1.default.dim(` You can install it manually after connecting.`));
|
|
1428
1481
|
}
|
|
1482
|
+
// Always create workspace directory (no mounting)
|
|
1483
|
+
showProgress('Setting up workspace', 0, 92);
|
|
1484
|
+
try {
|
|
1485
|
+
(0, child_process_1.execSync)(`ssh -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR dev@${vmIp} "mkdir -p /home/dev/workspace"`, { timeout: 10000, stdio: 'pipe' });
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
// Ignore - directory might already exist
|
|
1489
|
+
}
|
|
1490
|
+
// Clone repos if provided (for project-based genboxes)
|
|
1491
|
+
if (localOptions.repos && localOptions.repos.length > 0) {
|
|
1492
|
+
console.log(chalk_1.default.dim(`\n Cloning ${localOptions.repos.length} repository(ies)...`));
|
|
1493
|
+
await cloneReposInGenbox(`dev@${vmIp}`, [], // No port args needed for VM
|
|
1494
|
+
keyPath, localOptions.repos, localOptions.gitToken);
|
|
1495
|
+
console.log(chalk_1.default.green(` ✓ Repositories cloned`));
|
|
1496
|
+
}
|
|
1429
1497
|
}
|
|
1430
1498
|
}
|
|
1431
1499
|
// Create session record
|
|
@@ -1468,7 +1536,7 @@ ${credentialsScript}`;
|
|
|
1468
1536
|
name: session.name,
|
|
1469
1537
|
status: 'running',
|
|
1470
1538
|
ipAddress: vmIp,
|
|
1471
|
-
projectPath:
|
|
1539
|
+
projectPath: '/home/dev/workspace',
|
|
1472
1540
|
_isLocal: true,
|
|
1473
1541
|
_localType: 'multipass',
|
|
1474
1542
|
},
|
|
@@ -1538,6 +1606,32 @@ async function createLocalDockerFromProject(config, options) {
|
|
|
1538
1606
|
console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
|
|
1539
1607
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
1540
1608
|
console.log('');
|
|
1609
|
+
// Resolve repos from profile configuration
|
|
1610
|
+
const configLoader = new config_loader_1.ConfigLoader();
|
|
1611
|
+
const resolver = new profile_resolver_1.ProfileResolver(configLoader);
|
|
1612
|
+
const createOptions = {
|
|
1613
|
+
name,
|
|
1614
|
+
profile: selectedProfile,
|
|
1615
|
+
yes: options.skipPrompts,
|
|
1616
|
+
};
|
|
1617
|
+
const resolvedConfig = await resolver.resolve(config, createOptions);
|
|
1618
|
+
const repos = resolvedConfig.repos || [];
|
|
1619
|
+
// Get git token from environment for private repos
|
|
1620
|
+
const gitToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
1621
|
+
// Create genbox with repos to clone (no mounting - matching cloud behavior)
|
|
1622
|
+
if (repos.length > 0) {
|
|
1623
|
+
console.log(chalk_1.default.dim(` Found ${repos.length} repository(ies) to clone`));
|
|
1624
|
+
return await createLocalDockerGenbox(name, options.provider || 'claude', {
|
|
1625
|
+
repos: repos.map((r) => ({
|
|
1626
|
+
name: r.name,
|
|
1627
|
+
url: r.url,
|
|
1628
|
+
path: r.path || `/home/dev/workspace/${r.name}`,
|
|
1629
|
+
branch: r.branch,
|
|
1630
|
+
})),
|
|
1631
|
+
gitToken,
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
// No repos from config - create empty genbox
|
|
1541
1635
|
return await createLocalDockerGenbox(name, options.provider || 'claude');
|
|
1542
1636
|
}
|
|
1543
1637
|
/**
|
|
@@ -1714,6 +1808,32 @@ async function createLocalVmFromProject(config, options) {
|
|
|
1714
1808
|
console.log(` ${chalk_1.default.bold('Name:')} ${name}`);
|
|
1715
1809
|
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
1716
1810
|
console.log('');
|
|
1811
|
+
// Resolve repos from profile configuration
|
|
1812
|
+
const configLoader = new config_loader_1.ConfigLoader();
|
|
1813
|
+
const resolver = new profile_resolver_1.ProfileResolver(configLoader);
|
|
1814
|
+
const createOptions = {
|
|
1815
|
+
name,
|
|
1816
|
+
profile: selectedProfile,
|
|
1817
|
+
yes: options.skipPrompts,
|
|
1818
|
+
};
|
|
1819
|
+
const resolvedConfig = await resolver.resolve(config, createOptions);
|
|
1820
|
+
const repos = resolvedConfig.repos || [];
|
|
1821
|
+
// Get git token from environment for private repos
|
|
1822
|
+
const gitToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
1823
|
+
// Create genbox with repos to clone (no mounting - matching cloud behavior)
|
|
1824
|
+
if (repos.length > 0) {
|
|
1825
|
+
console.log(chalk_1.default.dim(` Found ${repos.length} repository(ies) to clone`));
|
|
1826
|
+
return await createLocalVmGenbox(name, options.provider || 'claude', {
|
|
1827
|
+
repos: repos.map((r) => ({
|
|
1828
|
+
name: r.name,
|
|
1829
|
+
url: r.url,
|
|
1830
|
+
path: r.path || `/home/dev/workspace/${r.name}`,
|
|
1831
|
+
branch: r.branch,
|
|
1832
|
+
})),
|
|
1833
|
+
gitToken,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
// No repos from config - create empty genbox
|
|
1717
1837
|
return await createLocalVmGenbox(name, options.provider || 'claude');
|
|
1718
1838
|
}
|
|
1719
1839
|
/**
|
|
@@ -242,8 +242,11 @@ function formatRemoteSession(session) {
|
|
|
242
242
|
/**
|
|
243
243
|
* Display sessions in formatted output
|
|
244
244
|
*/
|
|
245
|
-
function displaySessions(result, options) {
|
|
245
|
+
function displaySessions(result, options = {}) {
|
|
246
246
|
const { sessions, remoteSessions, cloudGenboxes } = result;
|
|
247
|
+
const terminology = options.terminology || 'sessions';
|
|
248
|
+
const termSingular = terminology === 'genboxes' ? 'genbox' : 'session';
|
|
249
|
+
const termPlural = terminology === 'genboxes' ? 'genboxes' : 'sessions';
|
|
247
250
|
// Merge cloud genboxes that aren't in local sessions
|
|
248
251
|
const localCloudIds = new Set(sessions.filter(s => s.type === 'cloud').map(s => s.infrastructure?.genboxId));
|
|
249
252
|
const additionalCloudSessions = cloudGenboxes
|
|
@@ -264,68 +267,59 @@ function displaySessions(result, options) {
|
|
|
264
267
|
},
|
|
265
268
|
}));
|
|
266
269
|
const allSessions = [...sessions, ...additionalCloudSessions];
|
|
270
|
+
// Separate into local and cloud
|
|
271
|
+
const localTypes = ['native', 'docker', 'multipass', 'genbox'];
|
|
272
|
+
const localSessions = allSessions.filter(s => localTypes.includes(s.type));
|
|
273
|
+
const cloudSessions = allSessions.filter(s => s.type === 'cloud');
|
|
267
274
|
// No sessions at all
|
|
268
275
|
if (allSessions.length === 0 && remoteSessions.length === 0) {
|
|
269
|
-
console.log(chalk_1.default.dim(
|
|
276
|
+
console.log(chalk_1.default.dim(`\n No ${termPlural} found.\n`));
|
|
270
277
|
console.log(chalk_1.default.dim(' Run ') + chalk_1.default.cyan('gb claude') + chalk_1.default.dim(' or ') + chalk_1.default.cyan('gb session') + chalk_1.default.dim(' to create one.\n'));
|
|
271
278
|
return;
|
|
272
279
|
}
|
|
273
280
|
console.log('');
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
// Display LOCAL sessions/genboxes
|
|
282
|
+
if (localSessions.length > 0) {
|
|
283
|
+
const localTitle = terminology === 'genboxes' ? 'Local Genboxes' : 'Local Sessions';
|
|
284
|
+
console.log(chalk_1.default.bold(` ${localTitle}:`));
|
|
285
|
+
console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
|
|
286
|
+
for (const session of localSessions) {
|
|
287
|
+
console.log(formatSessionUnified(session));
|
|
288
|
+
}
|
|
289
|
+
console.log('');
|
|
280
290
|
}
|
|
281
|
-
// Display
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
docker: 'DOCKER SESSIONS',
|
|
286
|
-
multipass: 'MULTIPASS VM SESSIONS',
|
|
287
|
-
genbox: 'LOCAL GENBOX SESSIONS',
|
|
288
|
-
cloud: 'CLOUD SESSIONS',
|
|
289
|
-
};
|
|
290
|
-
for (const type of typeOrder) {
|
|
291
|
-
const typeSessions = groups.get(type);
|
|
292
|
-
if (!typeSessions || typeSessions.length === 0)
|
|
293
|
-
continue;
|
|
294
|
-
console.log(chalk_1.default.bold(` ${typeLabels[type]}`));
|
|
291
|
+
// Display CLOUD sessions/genboxes
|
|
292
|
+
if (cloudSessions.length > 0) {
|
|
293
|
+
const cloudTitle = terminology === 'genboxes' ? 'Cloud Genboxes' : 'Cloud Sessions';
|
|
294
|
+
console.log(chalk_1.default.bold(` ${cloudTitle}:`));
|
|
295
295
|
console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
|
|
296
|
-
for (const session of
|
|
297
|
-
console.log(
|
|
298
|
-
// Show
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
else if (type === 'cloud' && session.infrastructure?.ipAddress) {
|
|
303
|
-
const previewUrl = `https://${session.infrastructure.genboxName || session.name}.genbox.dev`;
|
|
296
|
+
for (const session of cloudSessions) {
|
|
297
|
+
console.log(formatSessionUnified(session));
|
|
298
|
+
// Show URL
|
|
299
|
+
const previewUrl = session.previewUrl ||
|
|
300
|
+
(session.infrastructure?.ipAddress ? `https://${session.infrastructure.genboxName || session.name}.genbox.dev` : null);
|
|
301
|
+
if (previewUrl) {
|
|
304
302
|
console.log(chalk_1.default.dim(` └─ ${previewUrl}`));
|
|
305
303
|
}
|
|
306
304
|
// Show AI sessions running on this cloud genbox
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
console.log(chalk_1.default.dim(` ├─ `) + chalk_1.default.green('●') + ` ${providerColor(remote.name)} ${chalk_1.default.dim(`(${remote.provider} session)`)}`);
|
|
317
|
-
}
|
|
305
|
+
const genboxName = session.infrastructure?.genboxName || session.name;
|
|
306
|
+
const sessionsOnGenbox = remoteSessions.filter(r => r.genboxName === genboxName);
|
|
307
|
+
for (const remote of sessionsOnGenbox) {
|
|
308
|
+
const providerColor = {
|
|
309
|
+
claude: chalk_1.default.magenta,
|
|
310
|
+
gemini: chalk_1.default.blue,
|
|
311
|
+
codex: chalk_1.default.green,
|
|
312
|
+
}[remote.provider] || chalk_1.default.cyan;
|
|
313
|
+
console.log(chalk_1.default.dim(` ├─ `) + chalk_1.default.green('●') + ` ${providerColor(remote.name)} ${chalk_1.default.dim(`(${remote.provider} session)`)}`);
|
|
318
314
|
}
|
|
319
315
|
}
|
|
320
316
|
console.log('');
|
|
321
317
|
}
|
|
322
318
|
// Show remote sessions that don't have a parent cloud session displayed
|
|
323
|
-
const displayedGenboxes = new Set(
|
|
324
|
-
.filter(s => s.type === 'cloud')
|
|
325
|
-
.map(s => s.infrastructure?.genboxName || s.name));
|
|
319
|
+
const displayedGenboxes = new Set(cloudSessions.map(s => s.infrastructure?.genboxName || s.name));
|
|
326
320
|
const orphanRemoteSessions = remoteSessions.filter(r => !displayedGenboxes.has(r.genboxName));
|
|
327
321
|
if (orphanRemoteSessions.length > 0) {
|
|
328
|
-
console.log(chalk_1.default.bold('
|
|
322
|
+
console.log(chalk_1.default.bold(' Remote AI Sessions:'));
|
|
329
323
|
console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
|
|
330
324
|
for (const session of orphanRemoteSessions) {
|
|
331
325
|
console.log(formatRemoteSession(session));
|
|
@@ -333,20 +327,67 @@ function displaySessions(result, options) {
|
|
|
333
327
|
console.log('');
|
|
334
328
|
}
|
|
335
329
|
// Summary
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (count > 0) {
|
|
340
|
-
countByType.push(`${count} ${type}`);
|
|
341
|
-
}
|
|
330
|
+
const countParts = [];
|
|
331
|
+
if (localSessions.length > 0) {
|
|
332
|
+
countParts.push(`${localSessions.length} local`);
|
|
342
333
|
}
|
|
343
|
-
if (
|
|
344
|
-
|
|
334
|
+
if (cloudSessions.length > 0) {
|
|
335
|
+
countParts.push(`${cloudSessions.length} cloud`);
|
|
345
336
|
}
|
|
346
337
|
const total = allSessions.length + orphanRemoteSessions.length;
|
|
347
|
-
console.log(chalk_1.default.dim(` ${total}
|
|
338
|
+
console.log(chalk_1.default.dim(` ${total} ${total !== 1 ? termPlural : termSingular} (${countParts.join(', ')})`));
|
|
348
339
|
console.log('');
|
|
349
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Format a session for unified display (used by both gb list and gb session list)
|
|
343
|
+
*/
|
|
344
|
+
function formatSessionUnified(session) {
|
|
345
|
+
const statusIcon = {
|
|
346
|
+
starting: chalk_1.default.yellow('◐'),
|
|
347
|
+
running: chalk_1.default.green('●'),
|
|
348
|
+
active: chalk_1.default.green('●'),
|
|
349
|
+
idle: chalk_1.default.yellow('●'),
|
|
350
|
+
stopped: chalk_1.default.red('●'),
|
|
351
|
+
error: chalk_1.default.red('✗'),
|
|
352
|
+
migrated: chalk_1.default.blue('↗'),
|
|
353
|
+
}[session.status] || chalk_1.default.dim('○');
|
|
354
|
+
const provider = chalk_1.default.cyan((session.provider || 'claude').padEnd(8));
|
|
355
|
+
// Show type/isolation
|
|
356
|
+
let typeLabel;
|
|
357
|
+
switch (session.type) {
|
|
358
|
+
case 'native':
|
|
359
|
+
typeLabel = chalk_1.default.magenta('native'.padEnd(12));
|
|
360
|
+
break;
|
|
361
|
+
case 'docker':
|
|
362
|
+
typeLabel = chalk_1.default.dim('docker'.padEnd(12));
|
|
363
|
+
break;
|
|
364
|
+
case 'multipass':
|
|
365
|
+
typeLabel = chalk_1.default.dim('multipass'.padEnd(12));
|
|
366
|
+
break;
|
|
367
|
+
case 'genbox':
|
|
368
|
+
typeLabel = chalk_1.default.dim('genbox'.padEnd(12));
|
|
369
|
+
break;
|
|
370
|
+
case 'cloud':
|
|
371
|
+
typeLabel = chalk_1.default.blue('cloud'.padEnd(12));
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
typeLabel = chalk_1.default.dim('unknown'.padEnd(12));
|
|
375
|
+
}
|
|
376
|
+
const status = {
|
|
377
|
+
starting: chalk_1.default.yellow('starting'),
|
|
378
|
+
running: chalk_1.default.green('running'),
|
|
379
|
+
active: chalk_1.default.green('active'),
|
|
380
|
+
idle: chalk_1.default.yellow('idle'),
|
|
381
|
+
stopped: chalk_1.default.red('stopped'),
|
|
382
|
+
error: chalk_1.default.red('error'),
|
|
383
|
+
migrated: chalk_1.default.blue('migrated'),
|
|
384
|
+
}[session.status] || chalk_1.default.dim(session.status);
|
|
385
|
+
const age = session.createdAt
|
|
386
|
+
? chalk_1.default.dim(formatTimeAgo(session.createdAt))
|
|
387
|
+
: '';
|
|
388
|
+
const syncLabel = session.syncEnabled ? chalk_1.default.dim('[synced]') : '';
|
|
389
|
+
return ` ${statusIcon} ${chalk_1.default.bold((session.name || 'unnamed').padEnd(22))} ${provider} ${typeLabel} ${status.padEnd(10)} ${age} ${syncLabel}`.trimEnd();
|
|
390
|
+
}
|
|
350
391
|
/**
|
|
351
392
|
* Simple list display for provider commands (--list)
|
|
352
393
|
*/
|
package/dist/ssh-config.js
CHANGED
|
@@ -114,6 +114,10 @@ function addSshConfigEntry(entry) {
|
|
|
114
114
|
' UserKnownHostsFile /dev/null',
|
|
115
115
|
' LogLevel ERROR',
|
|
116
116
|
];
|
|
117
|
+
// Add port if specified (for Docker containers)
|
|
118
|
+
if (entry.port) {
|
|
119
|
+
configBlock.push(` Port ${entry.port}`);
|
|
120
|
+
}
|
|
117
121
|
if (keyPath) {
|
|
118
122
|
configBlock.push(` IdentityFile ${keyPath}`);
|
|
119
123
|
configBlock.push(' IdentitiesOnly yes');
|