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.
- package/dist/commands/destroy.js +157 -6
- package/dist/commands/list.js +172 -3
- package/dist/commands/provider-command.js +221 -28
- package/dist/commands/status.js +18 -0
- package/dist/genbox-selector.js +22 -5
- package/dist/lib/genbox-progress.js +69 -11
- package/dist/lib/genbox-wizard.js +855 -23
- package/package.json +1 -1
package/dist/commands/destroy.js
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/commands/list.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|