genbox 1.0.223 → 1.0.224

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.
Files changed (2) hide show
  1. package/dist/commands/list.js +307 -112
  2. package/package.json +1 -1
@@ -104,107 +104,172 @@ function detectOrphanedGenboxes(trackedNames) {
104
104
  }
105
105
  return orphaned;
106
106
  }
107
- exports.listCommand = new commander_1.Command('list')
108
- .alias('ls')
109
- .description('List genboxes (scoped to current project by default)')
110
- .option('-a, --all', 'Show all genboxes across all projects')
111
- .option('--terminated', 'Include terminated genboxes')
112
- .option('--json', 'Output as JSON (for scripting)')
113
- .addHelpText('after', '\nAliases: gb ls')
114
- .action(async (options) => {
115
- try {
116
- const projectName = (0, genbox_selector_1.getProjectContext)();
117
- const projectPath = (0, genbox_selector_1.isInProjectContext)() ? process.cwd() : undefined;
118
- // JSON output mode - use legacy detailed format
119
- if (options.json) {
120
- const [cloudGenboxes, localSessions] = await Promise.all([
121
- (0, genbox_selector_1.getGenboxes)({ all: options.all, includeTerminated: options.terminated }).catch(() => []),
122
- getLocalGenboxesForJson(projectName, options.all),
123
- ]);
124
- const trackedNames = new Set(localSessions.map(s => s.name));
125
- for (const s of localSessions) {
126
- if (s.vmName)
127
- trackedNames.add(s.vmName);
107
+ /**
108
+ * Render a cloud genbox with detailed billing/status info
109
+ */
110
+ function renderCloudGenbox(genbox, nameWidth, showProject) {
111
+ const statusColor = genbox.status === 'running' ? chalk_1.default.green :
112
+ genbox.status === 'terminated' ? chalk_1.default.red :
113
+ genbox.status === 'provisioning' ? chalk_1.default.cyan : chalk_1.default.yellow;
114
+ // Show project info when listing all
115
+ const projectSuffix = showProject && genbox.project ? ` [${genbox.project}]` : '';
116
+ const nameWithProject = genbox.name + projectSuffix;
117
+ // Format end hour as relative time (destroy happens at 58 min mark, not 60)
118
+ let endHourInfo = '';
119
+ if (genbox.currentHourEnd && genbox.status === 'running') {
120
+ const endTime = new Date(genbox.currentHourEnd);
121
+ const now = new Date();
122
+ // Subtract 2 minutes since auto-destroy happens at 58 min mark
123
+ const destroyTime = endTime.getTime() - (2 * 60 * 1000);
124
+ const diffMs = destroyTime - now.getTime();
125
+ if (diffMs > 0) {
126
+ const totalSeconds = Math.floor(diffMs / 1000);
127
+ const mins = Math.floor(totalSeconds / 60);
128
+ const secs = totalSeconds % 60;
129
+ const timeStr = mins > 0
130
+ ? `${mins}m:${secs.toString().padStart(2, '0')}s`
131
+ : `${secs}s`;
132
+ // Red color if less than 5 minutes
133
+ if (mins < 5) {
134
+ endHourInfo = chalk_1.default.red(` hour ends in ${timeStr}`);
135
+ }
136
+ else {
137
+ endHourInfo = chalk_1.default.dim(` hour ends in ${timeStr}`);
128
138
  }
129
- const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
130
- const cloudOutput = cloudGenboxes.map(g => ({
131
- name: g.name,
132
- type: 'cloud',
133
- status: g.status,
134
- ipAddress: g.ipAddress,
135
- size: g.size,
136
- project: g.project,
137
- createdAt: g.createdAt,
138
- autoDestroyOnInactivity: g.autoDestroyOnInactivity,
139
- protectedUntil: g.protectedUntil,
140
- }));
141
- const localOutput = localSessions.map(s => ({
142
- name: s.name,
143
- type: 'local',
144
- status: s.apps.some((a) => a.status === 'running') ? 'running' : 'stopped',
145
- isolation: s.isolation,
146
- size: s.size,
147
- project: s.projectName,
148
- createdAt: s.createdAt,
149
- }));
150
- const orphanedOutput = orphanedGenboxes.map(o => ({
151
- name: o.name,
152
- type: 'orphaned',
153
- status: o.state === 'Running' ? 'running' : 'stopped',
154
- isolation: o.type,
155
- ipAddress: o.ipAddress,
156
- cpus: o.cpus,
157
- memoryGB: o.memoryGB,
158
- diskGB: o.diskGB,
159
- }));
160
- console.log(JSON.stringify([...cloudOutput, ...localOutput, ...orphanedOutput], null, 2));
161
- return;
162
139
  }
163
- // Use unified display format
164
- const result = await (0, unified_session_1.listAllSessions)({
165
- projectPath: options.all ? undefined : projectPath,
166
- includeEnded: options.terminated,
167
- });
168
- // Check for orphaned VMs/containers
169
- const trackedNames = new Set(result.sessions.map(s => s.name));
170
- const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
171
- // Display unified format
172
- (0, unified_session_1.displaySessions)(result, { terminology: 'genboxes' });
173
- // Show ORPHANED genboxes section if any
174
- if (orphanedGenboxes.length > 0) {
175
- console.log(chalk_1.default.yellow.bold(' Orphaned VMs/Containers:'));
176
- console.log(chalk_1.default.dim(' ' + ''.repeat(70)));
177
- const orphanedNameWidth = Math.max(12, ...orphanedGenboxes.map(o => o.name.length));
178
- for (const orphan of orphanedGenboxes) {
179
- renderOrphanedGenbox(orphan, orphanedNameWidth);
140
+ else {
141
+ endHourInfo = chalk_1.default.red(' hour ending...');
142
+ }
143
+ }
144
+ // Show auto-destroy status (not applicable for stopped/terminated)
145
+ let protectedInfo = '';
146
+ if (genbox.status === 'stopped') {
147
+ // Show snapshot status if available
148
+ if (genbox.snapshot) {
149
+ if (genbox.snapshot.status === 'creating') {
150
+ const progress = genbox.snapshot.progress || 0;
151
+ protectedInfo = chalk_1.default.yellow(` (snapshot: ${progress}%)`);
152
+ }
153
+ else if (genbox.snapshot.status === 'ready') {
154
+ protectedInfo = chalk_1.default.dim(' -> gb start to resume (instant)');
155
+ }
156
+ else {
157
+ protectedInfo = chalk_1.default.dim(' -> gb start to resume');
180
158
  }
181
- console.log('');
182
- console.log(chalk_1.default.yellow(` ${orphanedGenboxes.length} orphaned VM/container(s) found.`));
183
- console.log(chalk_1.default.dim(` These exist in ${chalk_1.default.cyan('Multipass/Docker')} but aren't tracked by genbox.`));
184
- console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb delete <name>')} to clean them up.`));
185
- console.log('');
186
159
  }
187
- // Hint for --all
188
- if (projectName && !options.all) {
189
- console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('--all')} to see genboxes from all projects.`));
190
- console.log('');
160
+ else {
161
+ protectedInfo = chalk_1.default.dim(' -> gb start to resume');
191
162
  }
192
- // Force exit since fetch may leave connections open
193
- process.exit(0);
194
163
  }
195
- catch (error) {
196
- if (error instanceof api_1.AuthenticationError) {
197
- (0, api_1.handleApiError)(error);
198
- process.exit(1);
164
+ else if (genbox.status === 'provisioning') {
165
+ // Show restore progress if available
166
+ if (genbox.restoreProgress) {
167
+ const progress = genbox.restoreProgress.progress || 0;
168
+ const source = genbox.restoreProgress.bootSource || 'snapshot';
169
+ if (progress < 100) {
170
+ protectedInfo = chalk_1.default.cyan(` (restoring from ${source}: ${progress}%)`);
171
+ }
172
+ else {
173
+ protectedInfo = chalk_1.default.cyan(' (booting...)');
174
+ }
175
+ }
176
+ else {
177
+ protectedInfo = chalk_1.default.cyan(' (provisioning...)');
199
178
  }
200
- console.error(chalk_1.default.red(`Error: ${error.message}`));
201
- process.exit(1);
202
179
  }
203
- });
180
+ else if (genbox.status === 'terminated') {
181
+ // No extra info for terminated
182
+ }
183
+ else if (genbox.autoDestroyOnInactivity === false) {
184
+ protectedInfo = chalk_1.default.yellow(' [protected]');
185
+ }
186
+ else if (genbox.protectedUntil) {
187
+ const protectedUntil = new Date(genbox.protectedUntil);
188
+ const now = new Date();
189
+ const diffMs = protectedUntil.getTime() - now.getTime();
190
+ const hoursRemaining = Math.ceil(diffMs / (1000 * 60 * 60));
191
+ if (hoursRemaining > 0) {
192
+ protectedInfo = chalk_1.default.cyan(` [extended ${hoursRemaining}h]`);
193
+ }
194
+ else {
195
+ protectedInfo = chalk_1.default.dim(' [auto-destroy]');
196
+ }
197
+ }
198
+ else {
199
+ // Check if auto-destroy is paused due to recent activity
200
+ const now = new Date();
201
+ const minutesInactive = genbox.lastActivityAt
202
+ ? Math.floor((now.getTime() - new Date(genbox.lastActivityAt).getTime()) / (60 * 1000))
203
+ : 999; // Assume inactive if no activity data
204
+ if (minutesInactive < 5) {
205
+ protectedInfo = chalk_1.default.green(' [auto-destroy paused]');
206
+ }
207
+ else {
208
+ protectedInfo = chalk_1.default.dim(' [auto-destroy]');
209
+ }
210
+ }
211
+ const namePart = chalk_1.default.cyan(nameWithProject.padEnd(nameWidth));
212
+ const statusPart = statusColor(genbox.status.padEnd(14));
213
+ // Show appropriate IP text based on status
214
+ let ipText = genbox.ipAddress || 'Pending IP';
215
+ if (!genbox.ipAddress) {
216
+ if (genbox.status === 'stopped') {
217
+ ipText = '(paused)';
218
+ }
219
+ else if (genbox.status === 'terminated') {
220
+ ipText = '-';
221
+ }
222
+ }
223
+ const ipPart = chalk_1.default.dim(ipText.padEnd(16));
224
+ const sizePart = chalk_1.default.dim(`(${genbox.size})`);
225
+ const extraInfo = endHourInfo + protectedInfo;
226
+ console.log(` ${namePart} ${statusPart} ${ipPart} ${sizePart}${extraInfo}`);
227
+ // Show URL if available
228
+ if (genbox.urls && Object.keys(genbox.urls).length > 0) {
229
+ const firstUrl = Object.values(genbox.urls)[0];
230
+ console.log(chalk_1.default.dim(` ${''.padEnd(nameWidth)} └─ ${firstUrl}`));
231
+ }
232
+ }
204
233
  /**
205
- * Get local genboxes for JSON output (preserves legacy format)
234
+ * Render a local genbox
206
235
  */
207
- async function getLocalGenboxesForJson(projectName, showAll) {
236
+ function renderLocalGenbox(session, nameWidth) {
237
+ const isRunning = session.apps.some(a => a.status === 'running');
238
+ const status = isRunning ? 'running' : 'stopped';
239
+ const statusColor = isRunning ? chalk_1.default.green : chalk_1.default.red;
240
+ const namePart = chalk_1.default.magenta(session.name.padEnd(nameWidth));
241
+ const statusPart = statusColor(status.padEnd(14));
242
+ const typePart = chalk_1.default.dim(`(${session.isolation})`.padEnd(16));
243
+ const sizePart = chalk_1.default.dim(`(${session.size})`);
244
+ console.log(` ${namePart} ${statusPart} ${typePart} ${sizePart}`);
245
+ }
246
+ /**
247
+ * Render an orphaned VM/container in the list
248
+ */
249
+ function renderOrphanedGenbox(orphan, nameWidth) {
250
+ const status = orphan.state === 'Running' || orphan.state === 'running' ? 'running' : 'stopped';
251
+ const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.red;
252
+ const namePart = chalk_1.default.yellow(orphan.name.padEnd(nameWidth));
253
+ const statusPart = statusColor(status.padEnd(14));
254
+ const typePart = chalk_1.default.dim(`orphaned/${orphan.type}`.padEnd(16));
255
+ // Show resource info if available
256
+ let resourcePart = '';
257
+ if (orphan.cpus || orphan.memoryGB) {
258
+ const parts = [];
259
+ if (orphan.cpus)
260
+ parts.push(`${orphan.cpus} CPU`);
261
+ if (orphan.memoryGB)
262
+ parts.push(`${orphan.memoryGB}GB`);
263
+ if (orphan.diskGB)
264
+ parts.push(`${orphan.diskGB}GB disk`);
265
+ resourcePart = chalk_1.default.dim(` (${parts.join(', ')})`);
266
+ }
267
+ console.log(` ${namePart} ${statusPart} ${typePart}${resourcePart}`);
268
+ }
269
+ /**
270
+ * Get local genboxes
271
+ */
272
+ function getLocalGenboxes(projectName, showAll) {
208
273
  const sessions = [];
209
274
  const seenNames = new Set();
210
275
  // Get sessions from LocalGenboxProvisioner (for legacy local genboxes)
@@ -288,26 +353,156 @@ async function getLocalGenboxesForJson(projectName, showAll) {
288
353
  }
289
354
  return sessions;
290
355
  }
291
- /**
292
- * Render an orphaned VM/container in the list
293
- */
294
- function renderOrphanedGenbox(orphan, nameWidth) {
295
- const status = orphan.state === 'Running' || orphan.state === 'running' ? 'running' : 'stopped';
296
- const statusColor = status === 'running' ? chalk_1.default.green : chalk_1.default.red;
297
- const namePart = chalk_1.default.yellow(orphan.name.padEnd(nameWidth));
298
- const statusPart = statusColor(status.padEnd(12));
299
- const typePart = chalk_1.default.dim(`orphaned/${orphan.type}`.padEnd(18));
300
- // Show resource info if available
301
- let resourcePart = '';
302
- if (orphan.cpus || orphan.memoryGB) {
356
+ exports.listCommand = new commander_1.Command('list')
357
+ .alias('ls')
358
+ .description('List genboxes (scoped to current project by default)')
359
+ .option('-a, --all', 'Show all genboxes across all projects')
360
+ .option('--terminated', 'Include terminated genboxes')
361
+ .option('--json', 'Output as JSON (for scripting)')
362
+ .addHelpText('after', '\nAliases: gb ls')
363
+ .action(async (options) => {
364
+ try {
365
+ const projectName = (0, genbox_selector_1.getProjectContext)();
366
+ // Fetch cloud and local genboxes in parallel
367
+ const [cloudGenboxes, localSessions] = await Promise.all([
368
+ (0, genbox_selector_1.getGenboxes)({ all: options.all, includeTerminated: options.terminated }).catch(() => []),
369
+ Promise.resolve(getLocalGenboxes(projectName, options.all)),
370
+ ]);
371
+ // Track names for orphan detection
372
+ const trackedNames = new Set();
373
+ for (const g of cloudGenboxes)
374
+ trackedNames.add(g.name);
375
+ for (const s of localSessions) {
376
+ trackedNames.add(s.name);
377
+ if (s.vmName)
378
+ trackedNames.add(s.vmName);
379
+ }
380
+ const orphanedGenboxes = detectOrphanedGenboxes(trackedNames);
381
+ // JSON output mode
382
+ if (options.json) {
383
+ const cloudOutput = cloudGenboxes.map(g => ({
384
+ name: g.name,
385
+ type: 'cloud',
386
+ status: g.status,
387
+ ipAddress: g.ipAddress,
388
+ size: g.size,
389
+ project: g.project,
390
+ urls: g.urls,
391
+ createdAt: g.createdAt,
392
+ currentHourEnd: g.currentHourEnd,
393
+ autoDestroyOnInactivity: g.autoDestroyOnInactivity,
394
+ protectedUntil: g.protectedUntil,
395
+ }));
396
+ const localOutput = localSessions.map(s => ({
397
+ name: s.name,
398
+ type: 'local',
399
+ status: s.apps.some((a) => a.status === 'running') ? 'running' : 'stopped',
400
+ isolation: s.isolation,
401
+ size: s.size,
402
+ project: s.projectName,
403
+ createdAt: s.createdAt,
404
+ }));
405
+ const orphanedOutput = orphanedGenboxes.map(o => ({
406
+ name: o.name,
407
+ type: 'orphaned',
408
+ status: o.state === 'Running' ? 'running' : 'stopped',
409
+ isolation: o.type,
410
+ ipAddress: o.ipAddress,
411
+ cpus: o.cpus,
412
+ memoryGB: o.memoryGB,
413
+ diskGB: o.diskGB,
414
+ }));
415
+ console.log(JSON.stringify([...cloudOutput, ...localOutput, ...orphanedOutput], null, 2));
416
+ process.exit(0);
417
+ }
418
+ // Calculate name width
419
+ const allNames = [
420
+ ...cloudGenboxes.map(g => g.name + (options.all && g.project ? ` [${g.project}]` : '')),
421
+ ...localSessions.map(s => s.name),
422
+ ...orphanedGenboxes.map(o => o.name),
423
+ ];
424
+ const nameWidth = Math.max(18, ...allNames.map(n => n.length));
425
+ // No genboxes found
426
+ if (cloudGenboxes.length === 0 && localSessions.length === 0 && orphanedGenboxes.length === 0) {
427
+ console.log('');
428
+ if (projectName && !options.all) {
429
+ console.log(chalk_1.default.yellow(` No genboxes found for project '${projectName}'.`));
430
+ console.log('');
431
+ console.log(chalk_1.default.dim(' Create one with:'));
432
+ console.log(chalk_1.default.cyan(' $ gb new'));
433
+ console.log('');
434
+ console.log(chalk_1.default.dim(` Or see all genboxes with:`));
435
+ console.log(chalk_1.default.cyan(' $ gb ls --all'));
436
+ }
437
+ else {
438
+ console.log(chalk_1.default.yellow(' No genboxes found.'));
439
+ console.log('');
440
+ console.log(chalk_1.default.dim(' Create one with:'));
441
+ console.log(chalk_1.default.cyan(' $ gb new'));
442
+ }
443
+ console.log('');
444
+ process.exit(0);
445
+ }
446
+ console.log('');
447
+ // Sort cloud genboxes by createdAt (newest first)
448
+ cloudGenboxes.sort((a, b) => {
449
+ const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
450
+ const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
451
+ return dateB - dateA;
452
+ });
453
+ // Display CLOUD genboxes
454
+ if (cloudGenboxes.length > 0) {
455
+ console.log(chalk_1.default.bold(' Cloud Genboxes:'));
456
+ console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
457
+ for (const genbox of cloudGenboxes) {
458
+ renderCloudGenbox(genbox, nameWidth, options.all);
459
+ }
460
+ console.log('');
461
+ }
462
+ // Display LOCAL genboxes
463
+ if (localSessions.length > 0) {
464
+ console.log(chalk_1.default.bold(' Local Genboxes:'));
465
+ console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
466
+ for (const session of localSessions) {
467
+ renderLocalGenbox(session, nameWidth);
468
+ }
469
+ console.log('');
470
+ }
471
+ // Display ORPHANED genboxes
472
+ if (orphanedGenboxes.length > 0) {
473
+ console.log(chalk_1.default.yellow.bold(' Orphaned VMs/Containers:'));
474
+ console.log(chalk_1.default.dim(' ' + '─'.repeat(70)));
475
+ for (const orphan of orphanedGenboxes) {
476
+ renderOrphanedGenbox(orphan, nameWidth);
477
+ }
478
+ console.log('');
479
+ console.log(chalk_1.default.yellow(` ${orphanedGenboxes.length} orphaned VM/container(s) found.`));
480
+ console.log(chalk_1.default.dim(` These exist in ${chalk_1.default.cyan('Multipass/Docker')} but aren't tracked by genbox.`));
481
+ console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('gb destroy <name>')} to clean them up.`));
482
+ console.log('');
483
+ }
484
+ // Summary
485
+ const total = cloudGenboxes.length + localSessions.length;
303
486
  const parts = [];
304
- if (orphan.cpus)
305
- parts.push(`${orphan.cpus} CPU`);
306
- if (orphan.memoryGB)
307
- parts.push(`${orphan.memoryGB}GB`);
308
- if (orphan.diskGB)
309
- parts.push(`${orphan.diskGB}GB disk`);
310
- resourcePart = chalk_1.default.dim(` (${parts.join(', ')})`);
487
+ if (cloudGenboxes.length > 0)
488
+ parts.push(`${cloudGenboxes.length} cloud`);
489
+ if (localSessions.length > 0)
490
+ parts.push(`${localSessions.length} local`);
491
+ console.log(chalk_1.default.dim(` ${total} genbox${total !== 1 ? 'es' : ''} (${parts.join(', ')})`));
492
+ // Hint for --all
493
+ if (projectName && !options.all) {
494
+ console.log(chalk_1.default.dim(` Use ${chalk_1.default.cyan('--all')} to see genboxes from all projects.`));
495
+ }
496
+ console.log('');
497
+ // Force exit since fetch may leave connections open
498
+ process.exit(0);
311
499
  }
312
- console.log(`${namePart} ${statusPart} ${typePart}${resourcePart}`);
313
- }
500
+ catch (error) {
501
+ if (error instanceof api_1.AuthenticationError) {
502
+ (0, api_1.handleApiError)(error);
503
+ process.exit(1);
504
+ }
505
+ console.error(chalk_1.default.red(`Error: ${error.message}`));
506
+ process.exit(1);
507
+ }
508
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.223",
3
+ "version": "1.0.224",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {