genbox 1.0.198 → 1.0.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,11 +43,13 @@ const confirm_1 = __importDefault(require("@inquirer/confirm"));
43
43
  const select_1 = __importDefault(require("@inquirer/select"));
44
44
  const prompts = __importStar(require("@inquirer/prompts"));
45
45
  const ora_1 = __importDefault(require("ora"));
46
+ const child_process_1 = require("child_process");
46
47
  const api_1 = require("../api");
47
48
  const genbox_selector_1 = require("../genbox-selector");
48
49
  const ssh_config_1 = require("../ssh-config");
49
50
  const unified_session_1 = require("../lib/unified-session");
50
51
  const local_genbox_provisioner_1 = require("../lib/local-genbox-provisioner");
52
+ const list_1 = require("./list");
51
53
  /**
52
54
  * Format genbox for display in selection list
53
55
  */
@@ -72,6 +74,18 @@ function formatLocalSessionChoice(s) {
72
74
  value: { type: 'local', session: s },
73
75
  };
74
76
  }
77
+ /**
78
+ * Format orphaned VM/container for display in selection list
79
+ */
80
+ function formatOrphanedChoice(o) {
81
+ const status = o.state === 'Running' || o.state === 'running' ? 'running' : 'stopped';
82
+ const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.yellow;
83
+ const resourceInfo = o.cpus ? chalk_1.default.dim(` (${o.cpus} CPU, ${o.memoryGB}GB)`) : '';
84
+ return {
85
+ name: `${chalk_1.default.yellow(o.name)} ${statusColor(`(${status})`)} ${chalk_1.default.yellow(`[orphaned/${o.type}]`)}${resourceInfo}`,
86
+ value: { type: 'orphaned', orphan: o },
87
+ };
88
+ }
75
89
  /**
76
90
  * Handle bulk delete flow when --all flag is used without a name
77
91
  */
@@ -131,9 +145,17 @@ async function handleBulkDelete(options) {
131
145
  const projectLocalSessions = projectName
132
146
  ? allLocalSessions.filter(s => s.projectName === projectName || !s.projectName)
133
147
  : allLocalSessions;
148
+ // Detect orphaned VMs/containers
149
+ const trackedNames = new Set(allLocalSessions.map(s => s.name));
150
+ for (const s of allLocalSessions) {
151
+ if (s.vmName)
152
+ trackedNames.add(s.vmName);
153
+ }
154
+ const orphanedGenboxes = (0, list_1.detectOrphanedGenboxes)(trackedNames);
134
155
  const totalCloud = allCloudGenboxes.length;
135
156
  const totalLocal = allLocalSessions.length;
136
- const totalAll = totalCloud + totalLocal;
157
+ const totalOrphaned = orphanedGenboxes.length;
158
+ const totalAll = totalCloud + totalLocal + totalOrphaned;
137
159
  const projectCloud = projectCloudGenboxes.length;
138
160
  const projectLocal = projectLocalSessions.length;
139
161
  const projectTotal = projectCloud + projectLocal;
@@ -143,16 +165,18 @@ async function handleBulkDelete(options) {
143
165
  }
144
166
  let targetCloudGenboxes;
145
167
  let targetLocalSessions;
168
+ let targetOrphaned;
146
169
  let scopeLabel;
147
170
  // If in project context, ask user to choose scope
148
171
  if (projectName && projectTotal > 0) {
172
+ const orphanedNote = totalOrphaned > 0 ? `, ${totalOrphaned} orphaned` : '';
149
173
  const scopeChoices = [
150
174
  {
151
175
  name: `Delete from ${chalk_1.default.cyan(projectName)} project ${chalk_1.default.dim(`(${projectTotal} genbox${projectTotal === 1 ? '' : 'es'}: ${projectCloud} cloud, ${projectLocal} local)`)}`,
152
176
  value: 'project',
153
177
  },
154
178
  {
155
- name: `Delete globally ${chalk_1.default.dim(`(${totalAll} genbox${totalAll === 1 ? '' : 'es'}: ${totalCloud} cloud, ${totalLocal} local)`)}`,
179
+ name: `Delete globally ${chalk_1.default.dim(`(${totalAll} total: ${totalCloud} cloud, ${totalLocal} local${orphanedNote})`)}`,
156
180
  value: 'global',
157
181
  },
158
182
  ];
@@ -163,11 +187,13 @@ async function handleBulkDelete(options) {
163
187
  if (scope === 'project') {
164
188
  targetCloudGenboxes = projectCloudGenboxes;
165
189
  targetLocalSessions = projectLocalSessions;
190
+ targetOrphaned = []; // Orphaned VMs are shown only in global scope
166
191
  scopeLabel = `project '${projectName}'`;
167
192
  }
168
193
  else {
169
194
  targetCloudGenboxes = allCloudGenboxes;
170
195
  targetLocalSessions = allLocalSessions;
196
+ targetOrphaned = orphanedGenboxes;
171
197
  scopeLabel = 'all projects';
172
198
  }
173
199
  }
@@ -175,10 +201,12 @@ async function handleBulkDelete(options) {
175
201
  // Not in project context or no project genboxes - show all
176
202
  targetCloudGenboxes = allCloudGenboxes;
177
203
  targetLocalSessions = allLocalSessions;
204
+ targetOrphaned = orphanedGenboxes;
178
205
  scopeLabel = 'all projects';
179
- console.log(chalk_1.default.dim(`Found ${totalAll} genbox${totalAll === 1 ? '' : 'es'} globally (${totalCloud} cloud, ${totalLocal} local).\n`));
206
+ const orphanedNote = totalOrphaned > 0 ? `, ${totalOrphaned} orphaned` : '';
207
+ console.log(chalk_1.default.dim(`Found ${totalAll} genbox${totalAll === 1 ? '' : 'es'} globally (${totalCloud} cloud, ${totalLocal} local${orphanedNote}).\n`));
180
208
  }
181
- if (targetCloudGenboxes.length === 0 && targetLocalSessions.length === 0) {
209
+ if (targetCloudGenboxes.length === 0 && targetLocalSessions.length === 0 && targetOrphaned.length === 0) {
182
210
  console.log(chalk_1.default.yellow(`No genboxes found in ${scopeLabel}.`));
183
211
  return;
184
212
  }
@@ -202,6 +230,15 @@ async function handleBulkDelete(options) {
202
230
  checked: false,
203
231
  });
204
232
  }
233
+ // Add orphaned VMs/containers
234
+ for (const o of targetOrphaned) {
235
+ const formatted = formatOrphanedChoice(o);
236
+ choices.push({
237
+ name: formatted.name,
238
+ value: formatted.value,
239
+ checked: false,
240
+ });
241
+ }
205
242
  console.log(''); // Add spacing
206
243
  const selectedItems = await prompts.checkbox({
207
244
  message: `Select genboxes to destroy from ${scopeLabel}:`,
@@ -212,9 +249,10 @@ async function handleBulkDelete(options) {
212
249
  console.log(chalk_1.default.yellow('No genboxes selected.'));
213
250
  return;
214
251
  }
215
- // Separate cloud and local selections
252
+ // Separate cloud, local, and orphaned selections
216
253
  const selectedCloudGenboxes = selectedItems.filter(i => i.type === 'cloud').map(i => i.genbox);
217
254
  const selectedLocalSessions = selectedItems.filter(i => i.type === 'local').map(i => i.session);
255
+ const selectedOrphaned = selectedItems.filter(i => i.type === 'orphaned').map(i => i.orphan);
218
256
  // Show summary and confirm
219
257
  console.log('');
220
258
  console.log(chalk_1.default.yellow.bold('⚠️ The following genboxes will be PERMANENTLY destroyed:'));
@@ -229,6 +267,11 @@ async function handleBulkDelete(options) {
229
267
  const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.yellow;
230
268
  console.log(` ${chalk_1.default.red('•')} ${s.name} ${statusColor(`(${status})`)} ${chalk_1.default.dim(`[local/${s.isolation}]`)}`);
231
269
  });
270
+ selectedOrphaned.forEach(o => {
271
+ const status = o.state === 'Running' || o.state === 'running' ? 'running' : 'stopped';
272
+ const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.yellow;
273
+ console.log(` ${chalk_1.default.red('•')} ${chalk_1.default.yellow(o.name)} ${statusColor(`(${status})`)} ${chalk_1.default.yellow(`[orphaned/${o.type}]`)}`);
274
+ });
232
275
  console.log('');
233
276
  let confirmed = options.yes;
234
277
  if (!confirmed) {
@@ -260,10 +303,51 @@ async function handleBulkDelete(options) {
260
303
  }
261
304
  }
262
305
  // Delete local genboxes
306
+ const sessionManager = (0, unified_session_1.getUnifiedSessionManager)();
263
307
  for (const session of selectedLocalSessions) {
264
308
  const spinner = (0, ora_1.default)(`Destroying ${session.name} (local)...`).start();
265
309
  try {
266
- await provisioner.destroy(session.id);
310
+ // Check if this is a UnifiedSessionManager session (wizard-created VM)
311
+ const unifiedSession = sessionManager.getSession(session.id);
312
+ if (unifiedSession) {
313
+ // Delete the actual VM/container based on type
314
+ if (unifiedSession.type === 'multipass') {
315
+ const vmName = unifiedSession.infrastructure?.vmName || unifiedSession.name;
316
+ try {
317
+ (0, child_process_1.execSync)(`multipass delete ${vmName} --purge`, { stdio: 'pipe' });
318
+ }
319
+ catch {
320
+ // VM might already be deleted
321
+ }
322
+ }
323
+ else if (unifiedSession.type === 'docker') {
324
+ // Try containerId first, then fall back to container name
325
+ const containerId = unifiedSession.infrastructure?.containerId;
326
+ const containerName = unifiedSession.infrastructure?.containerName || unifiedSession.name;
327
+ if (containerId) {
328
+ try {
329
+ (0, child_process_1.execSync)(`docker rm -f ${containerId}`, { stdio: 'pipe' });
330
+ }
331
+ catch {
332
+ // Container might already be deleted
333
+ }
334
+ }
335
+ else if (containerName) {
336
+ try {
337
+ (0, child_process_1.execSync)(`docker rm -f ${containerName}`, { stdio: 'pipe' });
338
+ }
339
+ catch {
340
+ // Container might not exist or already be deleted
341
+ }
342
+ }
343
+ }
344
+ // Delete from UnifiedSessionManager
345
+ await sessionManager.deleteSession(session.id);
346
+ }
347
+ else {
348
+ // Fallback to LocalGenboxProvisioner for legacy sessions
349
+ await provisioner.destroy(session.id);
350
+ }
267
351
  spinner.succeed(chalk_1.default.green(`Destroyed '${session.name}' (local)`));
268
352
  successCount++;
269
353
  }
@@ -272,6 +356,24 @@ async function handleBulkDelete(options) {
272
356
  failCount++;
273
357
  }
274
358
  }
359
+ // Delete orphaned VMs/containers
360
+ for (const orphan of selectedOrphaned) {
361
+ const spinner = (0, ora_1.default)(`Destroying ${orphan.name} (orphaned ${orphan.type})...`).start();
362
+ try {
363
+ if (orphan.type === 'multipass') {
364
+ (0, child_process_1.execSync)(`multipass delete ${orphan.name} --purge`, { stdio: 'pipe' });
365
+ }
366
+ else if (orphan.type === 'docker') {
367
+ (0, child_process_1.execSync)(`docker rm -f ${orphan.name}`, { stdio: 'pipe' });
368
+ }
369
+ spinner.succeed(chalk_1.default.green(`Destroyed '${orphan.name}' (orphaned ${orphan.type})`));
370
+ successCount++;
371
+ }
372
+ catch (error) {
373
+ spinner.fail(chalk_1.default.red(`Failed to destroy '${orphan.name}': ${error.message}`));
374
+ failCount++;
375
+ }
376
+ }
275
377
  // Summary
276
378
  console.log('');
277
379
  if (failCount === 0) {
@@ -468,6 +570,55 @@ exports.destroyCommand = new commander_1.Command('destroy')
468
570
  await handleBulkDelete(options);
469
571
  return;
470
572
  }
573
+ // Check if the name matches an orphaned genbox first
574
+ if (name) {
575
+ // Get tracked names to detect orphans
576
+ const sessionManager = (0, unified_session_1.getUnifiedSessionManager)();
577
+ const provisioner = (0, local_genbox_provisioner_1.getLocalGenboxProvisioner)();
578
+ const trackedNames = new Set();
579
+ for (const s of provisioner.listSessions()) {
580
+ trackedNames.add(s.name);
581
+ if (s.vmName)
582
+ trackedNames.add(s.vmName);
583
+ }
584
+ for (const s of sessionManager.listSessions({ type: ['multipass', 'docker'] })) {
585
+ trackedNames.add(s.name);
586
+ if (s.infrastructure?.vmName)
587
+ trackedNames.add(s.infrastructure.vmName);
588
+ }
589
+ const orphanedGenboxes = (0, list_1.detectOrphanedGenboxes)(trackedNames);
590
+ const matchedOrphan = orphanedGenboxes.find(o => o.name === name);
591
+ if (matchedOrphan) {
592
+ // Handle orphaned genbox deletion
593
+ const status = matchedOrphan.state === 'Running' || matchedOrphan.state === 'running' ? 'running' : 'stopped';
594
+ console.log(chalk_1.default.yellow(`Found orphaned ${matchedOrphan.type} VM: ${matchedOrphan.name} (${status})`));
595
+ let confirmed = options.yes;
596
+ if (!confirmed) {
597
+ confirmed = await (0, confirm_1.default)({
598
+ message: `Are you sure you want to destroy orphaned ${matchedOrphan.type} '${matchedOrphan.name}'?`,
599
+ default: false,
600
+ });
601
+ }
602
+ if (!confirmed) {
603
+ console.log('Operation cancelled.');
604
+ return;
605
+ }
606
+ const spinner = (0, ora_1.default)(`Destroying ${matchedOrphan.name} (orphaned ${matchedOrphan.type})...`).start();
607
+ try {
608
+ if (matchedOrphan.type === 'multipass') {
609
+ (0, child_process_1.execSync)(`multipass delete ${matchedOrphan.name} --purge`, { stdio: 'pipe' });
610
+ }
611
+ else if (matchedOrphan.type === 'docker') {
612
+ (0, child_process_1.execSync)(`docker rm -f ${matchedOrphan.name}`, { stdio: 'pipe' });
613
+ }
614
+ spinner.succeed(chalk_1.default.green(`Destroyed orphaned ${matchedOrphan.type} '${matchedOrphan.name}' successfully.`));
615
+ }
616
+ catch (error) {
617
+ spinner.fail(chalk_1.default.red(`Failed to destroy '${matchedOrphan.name}': ${error.message}`));
618
+ }
619
+ return;
620
+ }
621
+ }
471
622
  // Single genbox deletion flow
472
623
  const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
473
624
  all: options.all, // If --all with name, search in all genboxes
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.listCommand = void 0;
6
+ exports.GENBOX_VM_PREFIXES = exports.listCommand = void 0;
7
+ exports.detectOrphanedGenboxes = detectOrphanedGenboxes;
7
8
  const commander_1 = require("commander");
8
9
  const chalk_1 = __importDefault(require("chalk"));
9
10
  const child_process_1 = require("child_process");
@@ -11,6 +12,98 @@ const api_1 = require("../api");
11
12
  const genbox_selector_1 = require("../genbox-selector");
12
13
  const local_genbox_provisioner_1 = require("../lib/local-genbox-provisioner");
13
14
  const unified_session_1 = require("../lib/unified-session");
15
+ // Prefixes used by genbox VMs/containers
16
+ const GENBOX_VM_PREFIXES = ['gemini-', 'claude-', 'codex-', 'genbox-'];
17
+ exports.GENBOX_VM_PREFIXES = GENBOX_VM_PREFIXES;
18
+ /**
19
+ * Detect orphaned Multipass VMs and Docker containers that look like genboxes
20
+ * but aren't tracked in the session registry
21
+ */
22
+ function detectOrphanedGenboxes(trackedNames) {
23
+ const orphaned = [];
24
+ // Check Multipass VMs
25
+ try {
26
+ const vmList = (0, child_process_1.execSync)('multipass list --format json 2>/dev/null', { encoding: 'utf8' });
27
+ const vms = JSON.parse(vmList);
28
+ for (const vm of vms.list || []) {
29
+ const vmName = vm.name;
30
+ // Check if it looks like a genbox VM and isn't tracked
31
+ const looksLikeGenbox = GENBOX_VM_PREFIXES.some(prefix => vmName.startsWith(prefix));
32
+ if (looksLikeGenbox && !trackedNames.has(vmName)) {
33
+ // Get detailed info
34
+ try {
35
+ const vmInfo = (0, child_process_1.execSync)(`multipass info ${vmName} --format json 2>/dev/null`, { encoding: 'utf8' });
36
+ const info = JSON.parse(vmInfo);
37
+ const details = info.info?.[vmName];
38
+ if (details) {
39
+ // Parse memory
40
+ let memoryGB = 0;
41
+ if (details.memory?.total) {
42
+ const memStr = String(details.memory.total);
43
+ if (memStr.includes('GiB')) {
44
+ memoryGB = parseFloat(memStr.replace('GiB', ''));
45
+ }
46
+ else if (memStr.includes('MiB')) {
47
+ memoryGB = parseFloat(memStr.replace('MiB', '')) / 1024;
48
+ }
49
+ }
50
+ // Parse disk
51
+ let diskGB = 0;
52
+ const diskInfo = details.disks?.sda1;
53
+ if (diskInfo?.total) {
54
+ const diskStr = String(diskInfo.total);
55
+ if (diskStr.includes('GiB')) {
56
+ diskGB = parseFloat(diskStr.replace('GiB', ''));
57
+ }
58
+ else if (diskStr.includes('MiB')) {
59
+ diskGB = parseFloat(diskStr.replace('MiB', '')) / 1024;
60
+ }
61
+ }
62
+ orphaned.push({
63
+ name: vmName,
64
+ type: 'multipass',
65
+ state: details.state || 'unknown',
66
+ cpus: parseInt(details.cpu_count, 10) || 0,
67
+ memoryGB: Math.round(memoryGB * 10) / 10,
68
+ diskGB: Math.round(diskGB * 10) / 10,
69
+ ipAddress: details.ipv4?.[0],
70
+ });
71
+ }
72
+ }
73
+ catch {
74
+ // Still add with basic info
75
+ orphaned.push({
76
+ name: vmName,
77
+ type: 'multipass',
78
+ state: vm.state || 'unknown',
79
+ });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // Multipass not available
86
+ }
87
+ // Check Docker containers
88
+ try {
89
+ const containerList = (0, child_process_1.execSync)('docker ps -a --format "{{.Names}}\\t{{.State}}" 2>/dev/null', { encoding: 'utf8' });
90
+ for (const line of containerList.trim().split('\n').filter(Boolean)) {
91
+ const [containerName, state] = line.split('\t');
92
+ const looksLikeGenbox = GENBOX_VM_PREFIXES.some(prefix => containerName.startsWith(prefix));
93
+ if (looksLikeGenbox && !trackedNames.has(containerName)) {
94
+ orphaned.push({
95
+ name: containerName,
96
+ type: 'docker',
97
+ state: state || 'unknown',
98
+ });
99
+ }
100
+ }
101
+ }
102
+ catch {
103
+ // Docker not available
104
+ }
105
+ return orphaned;
106
+ }
14
107
  exports.listCommand = new commander_1.Command('list')
15
108
  .alias('ls')
16
109
  .description('List genboxes (scoped to current project by default)')
@@ -72,6 +165,24 @@ exports.listCommand = new commander_1.Command('list')
72
165
  // VM might not exist anymore
73
166
  }
74
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
+ }
75
186
  // Convert to LocalGenboxSession format for rendering
76
187
  sessions.push({
77
188
  id: s.id,
@@ -121,9 +232,18 @@ exports.listCommand = new commander_1.Command('list')
121
232
  }
122
233
  console.log(chalk_1.default.yellow(`⚠ Could not fetch cloud genboxes: ${error.message}`));
123
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);
124
243
  const totalCloud = cloudGenboxes.length;
125
244
  const totalLocal = localSessions.length;
126
- const totalAll = totalCloud + totalLocal;
245
+ const totalOrphaned = orphanedGenboxes.length;
246
+ const totalAll = totalCloud + totalLocal + totalOrphaned;
127
247
  // JSON output mode
128
248
  if (options.json) {
129
249
  const cloudOutput = cloudGenboxes.map(g => ({
@@ -146,7 +266,17 @@ exports.listCommand = new commander_1.Command('list')
146
266
  project: s.projectName,
147
267
  createdAt: s.createdAt,
148
268
  }));
149
- console.log(JSON.stringify([...cloudOutput, ...localOutput], null, 2));
269
+ const orphanedOutput = orphanedGenboxes.map(o => ({
270
+ name: o.name,
271
+ type: 'orphaned',
272
+ status: o.state === 'Running' ? 'running' : 'stopped',
273
+ isolation: o.type,
274
+ ipAddress: o.ipAddress,
275
+ cpus: o.cpus,
276
+ memoryGB: o.memoryGB,
277
+ diskGB: o.diskGB,
278
+ }));
279
+ console.log(JSON.stringify([...cloudOutput, ...localOutput, ...orphanedOutput], null, 2));
150
280
  return;
151
281
  }
152
282
  // Show context header
@@ -213,6 +343,22 @@ exports.listCommand = new commander_1.Command('list')
213
343
  console.log(chalk_1.default.dim(` ${totalLocal} local genbox(es).`));
214
344
  console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb session <name>')} to start or attach to an AI session.`));
215
345
  }
346
+ // Show ORPHANED genboxes section
347
+ 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('────────────────────────────────────────────────────'));
353
+ const orphanedNameWidth = Math.max(12, ...orphanedGenboxes.map(o => o.name.length));
354
+ for (const orphan of orphanedGenboxes) {
355
+ renderOrphanedGenbox(orphan, orphanedNameWidth);
356
+ }
357
+ console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
358
+ console.log(chalk_1.default.yellow(` ${totalOrphaned} orphaned VM/container(s) found.`));
359
+ console.log(chalk_1.default.dim(` These exist in ${chalk_1.default.cyan('Multipass/Docker')} but aren't tracked by genbox.`));
360
+ console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb delete <name>')} to clean them up.`));
361
+ }
216
362
  // Hint for --all
217
363
  if (projectName && !options.all) {
218
364
  console.log('');
@@ -404,3 +550,26 @@ function renderCloudGenbox(genbox, nameWidth, showAll) {
404
550
  const extraInfo = endHourInfo + protectedInfo;
405
551
  console.log(`${namePart} ${statusPart} ${ipPart} ${sizePart}${extraInfo}`);
406
552
  }
553
+ /**
554
+ * Render an orphaned VM/container in the list
555
+ */
556
+ function renderOrphanedGenbox(orphan, nameWidth) {
557
+ const status = orphan.state === 'Running' || orphan.state === 'running' ? 'running' : 'stopped';
558
+ const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.red;
559
+ const namePart = chalk_1.default.yellow(orphan.name.padEnd(nameWidth));
560
+ const statusPart = statusColor(status.padEnd(12));
561
+ const typePart = chalk_1.default.dim(`orphaned/${orphan.type}`.padEnd(18));
562
+ // Show resource info if available
563
+ let resourcePart = '';
564
+ if (orphan.cpus || orphan.memoryGB) {
565
+ const parts = [];
566
+ if (orphan.cpus)
567
+ parts.push(`${orphan.cpus} CPU`);
568
+ if (orphan.memoryGB)
569
+ parts.push(`${orphan.memoryGB}GB`);
570
+ if (orphan.diskGB)
571
+ parts.push(`${orphan.diskGB}GB disk`);
572
+ resourcePart = chalk_1.default.dim(` (${parts.join(', ')})`);
573
+ }
574
+ console.log(`${namePart} ${statusPart} ${typePart}${resourcePart}`);
575
+ }