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.
@@ -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;
@@ -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
@@ -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);
@@ -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
+ }
@@ -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`));
@@ -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;
@@ -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;
@@ -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
- let genboxes = await getGenboxes({
64
- all: options.all,
65
- includeTerminated: options.includeTerminated,
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
- genboxes = genboxes.filter(g => g.status === options.statusFilter);
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
- const genbox = genboxes.find(g => g.name === name);
74
- if (!genbox) {
75
- // Check if it exists in all genboxes (might be in different project)
76
- if (!options.all && projectName) {
77
- const allGenboxes = await getGenboxes({ all: true, includeTerminated: options.includeTerminated });
78
- const existsElsewhere = allGenboxes.find(g => g.name === name);
79
- if (existsElsewhere) {
80
- console.error(chalk_1.default.yellow(`Genbox '${name}' exists but belongs to a different project.`));
81
- console.error(chalk_1.default.dim(` Use ${chalk_1.default.cyan('--all')} flag to access it.`));
82
- }
83
- else {
84
- console.error(chalk_1.default.red(`Genbox '${name}' not found.`));
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
- return { genbox, genboxes };
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 (genboxes.length === 0) {
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 (genboxes.length === 1) {
107
- const genbox = genboxes[0];
108
- console.log(chalk_1.default.dim(`Auto-selected: ${chalk_1.default.cyan(genbox.name)}`));
109
- return { genbox, genboxes };
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
- const choices = genboxes.map(g => {
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 { genbox: selected, genboxes };
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:ro"`);
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.176",
3
+ "version": "1.0.177",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {