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.
@@ -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
- // Fetch cloud and local genboxes in parallel
118
- const [cloudResult, localResult] = await Promise.allSettled([
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
- // Show context header
283
- if (options.all) {
284
- console.log(chalk_1.default.bold('All Genboxes:'));
285
- }
286
- else if (projectName) {
287
- console.log(chalk_1.default.bold(`Genboxes for ${chalk_1.default.cyan(projectName)}:`));
288
- }
289
- else {
290
- console.log(chalk_1.default.bold('Your Genboxes:'));
291
- console.log(chalk_1.default.dim(' (Not in a project directory. Showing all genboxes.)'));
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
- if (cloudGenboxes.length > 0 || localSessions.length > 0) {
349
- console.log('');
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(chalk_1.default.dim('────────────────────────────────────────────────────'));
358
- console.log(chalk_1.default.yellow(` ${totalOrphaned} orphaned VM/container(s) found.`));
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
- * Render a local session in the list
205
+ * Get local genboxes for JSON output (preserves legacy format)
387
206
  */
388
- function renderLocalSession(session, nameWidth) {
389
- const appsRunning = session.apps.filter((a) => a.status === 'running').length;
390
- const infraRunning = session.infrastructure.filter((i) => i.status === 'running').length;
391
- const status = (appsRunning > 0 || infraRunning > 0) ? 'running' : 'stopped';
392
- const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.red;
393
- const namePart = chalk_1.default.cyan(session.name.padEnd(nameWidth));
394
- const statusPart = statusColor(status.padEnd(12));
395
- const typePart = chalk_1.default.dim(`local/${session.isolation || 'native'}`.padEnd(16));
396
- const sizePart = chalk_1.default.dim(`(${session.size})`);
397
- // Show age
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
- // Show auto-destroy status (not applicable for stopped/terminated)
475
- let protectedInfo = '';
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
- else if (genbox.status === 'provisioning') {
495
- // Show restore progress if available
496
- if (genbox.restoreProgress) {
497
- const progress = genbox.restoreProgress.progress || 0;
498
- const source = genbox.restoreProgress.bootSource || 'snapshot';
499
- if (progress < 100) {
500
- protectedInfo = chalk_1.default.cyan(` (restoring from ${source}: ${progress}%)`);
501
- }
502
- else {
503
- protectedInfo = chalk_1.default.cyan(' (booting...)');
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
- else if (genbox.status === 'terminated') {
511
- // No extra info for terminated
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
- const namePart = chalk_1.default.cyan(nameWithProject.padEnd(nameWidth));
537
- const statusPart = statusColor(genbox.status.padEnd(12));
538
- // Show appropriate IP text based on status
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
- const ipPart = chalk_1.default.dim(ipText.padEnd(16));
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) {
@@ -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 (for multipass VMs) ===
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
- process.stdout.write(`\r${statusLine.padEnd(80)}`);
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: workdir,
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 -p ${sshPort} dev@localhost`);
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: workdir,
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
- // Mount current working directory to /home/dev/workspace
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: workdir,
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('\n No sessions found.\n'));
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
- // Group sessions by type
275
- const groups = new Map();
276
- for (const session of allSessions) {
277
- const existing = groups.get(session.type) || [];
278
- existing.push(session);
279
- groups.set(session.type, existing);
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 order and labels
282
- const typeOrder = ['native', 'docker', 'multipass', 'genbox', 'cloud'];
283
- const typeLabels = {
284
- native: 'NATIVE SESSIONS (running directly on your machine)',
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 typeSessions) {
297
- console.log(formatSession(session));
298
- // Show additional info
299
- if (session.previewUrl) {
300
- console.log(chalk_1.default.dim(` └─ ${session.previewUrl}`));
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
- if (type === 'cloud') {
308
- const genboxName = session.infrastructure?.genboxName || session.name;
309
- const sessionsOnGenbox = remoteSessions.filter(r => r.genboxName === genboxName);
310
- for (const remote of sessionsOnGenbox) {
311
- const providerColor = {
312
- claude: chalk_1.default.magenta,
313
- gemini: chalk_1.default.blue,
314
- codex: chalk_1.default.green,
315
- }[remote.provider] || chalk_1.default.cyan;
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(allSessions
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(' REMOTE AI SESSIONS'));
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 countByType = [];
337
- for (const type of typeOrder) {
338
- const count = groups.get(type)?.length || 0;
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 (remoteSessions.length > 0) {
344
- countByType.push(`${remoteSessions.length} remote`);
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} session${total !== 1 ? 's' : ''} (${countByType.join(', ')})`));
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
  */
@@ -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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.203",
3
+ "version": "1.0.204",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {