genbox 1.0.176 → 1.0.177
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/connect.js +10 -2
- package/dist/commands/create.js +69 -0
- package/dist/commands/destroy.js +33 -1
- package/dist/commands/list.js +53 -2
- package/dist/commands/start.js +37 -1
- package/dist/commands/status.js +76 -1
- package/dist/commands/stop.js +74 -1
- package/dist/genbox-selector.js +148 -29
- package/dist/lib/local-session-manager.js +3 -3
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -42,6 +42,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
42
42
|
const api_1 = require("../api");
|
|
43
43
|
const genbox_selector_1 = require("../genbox-selector");
|
|
44
44
|
const ssh_config_1 = require("../ssh-config");
|
|
45
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
45
46
|
const os = __importStar(require("os"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
47
48
|
const fs = __importStar(require("fs"));
|
|
@@ -60,13 +61,13 @@ function getPrivateSshKey() {
|
|
|
60
61
|
throw new Error('No SSH private key found in ~/.ssh/');
|
|
61
62
|
}
|
|
62
63
|
exports.connectCommand = new commander_1.Command('connect')
|
|
63
|
-
.description('SSH into a Genbox')
|
|
64
|
+
.description('SSH into a Genbox (or attach to local genbox)')
|
|
64
65
|
.argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
|
|
65
66
|
.option('-a, --all', 'Select from all genboxes (not just current project)')
|
|
66
67
|
.action(async (name, options) => {
|
|
67
68
|
try {
|
|
68
69
|
// 1. Select Genbox (interactive if no name provided)
|
|
69
|
-
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
70
|
+
const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
70
71
|
all: options.all,
|
|
71
72
|
selectMessage: 'Select a genbox to connect to:',
|
|
72
73
|
});
|
|
@@ -77,6 +78,13 @@ exports.connectCommand = new commander_1.Command('connect')
|
|
|
77
78
|
if (!target) {
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
// Handle local genbox - attach to session
|
|
82
|
+
if (isLocal && localSession) {
|
|
83
|
+
console.log(chalk_1.default.dim(`Attaching to local genbox ${chalk_1.default.bold(localSession.name)}...`));
|
|
84
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
85
|
+
await manager.attachToSession(localSession);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
80
88
|
if (!target.ipAddress) {
|
|
81
89
|
console.error(chalk_1.default.yellow(`Genbox '${target.name}' is still provisioning (no IP). Please wait.`));
|
|
82
90
|
return;
|
package/dist/commands/create.js
CHANGED
|
@@ -45,6 +45,7 @@ const fs = __importStar(require("fs"));
|
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
46
|
const child_process_1 = require("child_process");
|
|
47
47
|
const config_loader_1 = require("../config-loader");
|
|
48
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
48
49
|
const profile_resolver_1 = require("../profile-resolver");
|
|
49
50
|
const api_1 = require("../api");
|
|
50
51
|
const ssh_config_1 = require("../ssh-config");
|
|
@@ -326,9 +327,72 @@ async function promptForProfile(profiles) {
|
|
|
326
327
|
}
|
|
327
328
|
return selected;
|
|
328
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Create a local genbox (Docker container or VM)
|
|
332
|
+
* This creates the infrastructure without starting a session
|
|
333
|
+
*/
|
|
334
|
+
async function createLocalGenbox(nameArg, options) {
|
|
335
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
336
|
+
const provider = options.gemini ? 'gemini' : 'claude';
|
|
337
|
+
const isolation = options.vm ? 'multipass' : options.native ? 'native' : 'docker';
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(chalk_1.default.blue('=== Creating Local Genbox ==='));
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log(` ${chalk_1.default.bold('Type:')} ${isolation}`);
|
|
342
|
+
console.log(` ${chalk_1.default.bold('Provider:')} ${provider}`);
|
|
343
|
+
console.log(` ${chalk_1.default.bold('Directory:')} ${process.cwd()}`);
|
|
344
|
+
console.log('');
|
|
345
|
+
const spinner = (0, ora_1.default)(`Creating ${isolation} environment...`).start();
|
|
346
|
+
try {
|
|
347
|
+
const session = await manager.createSession({
|
|
348
|
+
provider,
|
|
349
|
+
isolation,
|
|
350
|
+
workdir: process.cwd(),
|
|
351
|
+
name: nameArg, // Use provided name if any
|
|
352
|
+
});
|
|
353
|
+
spinner.succeed(chalk_1.default.green('Local genbox created!'));
|
|
354
|
+
console.log('');
|
|
355
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
356
|
+
console.log(` ${chalk_1.default.bold('Name:')} ${session.name}`);
|
|
357
|
+
console.log(` ${chalk_1.default.bold('ID:')} ${session.id}`);
|
|
358
|
+
console.log(` ${chalk_1.default.bold('Provider:')} ${session.provider}`);
|
|
359
|
+
console.log(` ${chalk_1.default.bold('Isolation:')} ${session.isolation}`);
|
|
360
|
+
console.log(` ${chalk_1.default.bold('Status:')} ${chalk_1.default.green(session.status)}`);
|
|
361
|
+
if (session.containerId) {
|
|
362
|
+
console.log(` ${chalk_1.default.bold('Container:')} ${session.containerId.slice(0, 12)}`);
|
|
363
|
+
}
|
|
364
|
+
if (session.vmName) {
|
|
365
|
+
console.log(` ${chalk_1.default.bold('VM:')} ${session.vmName}`);
|
|
366
|
+
}
|
|
367
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
368
|
+
console.log('');
|
|
369
|
+
console.log(chalk_1.default.bold('Next steps:'));
|
|
370
|
+
console.log(` Attach: ${chalk_1.default.cyan(`gb session ${session.name}`)}`);
|
|
371
|
+
console.log(` List: ${chalk_1.default.cyan('gb session list')}`);
|
|
372
|
+
console.log(` Stop: ${chalk_1.default.cyan(`gb session stop ${session.name}`)}`);
|
|
373
|
+
console.log(` Destroy: ${chalk_1.default.cyan(`gb session kill ${session.name}`)}`);
|
|
374
|
+
console.log('');
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
spinner.fail(chalk_1.default.red(`Failed to create local genbox: ${error.message}`));
|
|
378
|
+
if (error.message.includes('Docker')) {
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log(chalk_1.default.yellow('Tip: Make sure Docker Desktop is running.'));
|
|
381
|
+
}
|
|
382
|
+
if (error.message.includes('multipass')) {
|
|
383
|
+
console.log('');
|
|
384
|
+
console.log(chalk_1.default.yellow('Tip: Install Multipass from https://multipass.run'));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
329
388
|
exports.createCommand = new commander_1.Command('create')
|
|
330
389
|
.description('Create a new Genbox environment')
|
|
331
390
|
.argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
|
|
391
|
+
.option('-l, --local', 'Create local genbox (Docker container or VM)')
|
|
392
|
+
.option('--vm', 'Use Multipass VM for local genbox')
|
|
393
|
+
.option('--native', 'Use native mode (no isolation) for local genbox')
|
|
394
|
+
.option('--claude', 'Use Claude provider (default)')
|
|
395
|
+
.option('--gemini', 'Use Gemini provider')
|
|
332
396
|
.option('-p, --profile <profile>', 'Use a predefined profile')
|
|
333
397
|
.option('-a, --apps <apps>', 'Comma-separated list of apps to include')
|
|
334
398
|
.option('--add-apps <apps>', 'Add apps to the profile')
|
|
@@ -347,6 +411,11 @@ exports.createCommand = new commander_1.Command('create')
|
|
|
347
411
|
.option('--inject-claude-auth', 'Inject local Claude Code credentials for remote execution')
|
|
348
412
|
.action(async (nameArg, options) => {
|
|
349
413
|
try {
|
|
414
|
+
// Handle local genbox creation
|
|
415
|
+
if (options.local) {
|
|
416
|
+
await createLocalGenbox(nameArg, options);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
350
419
|
// Show config warnings (if any)
|
|
351
420
|
(0, config_warnings_1.showConfigWarnings)();
|
|
352
421
|
// Handle restore mode
|
package/dist/commands/destroy.js
CHANGED
|
@@ -46,6 +46,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
46
46
|
const api_1 = require("../api");
|
|
47
47
|
const genbox_selector_1 = require("../genbox-selector");
|
|
48
48
|
const ssh_config_1 = require("../ssh-config");
|
|
49
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
49
50
|
/**
|
|
50
51
|
* Format genbox for display in selection list
|
|
51
52
|
*/
|
|
@@ -316,6 +317,32 @@ async function handleUncommittedChanges(genbox, options) {
|
|
|
316
317
|
}
|
|
317
318
|
return { proceed: true, saveWorkResult: result };
|
|
318
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Destroy a local genbox
|
|
322
|
+
*/
|
|
323
|
+
async function destroyLocalGenbox(session, options) {
|
|
324
|
+
// Confirm
|
|
325
|
+
let confirmed = options.yes;
|
|
326
|
+
if (!confirmed) {
|
|
327
|
+
confirmed = await (0, confirm_1.default)({
|
|
328
|
+
message: `Are you sure you want to destroy local genbox '${session.name}' (${session.isolation})?`,
|
|
329
|
+
default: false,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (!confirmed) {
|
|
333
|
+
console.log('Operation cancelled.');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const spinner = (0, ora_1.default)(`Destroying local genbox ${session.name}...`).start();
|
|
337
|
+
try {
|
|
338
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
339
|
+
await manager.destroySession(session.id);
|
|
340
|
+
spinner.succeed(chalk_1.default.green(`Local genbox '${session.name}' destroyed successfully.`));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
spinner.fail(chalk_1.default.red(`Failed to destroy local genbox: ${error.message}`));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
319
346
|
exports.destroyCommand = new commander_1.Command('destroy')
|
|
320
347
|
.alias('delete')
|
|
321
348
|
.description('Destroy one or more Genboxes')
|
|
@@ -332,7 +359,7 @@ exports.destroyCommand = new commander_1.Command('destroy')
|
|
|
332
359
|
return;
|
|
333
360
|
}
|
|
334
361
|
// Single genbox deletion flow
|
|
335
|
-
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
362
|
+
const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
336
363
|
all: options.all, // If --all with name, search in all genboxes
|
|
337
364
|
selectMessage: 'Select a genbox to destroy:',
|
|
338
365
|
});
|
|
@@ -343,6 +370,11 @@ exports.destroyCommand = new commander_1.Command('destroy')
|
|
|
343
370
|
if (!target) {
|
|
344
371
|
return;
|
|
345
372
|
}
|
|
373
|
+
// Handle local genbox destruction
|
|
374
|
+
if (isLocal && localSession) {
|
|
375
|
+
await destroyLocalGenbox(localSession, options);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
346
378
|
// Check for uncommitted changes if genbox is running
|
|
347
379
|
if (target.status === 'running') {
|
|
348
380
|
const { proceed } = await handleUncommittedChanges(target, options);
|
package/dist/commands/list.js
CHANGED
|
@@ -8,6 +8,7 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const api_1 = require("../api");
|
|
10
10
|
const genbox_selector_1 = require("../genbox-selector");
|
|
11
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
11
12
|
exports.listCommand = new commander_1.Command('list')
|
|
12
13
|
.alias('ls')
|
|
13
14
|
.description('List genboxes (scoped to current project by default)')
|
|
@@ -51,7 +52,7 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
51
52
|
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
52
53
|
if (genboxes.length === 0) {
|
|
53
54
|
if (projectName && !options.all) {
|
|
54
|
-
console.log(chalk_1.default.yellow(`No genboxes found for project '${projectName}'.`));
|
|
55
|
+
console.log(chalk_1.default.yellow(`No cloud genboxes found for project '${projectName}'.`));
|
|
55
56
|
console.log('');
|
|
56
57
|
console.log(chalk_1.default.dim(' Create one with:'));
|
|
57
58
|
console.log(chalk_1.default.cyan(' $ genbox create <name>'));
|
|
@@ -60,11 +61,13 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
60
61
|
console.log(chalk_1.default.cyan(' $ genbox list --all'));
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
|
-
console.log(chalk_1.default.yellow('No genboxes found.'));
|
|
64
|
+
console.log(chalk_1.default.yellow('No cloud genboxes found.'));
|
|
64
65
|
console.log('');
|
|
65
66
|
console.log(chalk_1.default.dim(' Create one with:'));
|
|
66
67
|
console.log(chalk_1.default.cyan(' $ genbox create <name>'));
|
|
67
68
|
}
|
|
69
|
+
// Still show local genboxes even if no cloud ones
|
|
70
|
+
await showLocalGenboxes(options);
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
// Sort by createdAt (newest first)
|
|
@@ -218,6 +221,8 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
218
221
|
if (projectName && !options.all) {
|
|
219
222
|
console.log(chalk_1.default.dim(` Showing ${genboxes.length} genbox(es) for this project. Use ${chalk_1.default.cyan('--all')} to see all.`));
|
|
220
223
|
}
|
|
224
|
+
// Show local genboxes
|
|
225
|
+
await showLocalGenboxes(options);
|
|
221
226
|
}
|
|
222
227
|
catch (error) {
|
|
223
228
|
if (error instanceof api_1.AuthenticationError) {
|
|
@@ -227,3 +232,49 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
227
232
|
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
228
233
|
}
|
|
229
234
|
});
|
|
235
|
+
/**
|
|
236
|
+
* Show local genboxes section
|
|
237
|
+
*/
|
|
238
|
+
async function showLocalGenboxes(options) {
|
|
239
|
+
try {
|
|
240
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
241
|
+
const localSessions = await manager.listSessions();
|
|
242
|
+
if (localSessions.length === 0) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log(chalk_1.default.bold('Local Genboxes:'));
|
|
247
|
+
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
248
|
+
const nameWidth = Math.max(12, ...localSessions.map(s => s.name.length));
|
|
249
|
+
for (const session of localSessions) {
|
|
250
|
+
const statusColor = session.status === 'running' || session.status === 'active' ? chalk_1.default.green :
|
|
251
|
+
session.status === 'stopped' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
252
|
+
const namePart = chalk_1.default.cyan(session.name.padEnd(nameWidth));
|
|
253
|
+
const statusPart = statusColor(session.status.padEnd(12));
|
|
254
|
+
const isolationPart = chalk_1.default.dim(session.isolation.padEnd(10));
|
|
255
|
+
const providerPart = chalk_1.default.dim(`(${session.provider})`);
|
|
256
|
+
// Show age
|
|
257
|
+
let agePart = '';
|
|
258
|
+
if (session.createdAt) {
|
|
259
|
+
const age = Date.now() - new Date(session.createdAt).getTime();
|
|
260
|
+
const mins = Math.floor(age / 60000);
|
|
261
|
+
const hours = Math.floor(mins / 60);
|
|
262
|
+
if (hours > 0) {
|
|
263
|
+
agePart = chalk_1.default.dim(` ${hours}h ago`);
|
|
264
|
+
}
|
|
265
|
+
else if (mins > 0) {
|
|
266
|
+
agePart = chalk_1.default.dim(` ${mins}m ago`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
agePart = chalk_1.default.dim(' just now');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(`${namePart} ${statusPart} ${isolationPart} ${providerPart}${agePart}`);
|
|
273
|
+
}
|
|
274
|
+
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
275
|
+
console.log(chalk_1.default.dim(` ${localSessions.length} local genbox(es). Use ${chalk_1.default.cyan('gb session')} to attach.`));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Silently ignore local session errors
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -7,9 +7,40 @@ exports.startCommand = void 0;
|
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
10
11
|
const api_1 = require("../api");
|
|
11
12
|
const genbox_selector_1 = require("../genbox-selector");
|
|
12
13
|
const ssh_config_1 = require("../ssh-config");
|
|
14
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
15
|
+
/**
|
|
16
|
+
* Start a stopped local genbox (Docker container or VM)
|
|
17
|
+
*/
|
|
18
|
+
async function startLocalGenbox(session) {
|
|
19
|
+
const spinner = (0, ora_1.default)(`Starting ${session.name}...`).start();
|
|
20
|
+
try {
|
|
21
|
+
if (session.isolation === 'docker' && session.containerId) {
|
|
22
|
+
(0, child_process_1.execSync)(`docker start ${session.containerId}`, { stdio: 'pipe' });
|
|
23
|
+
}
|
|
24
|
+
else if (session.isolation === 'multipass') {
|
|
25
|
+
(0, child_process_1.execSync)(`multipass start ${session.name}`, { stdio: 'pipe' });
|
|
26
|
+
}
|
|
27
|
+
else if (session.isolation === 'native') {
|
|
28
|
+
spinner.info(chalk_1.default.yellow('Native mode sessions do not need to be started'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Update session status
|
|
32
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
33
|
+
session.status = 'running';
|
|
34
|
+
await manager.updateSession(session);
|
|
35
|
+
spinner.succeed(chalk_1.default.green(`Local genbox '${session.name}' started`));
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk_1.default.dim(` Attach with: ${chalk_1.default.cyan(`gb session ${session.name}`)}`));
|
|
38
|
+
console.log(chalk_1.default.dim(` Or connect: ${chalk_1.default.cyan(`gb connect ${session.name}`)}`));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
spinner.fail(chalk_1.default.red(`Failed to start local genbox: ${error.message}`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
13
44
|
exports.startCommand = new commander_1.Command('start')
|
|
14
45
|
.alias('resume')
|
|
15
46
|
.description('Start/resume a stopped Genbox')
|
|
@@ -18,7 +49,7 @@ exports.startCommand = new commander_1.Command('start')
|
|
|
18
49
|
.addHelpText('after', '\nAliases: gb resume')
|
|
19
50
|
.action(async (name, options) => {
|
|
20
51
|
try {
|
|
21
|
-
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
52
|
+
const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
22
53
|
selectMessage: 'Select a genbox to start:',
|
|
23
54
|
statusFilter: 'stopped',
|
|
24
55
|
});
|
|
@@ -29,6 +60,11 @@ exports.startCommand = new commander_1.Command('start')
|
|
|
29
60
|
if (!target) {
|
|
30
61
|
return;
|
|
31
62
|
}
|
|
63
|
+
// Handle local genbox start
|
|
64
|
+
if (isLocal && localSession) {
|
|
65
|
+
await startLocalGenbox(localSession);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
32
68
|
if (target.status !== 'stopped') {
|
|
33
69
|
if (target.status === 'running') {
|
|
34
70
|
console.log(chalk_1.default.yellow(`Genbox '${target.name}' is already running`));
|
package/dist/commands/status.js
CHANGED
|
@@ -239,6 +239,76 @@ function tailCloudInitLogs(ip, keyPath) {
|
|
|
239
239
|
function sleep(ms) {
|
|
240
240
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Display status for a local genbox
|
|
244
|
+
*/
|
|
245
|
+
async function displayLocalGenboxStatus(session) {
|
|
246
|
+
console.log(chalk_1.default.blue(`[INFO] Local Genbox: ${chalk_1.default.cyan(session.name)}`));
|
|
247
|
+
console.log('');
|
|
248
|
+
const statusColor = session.status === 'running' || session.status === 'active' ? chalk_1.default.green :
|
|
249
|
+
session.status === 'stopped' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
250
|
+
console.log(chalk_1.default.blue('[INFO] === Details ==='));
|
|
251
|
+
console.log(` Status: ${statusColor(session.status)}`);
|
|
252
|
+
console.log(` Provider: ${session.provider}`);
|
|
253
|
+
console.log(` Isolation: ${session.isolation}`);
|
|
254
|
+
console.log(` Workdir: ${chalk_1.default.dim(session.workdir || 'N/A')}`);
|
|
255
|
+
if (session.createdAt) {
|
|
256
|
+
const age = Date.now() - new Date(session.createdAt).getTime();
|
|
257
|
+
const mins = Math.floor(age / 60000);
|
|
258
|
+
const hours = Math.floor(mins / 60);
|
|
259
|
+
const ageStr = hours > 0 ? `${hours}h ${mins % 60}m ago` : mins > 0 ? `${mins}m ago` : 'just now';
|
|
260
|
+
console.log(` Created: ${chalk_1.default.dim(ageStr)}`);
|
|
261
|
+
}
|
|
262
|
+
if (session.lastActivityAt) {
|
|
263
|
+
const lastActivity = Date.now() - new Date(session.lastActivityAt).getTime();
|
|
264
|
+
const mins = Math.floor(lastActivity / 60000);
|
|
265
|
+
const hours = Math.floor(mins / 60);
|
|
266
|
+
const activityStr = hours > 0 ? `${hours}h ${mins % 60}m ago` : mins > 0 ? `${mins}m ago` : 'just now';
|
|
267
|
+
console.log(` Activity: ${chalk_1.default.dim(activityStr)}`);
|
|
268
|
+
}
|
|
269
|
+
console.log('');
|
|
270
|
+
// Get container/VM status if Docker
|
|
271
|
+
if (session.isolation === 'docker' && session.containerId) {
|
|
272
|
+
try {
|
|
273
|
+
const containerStatus = (0, child_process_1.execSync)(`docker inspect --format='{{.State.Status}}' ${session.containerId} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
274
|
+
console.log(chalk_1.default.blue('[INFO] === Docker Container ==='));
|
|
275
|
+
console.log(` Container ID: ${chalk_1.default.dim(session.containerId.substring(0, 12))}`);
|
|
276
|
+
console.log(` Status: ${containerStatus === 'running' ? chalk_1.default.green(containerStatus) : chalk_1.default.yellow(containerStatus)}`);
|
|
277
|
+
// Get resource usage
|
|
278
|
+
try {
|
|
279
|
+
const stats = (0, child_process_1.execSync)(`docker stats --no-stream --format='{{.CPUPerc}}\t{{.MemUsage}}' ${session.containerId} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
280
|
+
if (stats) {
|
|
281
|
+
const [cpu, mem] = stats.split('\t');
|
|
282
|
+
console.log(` CPU: ${cpu}`);
|
|
283
|
+
console.log(` Memory: ${mem}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Ignore stats errors
|
|
288
|
+
}
|
|
289
|
+
console.log('');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
console.log(chalk_1.default.yellow('[WARN] Container not found or not running'));
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if (session.isolation === 'multipass') {
|
|
297
|
+
console.log(chalk_1.default.blue('[INFO] === Multipass VM ==='));
|
|
298
|
+
try {
|
|
299
|
+
const vmInfo = (0, child_process_1.execSync)(`multipass info ${session.name} --format=csv 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
300
|
+
console.log(chalk_1.default.dim(vmInfo));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
console.log(chalk_1.default.yellow(' VM info not available'));
|
|
304
|
+
}
|
|
305
|
+
console.log('');
|
|
306
|
+
}
|
|
307
|
+
// Show next steps
|
|
308
|
+
console.log(chalk_1.default.dim('Commands:'));
|
|
309
|
+
console.log(chalk_1.default.dim(` Attach: ${chalk_1.default.cyan(`gb session ${session.name}`)}`));
|
|
310
|
+
console.log(chalk_1.default.dim(` Destroy: ${chalk_1.default.cyan(`gb destroy ${session.name}`)}`));
|
|
311
|
+
}
|
|
242
312
|
exports.statusCommand = new commander_1.Command('status')
|
|
243
313
|
.description('Check cloud-init progress and service status of a Genbox')
|
|
244
314
|
.argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
|
|
@@ -248,7 +318,7 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
248
318
|
.action(async (name, options) => {
|
|
249
319
|
try {
|
|
250
320
|
// 1. Select Genbox (interactive if no name provided)
|
|
251
|
-
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
321
|
+
const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
252
322
|
all: options.all,
|
|
253
323
|
selectMessage: 'Select a genbox to check status:',
|
|
254
324
|
});
|
|
@@ -259,6 +329,11 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
259
329
|
if (!target) {
|
|
260
330
|
return;
|
|
261
331
|
}
|
|
332
|
+
// Handle local genbox status
|
|
333
|
+
if (isLocal && localSession) {
|
|
334
|
+
await displayLocalGenboxStatus(localSession);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
262
337
|
if (!target.ipAddress) {
|
|
263
338
|
console.error(chalk_1.default.yellow(`Genbox '${target.name}' is still provisioning (no IP yet). Please wait.`));
|
|
264
339
|
return;
|
package/dist/commands/stop.js
CHANGED
|
@@ -8,9 +8,77 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const confirm_1 = __importDefault(require("@inquirer/confirm"));
|
|
10
10
|
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
11
12
|
const api_1 = require("../api");
|
|
12
13
|
const genbox_selector_1 = require("../genbox-selector");
|
|
13
14
|
const ssh_config_1 = require("../ssh-config");
|
|
15
|
+
const local_session_manager_1 = require("../lib/local-session-manager");
|
|
16
|
+
/**
|
|
17
|
+
* Stop a local genbox (Docker container or VM)
|
|
18
|
+
*/
|
|
19
|
+
async function stopLocalGenbox(session, options) {
|
|
20
|
+
const log = (msg) => { if (!options.quiet)
|
|
21
|
+
console.log(msg); };
|
|
22
|
+
// Confirm
|
|
23
|
+
let confirmed = options.yes;
|
|
24
|
+
if (!confirmed) {
|
|
25
|
+
log('');
|
|
26
|
+
log(chalk_1.default.blue('Stop will:'));
|
|
27
|
+
if (session.isolation === 'docker') {
|
|
28
|
+
log(chalk_1.default.dim(' 1. Stop the Docker container'));
|
|
29
|
+
log(chalk_1.default.dim(' 2. Container state is preserved (can restart)'));
|
|
30
|
+
}
|
|
31
|
+
else if (session.isolation === 'multipass') {
|
|
32
|
+
log(chalk_1.default.dim(' 1. Stop the Multipass VM'));
|
|
33
|
+
log(chalk_1.default.dim(' 2. VM state is preserved (can restart)'));
|
|
34
|
+
}
|
|
35
|
+
log('');
|
|
36
|
+
confirmed = await (0, confirm_1.default)({
|
|
37
|
+
message: `Stop local genbox '${session.name}'?`,
|
|
38
|
+
default: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!confirmed) {
|
|
42
|
+
log(chalk_1.default.dim('Operation cancelled.'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const spinner = options.quiet ? null : (0, ora_1.default)(`Stopping ${session.name}...`).start();
|
|
46
|
+
try {
|
|
47
|
+
if (session.isolation === 'docker' && session.containerId) {
|
|
48
|
+
(0, child_process_1.execSync)(`docker stop ${session.containerId}`, { stdio: 'pipe' });
|
|
49
|
+
}
|
|
50
|
+
else if (session.isolation === 'multipass') {
|
|
51
|
+
(0, child_process_1.execSync)(`multipass stop ${session.name}`, { stdio: 'pipe' });
|
|
52
|
+
}
|
|
53
|
+
else if (session.isolation === 'native') {
|
|
54
|
+
// Native mode - just update status (tmux session stays)
|
|
55
|
+
log(chalk_1.default.yellow('Native mode sessions cannot be stopped (use gb destroy instead)'));
|
|
56
|
+
if (spinner)
|
|
57
|
+
spinner.stop();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Update session status
|
|
61
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
62
|
+
session.status = 'stopped';
|
|
63
|
+
await manager.updateSession(session);
|
|
64
|
+
if (spinner) {
|
|
65
|
+
spinner.succeed(chalk_1.default.green(`Local genbox '${session.name}' stopped`));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(session.name);
|
|
69
|
+
}
|
|
70
|
+
log('');
|
|
71
|
+
log(chalk_1.default.dim(` To resume: ${chalk_1.default.cyan(`gb start ${session.name}`)}`));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (spinner) {
|
|
75
|
+
spinner.fail(chalk_1.default.red(`Failed to stop local genbox: ${error.message}`));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.error(chalk_1.default.red(`Failed to stop local genbox: ${error.message}`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
14
82
|
exports.stopCommand = new commander_1.Command('stop')
|
|
15
83
|
.description('Stop a running Genbox (saves state for quick resume)')
|
|
16
84
|
.argument('[name]', 'Name of the Genbox to stop')
|
|
@@ -20,7 +88,7 @@ exports.stopCommand = new commander_1.Command('stop')
|
|
|
20
88
|
const log = (msg) => { if (!options.quiet)
|
|
21
89
|
console.log(msg); };
|
|
22
90
|
try {
|
|
23
|
-
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
91
|
+
const { genbox: target, cancelled, isLocal, localSession } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
24
92
|
selectMessage: 'Select a genbox to stop:',
|
|
25
93
|
statusFilter: 'running',
|
|
26
94
|
});
|
|
@@ -31,6 +99,11 @@ exports.stopCommand = new commander_1.Command('stop')
|
|
|
31
99
|
if (!target) {
|
|
32
100
|
return;
|
|
33
101
|
}
|
|
102
|
+
// Handle local genbox stop
|
|
103
|
+
if (isLocal && localSession) {
|
|
104
|
+
await stopLocalGenbox(localSession, options);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
34
107
|
if (target.status !== 'running') {
|
|
35
108
|
console.error(chalk_1.default.red(`Genbox '${target.name}' is not running (status: ${target.status})`));
|
|
36
109
|
return;
|
package/dist/genbox-selector.js
CHANGED
|
@@ -12,6 +12,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
12
12
|
const select_1 = __importDefault(require("@inquirer/select"));
|
|
13
13
|
const api_1 = require("./api");
|
|
14
14
|
const config_1 = require("./config");
|
|
15
|
+
const local_session_manager_1 = require("./lib/local-session-manager");
|
|
15
16
|
/**
|
|
16
17
|
* Get project name from genbox.yaml if available
|
|
17
18
|
*/
|
|
@@ -52,47 +53,121 @@ async function getGenboxes(options = {}) {
|
|
|
52
53
|
function isInProjectContext() {
|
|
53
54
|
return getProjectContext() !== null;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert a LocalSession to a Genbox-compatible object
|
|
58
|
+
*/
|
|
59
|
+
function localSessionToGenbox(session) {
|
|
60
|
+
// Handle createdAt - it may be a string (from JSON) or a Date object
|
|
61
|
+
let createdAtStr;
|
|
62
|
+
if (session.createdAt) {
|
|
63
|
+
if (typeof session.createdAt === 'string') {
|
|
64
|
+
createdAtStr = session.createdAt;
|
|
65
|
+
}
|
|
66
|
+
else if (session.createdAt instanceof Date) {
|
|
67
|
+
createdAtStr = session.createdAt.toISOString();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
_id: session.id,
|
|
72
|
+
name: session.name,
|
|
73
|
+
status: session.status === 'active' ? 'running' : session.status,
|
|
74
|
+
ipAddress: undefined, // Local genboxes don't have IP
|
|
75
|
+
size: 'local',
|
|
76
|
+
project: session.workdir?.split('/').pop() || undefined,
|
|
77
|
+
createdAt: createdAtStr,
|
|
78
|
+
_isLocal: true,
|
|
79
|
+
_localSession: session,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get local genboxes as Genbox objects
|
|
84
|
+
*/
|
|
85
|
+
async function getLocalGenboxes() {
|
|
86
|
+
try {
|
|
87
|
+
const manager = (0, local_session_manager_1.getLocalSessionManager)();
|
|
88
|
+
const sessions = await manager.listSessions();
|
|
89
|
+
return sessions.map(localSessionToGenbox);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
55
95
|
/**
|
|
56
96
|
* Interactive genbox selector with project context awareness
|
|
57
97
|
* - If one genbox: auto-selects it
|
|
58
98
|
* - If multiple: shows interactive select
|
|
59
99
|
* - If none: shows appropriate message
|
|
100
|
+
* - Now includes local genboxes by default
|
|
60
101
|
*/
|
|
61
102
|
async function selectGenbox(name, options = {}) {
|
|
62
103
|
const projectName = getProjectContext();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
const includeLocal = options.includeLocal !== false && !options.cloudOnly;
|
|
105
|
+
const localOnly = options.localOnly === true;
|
|
106
|
+
// Fetch cloud genboxes (unless localOnly)
|
|
107
|
+
let genboxes = [];
|
|
108
|
+
if (!localOnly) {
|
|
109
|
+
try {
|
|
110
|
+
genboxes = await getGenboxes({
|
|
111
|
+
all: options.all,
|
|
112
|
+
includeTerminated: options.includeTerminated,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// If we're including local, don't fail on cloud fetch errors
|
|
117
|
+
if (!includeLocal) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
// Silently continue with just local genboxes
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Fetch local genboxes
|
|
124
|
+
let localGenboxes = [];
|
|
125
|
+
if (includeLocal || localOnly) {
|
|
126
|
+
localGenboxes = await getLocalGenboxes();
|
|
127
|
+
}
|
|
128
|
+
// Combine cloud and local genboxes
|
|
129
|
+
const allGenboxes = [...genboxes, ...localGenboxes];
|
|
67
130
|
// Filter by status if specified
|
|
131
|
+
let filteredGenboxes = allGenboxes;
|
|
68
132
|
if (options.statusFilter) {
|
|
69
|
-
|
|
133
|
+
filteredGenboxes = filteredGenboxes.filter(g => g.status === options.statusFilter);
|
|
70
134
|
}
|
|
71
135
|
// If name is provided, find it directly
|
|
72
136
|
if (name) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
137
|
+
// First check local genboxes (higher priority for matching names)
|
|
138
|
+
const localMatch = localGenboxes.find(g => g.name === name);
|
|
139
|
+
if (localMatch) {
|
|
140
|
+
return {
|
|
141
|
+
genbox: localMatch,
|
|
142
|
+
genboxes: genboxes,
|
|
143
|
+
isLocal: true,
|
|
144
|
+
localSession: localMatch._localSession,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Then check cloud genboxes
|
|
148
|
+
const cloudMatch = genboxes.find(g => g.name === name);
|
|
149
|
+
if (cloudMatch) {
|
|
150
|
+
return { genbox: cloudMatch, genboxes };
|
|
151
|
+
}
|
|
152
|
+
// Check if it exists in all genboxes (might be in different project)
|
|
153
|
+
if (!options.all && projectName) {
|
|
154
|
+
const allCloudGenboxes = await getGenboxes({ all: true, includeTerminated: options.includeTerminated });
|
|
155
|
+
const existsElsewhere = allCloudGenboxes.find(g => g.name === name);
|
|
156
|
+
if (existsElsewhere) {
|
|
157
|
+
console.error(chalk_1.default.yellow(`Genbox '${name}' exists but belongs to a different project.`));
|
|
158
|
+
console.error(chalk_1.default.dim(` Use ${chalk_1.default.cyan('--all')} flag to access it.`));
|
|
86
159
|
}
|
|
87
160
|
else {
|
|
88
161
|
console.error(chalk_1.default.red(`Genbox '${name}' not found.`));
|
|
89
162
|
}
|
|
90
|
-
return { genbox: null, genboxes };
|
|
91
163
|
}
|
|
92
|
-
|
|
164
|
+
else {
|
|
165
|
+
console.error(chalk_1.default.red(`Genbox '${name}' not found.`));
|
|
166
|
+
}
|
|
167
|
+
return { genbox: null, genboxes };
|
|
93
168
|
}
|
|
94
169
|
// No name provided - interactive selection
|
|
95
|
-
if (
|
|
170
|
+
if (filteredGenboxes.length === 0) {
|
|
96
171
|
const emptyMsg = options.emptyMessage || (projectName
|
|
97
172
|
? `No genboxes found for project '${projectName}'.`
|
|
98
173
|
: 'No genboxes found.');
|
|
@@ -103,26 +178,70 @@ async function selectGenbox(name, options = {}) {
|
|
|
103
178
|
return { genbox: null, genboxes };
|
|
104
179
|
}
|
|
105
180
|
// Only one genbox - auto-select
|
|
106
|
-
if (
|
|
107
|
-
const genbox =
|
|
108
|
-
|
|
109
|
-
|
|
181
|
+
if (filteredGenboxes.length === 1) {
|
|
182
|
+
const genbox = filteredGenboxes[0];
|
|
183
|
+
const isLocal = !!genbox._isLocal;
|
|
184
|
+
const localLabel = isLocal ? chalk_1.default.magenta(' [local]') : '';
|
|
185
|
+
console.log(chalk_1.default.dim(`Auto-selected: ${chalk_1.default.cyan(genbox.name)}${localLabel}`));
|
|
186
|
+
return {
|
|
187
|
+
genbox,
|
|
188
|
+
genboxes,
|
|
189
|
+
isLocal,
|
|
190
|
+
localSession: genbox._localSession,
|
|
191
|
+
};
|
|
110
192
|
}
|
|
111
193
|
// Multiple genboxes - interactive select
|
|
112
194
|
try {
|
|
113
|
-
|
|
195
|
+
// Build choices with cloud genboxes first, then local
|
|
196
|
+
const cloudChoices = genboxes
|
|
197
|
+
.filter(g => options.statusFilter ? g.status === options.statusFilter : true)
|
|
198
|
+
.map(g => {
|
|
114
199
|
const statusColor = g.status === 'running' ? chalk_1.default.green :
|
|
115
200
|
g.status === 'terminated' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
116
201
|
return {
|
|
117
202
|
name: `${g.name} ${statusColor(`(${g.status})`)} ${chalk_1.default.dim(g.ipAddress || 'No IP')}`,
|
|
118
|
-
value: g,
|
|
203
|
+
value: { genbox: g, isLocal: false },
|
|
119
204
|
};
|
|
120
205
|
});
|
|
206
|
+
const localChoices = localGenboxes
|
|
207
|
+
.filter(g => options.statusFilter ? g.status === options.statusFilter : true)
|
|
208
|
+
.map(g => {
|
|
209
|
+
const statusColor = g.status === 'running' ? chalk_1.default.green :
|
|
210
|
+
g.status === 'stopped' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
211
|
+
const isolationInfo = g._localSession.isolation || 'docker';
|
|
212
|
+
return {
|
|
213
|
+
name: `${g.name} ${statusColor(`(${g.status})`)} ${chalk_1.default.magenta('[local]')} ${chalk_1.default.dim(`(${isolationInfo})`)}`,
|
|
214
|
+
value: { genbox: g, isLocal: true, localSession: g._localSession },
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
const choices = [];
|
|
218
|
+
// Add cloud section if there are cloud genboxes
|
|
219
|
+
if (cloudChoices.length > 0) {
|
|
220
|
+
choices.push(...cloudChoices);
|
|
221
|
+
}
|
|
222
|
+
// Add local section if there are local genboxes
|
|
223
|
+
if (localChoices.length > 0) {
|
|
224
|
+
if (cloudChoices.length > 0) {
|
|
225
|
+
// Add separator before local section
|
|
226
|
+
choices.push({
|
|
227
|
+
name: chalk_1.default.dim('── Local ──────────────────────────'),
|
|
228
|
+
value: null, // Separator (will be filtered)
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
choices.push(...localChoices);
|
|
232
|
+
}
|
|
233
|
+
// Filter out separators for selection
|
|
234
|
+
const selectableChoices = choices.filter(c => c.value !== null);
|
|
121
235
|
const selected = await (0, select_1.default)({
|
|
122
236
|
message: options.selectMessage || 'Select a genbox:',
|
|
123
|
-
choices,
|
|
237
|
+
choices: selectableChoices,
|
|
124
238
|
});
|
|
125
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
genbox: selected.genbox,
|
|
241
|
+
genboxes,
|
|
242
|
+
isLocal: selected.isLocal,
|
|
243
|
+
localSession: selected.localSession,
|
|
244
|
+
};
|
|
126
245
|
}
|
|
127
246
|
catch (error) {
|
|
128
247
|
// Handle Ctrl+C
|
|
@@ -323,7 +323,7 @@ class LocalSessionManager {
|
|
|
323
323
|
}
|
|
324
324
|
const session = {
|
|
325
325
|
id: this.generateId(),
|
|
326
|
-
name: this.generateName(options.provider),
|
|
326
|
+
name: options.name || this.generateName(options.provider),
|
|
327
327
|
location: 'local',
|
|
328
328
|
isolation: options.isolation,
|
|
329
329
|
provider: options.provider,
|
|
@@ -480,10 +480,10 @@ CMD ["tail", "-f", "/dev/null"]
|
|
|
480
480
|
getCredentialMounts() {
|
|
481
481
|
const home = os.homedir();
|
|
482
482
|
const mounts = [];
|
|
483
|
-
// Claude credentials
|
|
483
|
+
// Claude credentials (needs write access for debug files)
|
|
484
484
|
const claudeDir = path.join(home, '.claude');
|
|
485
485
|
if (fs.existsSync(claudeDir)) {
|
|
486
|
-
mounts.push(`-v "${claudeDir}:/home/dev/.claude
|
|
486
|
+
mounts.push(`-v "${claudeDir}:/home/dev/.claude"`);
|
|
487
487
|
}
|
|
488
488
|
// Google credentials (for Gemini)
|
|
489
489
|
const gcloudDir = path.join(home, '.config', 'gcloud');
|