genbox 1.0.214 → 1.0.217
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/rename.js +201 -0
- package/dist/commands/session/logs.js +117 -3
- package/dist/commands/session/show.js +109 -10
- package/dist/commands/session/watch.js +102 -28
- package/dist/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renameCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const confirm_1 = __importDefault(require("@inquirer/confirm"));
|
|
10
|
+
const select_1 = __importDefault(require("@inquirer/select"));
|
|
11
|
+
const input_1 = __importDefault(require("@inquirer/input"));
|
|
12
|
+
const ora_1 = __importDefault(require("ora"));
|
|
13
|
+
const api_1 = require("../api");
|
|
14
|
+
const genbox_selector_1 = require("../genbox-selector");
|
|
15
|
+
const ssh_config_1 = require("../ssh-config");
|
|
16
|
+
// Name validation regex - must be lowercase alphanumeric with optional hyphens
|
|
17
|
+
const NAME_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
18
|
+
exports.renameCommand = new commander_1.Command('rename')
|
|
19
|
+
.description('Rename a Genbox environment')
|
|
20
|
+
.argument('<name>', 'Current name of the Genbox')
|
|
21
|
+
.argument('<new-name>', 'New name for the Genbox')
|
|
22
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
Examples:
|
|
25
|
+
$ gb rename old-feature new-feature
|
|
26
|
+
$ gb rename my-env my-new-env -y
|
|
27
|
+
$ gb rename feat-1 feat-2
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
- The genbox name must be unique within the workspace
|
|
31
|
+
- Subdomain URLs will automatically update to use the new name
|
|
32
|
+
- SSH config will be updated (old alias removed, new alias added)
|
|
33
|
+
`)
|
|
34
|
+
.action(async (name, newName, options) => {
|
|
35
|
+
try {
|
|
36
|
+
// Normalize and validate new name format
|
|
37
|
+
const normalizedNewName = newName.toLowerCase();
|
|
38
|
+
if (!NAME_REGEX.test(normalizedNewName)) {
|
|
39
|
+
console.log(chalk_1.default.red('Invalid name format'));
|
|
40
|
+
console.log(chalk_1.default.dim(' Name must be lowercase alphanumeric with optional hyphens'));
|
|
41
|
+
console.log(chalk_1.default.dim(' Cannot start or end with a hyphen'));
|
|
42
|
+
console.log(chalk_1.default.dim(' Examples: my-feature, feat-1, api'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (normalizedNewName.length < 2 || normalizedNewName.length > 63) {
|
|
46
|
+
console.log(chalk_1.default.red('Name must be between 2 and 63 characters'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Find the genbox by name
|
|
50
|
+
const { genbox, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
51
|
+
selectMessage: 'Select genbox to rename:',
|
|
52
|
+
});
|
|
53
|
+
if (cancelled || !genbox) {
|
|
54
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Check if same name
|
|
58
|
+
if (genbox.name === normalizedNewName) {
|
|
59
|
+
console.log(chalk_1.default.yellow('New name is the same as current name'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Check if terminated
|
|
63
|
+
if (genbox.status === 'terminated') {
|
|
64
|
+
console.log(chalk_1.default.red('Cannot rename a terminated genbox'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Check name availability BEFORE confirmation
|
|
68
|
+
let finalNewName = normalizedNewName;
|
|
69
|
+
const checkResult = await (0, api_1.fetchApi)(`/genboxes/check-name?name=${encodeURIComponent(normalizedNewName)}&workspace=${encodeURIComponent(genbox.workspace || 'default')}`);
|
|
70
|
+
if (!checkResult.available) {
|
|
71
|
+
console.log(chalk_1.default.red(`Name '${normalizedNewName}' is already in use in workspace '${genbox.workspace || 'default'}'`));
|
|
72
|
+
// Interactive selection of alternatives
|
|
73
|
+
if (checkResult.suggestions && checkResult.suggestions.length > 0) {
|
|
74
|
+
console.log('');
|
|
75
|
+
const choices = [
|
|
76
|
+
...checkResult.suggestions.map((s) => ({
|
|
77
|
+
name: s,
|
|
78
|
+
value: s,
|
|
79
|
+
})),
|
|
80
|
+
{
|
|
81
|
+
name: chalk_1.default.dim('Enter custom name...'),
|
|
82
|
+
value: '__custom__',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: chalk_1.default.dim('Cancel'),
|
|
86
|
+
value: '__cancel__',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const selected = await (0, select_1.default)({
|
|
90
|
+
message: 'Select an alternative name:',
|
|
91
|
+
choices,
|
|
92
|
+
});
|
|
93
|
+
if (selected === '__cancel__') {
|
|
94
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (selected === '__custom__') {
|
|
98
|
+
const customName = await (0, input_1.default)({
|
|
99
|
+
message: 'Enter new name:',
|
|
100
|
+
validate: (value) => {
|
|
101
|
+
const normalized = value.toLowerCase();
|
|
102
|
+
if (!NAME_REGEX.test(normalized)) {
|
|
103
|
+
return 'Name must be lowercase alphanumeric with optional hyphens';
|
|
104
|
+
}
|
|
105
|
+
if (normalized.length < 2 || normalized.length > 63) {
|
|
106
|
+
return 'Name must be between 2 and 63 characters';
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
// Check if custom name is available
|
|
112
|
+
const customCheck = await (0, api_1.fetchApi)(`/genboxes/check-name?name=${encodeURIComponent(customName.toLowerCase())}&workspace=${encodeURIComponent(genbox.workspace || 'default')}`);
|
|
113
|
+
if (!customCheck.available) {
|
|
114
|
+
console.log(chalk_1.default.red(`Name '${customName}' is also taken. Please try again.`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
finalNewName = customName.toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
finalNewName = selected;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// No suggestions available
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Show current info
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
131
|
+
console.log(` ${chalk_1.default.bold('Current name:')} ${genbox.name}`);
|
|
132
|
+
console.log(` ${chalk_1.default.bold('New name:')} ${chalk_1.default.cyan(finalNewName)}`);
|
|
133
|
+
console.log(` ${chalk_1.default.bold('Workspace:')} ${genbox.workspace || 'default'}`);
|
|
134
|
+
console.log(` ${chalk_1.default.bold('Status:')} ${genbox.status}`);
|
|
135
|
+
console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
|
|
136
|
+
console.log('');
|
|
137
|
+
// Confirm rename
|
|
138
|
+
let confirmed = options.yes;
|
|
139
|
+
if (!confirmed) {
|
|
140
|
+
confirmed = await (0, confirm_1.default)({
|
|
141
|
+
message: `Rename genbox from '${genbox.name}' to '${finalNewName}'?`,
|
|
142
|
+
default: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (!confirmed) {
|
|
146
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Perform rename via API
|
|
150
|
+
const spinner = (0, ora_1.default)(`Renaming '${genbox.name}' to '${finalNewName}'...`).start();
|
|
151
|
+
const result = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/rename`, {
|
|
152
|
+
method: 'PATCH',
|
|
153
|
+
body: JSON.stringify({ newName: finalNewName }),
|
|
154
|
+
});
|
|
155
|
+
// Check for error response
|
|
156
|
+
if (result.error) {
|
|
157
|
+
spinner.fail(chalk_1.default.red(result.message || 'Failed to rename genbox'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Update SSH config - remove old, add new
|
|
161
|
+
const oldName = result.oldName || genbox.name;
|
|
162
|
+
(0, ssh_config_1.removeSshConfigEntry)(oldName);
|
|
163
|
+
if (genbox.ipAddress) {
|
|
164
|
+
(0, ssh_config_1.addSshConfigEntry)({
|
|
165
|
+
name: finalNewName,
|
|
166
|
+
ipAddress: genbox.ipAddress,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
spinner.succeed(chalk_1.default.green(`Renamed '${oldName}' to '${finalNewName}'`));
|
|
170
|
+
// Display new URLs
|
|
171
|
+
if (result.urls && Object.keys(result.urls).length > 0) {
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(chalk_1.default.bold('New URLs:'));
|
|
174
|
+
for (const [service, url] of Object.entries(result.urls)) {
|
|
175
|
+
console.log(` ${chalk_1.default.cyan(service.padEnd(12))} ${url}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Display SSH info
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(chalk_1.default.bold('SSH Access:'));
|
|
181
|
+
console.log(` ${chalk_1.default.dim('Old alias:')} ${chalk_1.default.strikethrough((0, ssh_config_1.getSshAlias)(oldName))}`);
|
|
182
|
+
console.log(` ${chalk_1.default.dim('New alias:')} ${chalk_1.default.cyan((0, ssh_config_1.getSshAlias)(finalNewName))}`);
|
|
183
|
+
// Display helpful commands
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(chalk_1.default.bold('Commands:'));
|
|
186
|
+
console.log(` Connect: ${chalk_1.default.cyan(`gb connect ${finalNewName}`)}`);
|
|
187
|
+
console.log(` Status: ${chalk_1.default.cyan(`gb status ${finalNewName}`)}`);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Handle user cancellation (Ctrl+C)
|
|
191
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
192
|
+
console.log(chalk_1.default.dim('\nCancelled.'));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (error instanceof api_1.AuthenticationError) {
|
|
196
|
+
(0, api_1.handleApiError)(error);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
@@ -50,11 +50,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
50
50
|
exports.sessionLogsCommand = void 0;
|
|
51
51
|
const commander_1 = require("commander");
|
|
52
52
|
const chalk_1 = __importDefault(require("chalk"));
|
|
53
|
+
const child_process_1 = require("child_process");
|
|
53
54
|
const path = __importStar(require("path"));
|
|
54
55
|
const os = __importStar(require("os"));
|
|
55
56
|
const fs = __importStar(require("fs"));
|
|
56
57
|
const unified_session_1 = require("../../lib/unified-session");
|
|
57
58
|
const api_1 = require("../../api");
|
|
59
|
+
/**
|
|
60
|
+
* Query genbox daemon directly via SSH to get session data
|
|
61
|
+
*/
|
|
62
|
+
async function queryGenboxDaemon(genboxName, sessionName) {
|
|
63
|
+
try {
|
|
64
|
+
const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
|
|
65
|
+
const data = JSON.parse(result);
|
|
66
|
+
const sessions = data.sessions || [];
|
|
67
|
+
if (sessionName && sessions.length > 0) {
|
|
68
|
+
const provider = sessionName.split('-')[0];
|
|
69
|
+
return sessions.find((s) => s.provider === provider) || sessions[0];
|
|
70
|
+
}
|
|
71
|
+
return sessions[0] || null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Query genbox daemon for events via SSH
|
|
79
|
+
*/
|
|
80
|
+
async function queryGenboxDaemonEvents(genboxName, sessionId, limit = 50) {
|
|
81
|
+
try {
|
|
82
|
+
const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s "http://127.0.0.1:47191/api/storage/events?sessionId=${sessionId}&limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47192/api/storage/events?sessionId=${sessionId}&limit=${limit}" 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
|
|
83
|
+
const data = JSON.parse(result);
|
|
84
|
+
return data.events || [];
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
58
90
|
/**
|
|
59
91
|
* Get SSH key path
|
|
60
92
|
*/
|
|
@@ -356,10 +388,92 @@ Examples:
|
|
|
356
388
|
process.exit(1);
|
|
357
389
|
}
|
|
358
390
|
if (found.remoteSession) {
|
|
359
|
-
|
|
391
|
+
// Query daemon for session data and events
|
|
392
|
+
const daemonSession = await queryGenboxDaemon(found.remoteSession.genboxName, found.remoteSession.name);
|
|
393
|
+
if (!daemonSession) {
|
|
394
|
+
console.log(chalk_1.default.yellow(`\nRemote session: ${found.remoteSession.name}`));
|
|
395
|
+
console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
|
|
396
|
+
console.log(chalk_1.default.dim('\nNo data available from daemon. Session may be starting up.'));
|
|
397
|
+
console.log(chalk_1.default.cyan(`\n gb ${found.remoteSession.name.split('-')[0]} attach ${found.remoteSession.name}\n`));
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
const events = await queryGenboxDaemonEvents(found.remoteSession.genboxName, daemonSession.id, parseInt(options.tail || '50'));
|
|
401
|
+
if (options.json) {
|
|
402
|
+
console.log(JSON.stringify({ session: daemonSession, events }, null, 2));
|
|
403
|
+
process.exit(0);
|
|
404
|
+
}
|
|
405
|
+
if (options.events) {
|
|
406
|
+
// Show events from daemon
|
|
407
|
+
console.log(chalk_1.default.bold(`\nSession Events: ${found.remoteSession.name}`));
|
|
408
|
+
console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
|
|
409
|
+
console.log(chalk_1.default.dim('─'.repeat(70)));
|
|
410
|
+
if (events.length === 0) {
|
|
411
|
+
console.log(chalk_1.default.dim('\nNo events recorded yet.'));
|
|
412
|
+
console.log(chalk_1.default.dim('Events will appear as Claude/Gemini processes prompts.\n'));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
for (const evt of events.reverse()) {
|
|
416
|
+
const icon = getEventIcon(evt.type);
|
|
417
|
+
const time = chalk_1.default.dim(`[${formatTime(evt.createdAt)}]`);
|
|
418
|
+
const eventLabel = (evt.type || '').replace(/_/g, ' ');
|
|
419
|
+
let output = `${time} ${icon} ${eventLabel}`;
|
|
420
|
+
if (evt.data) {
|
|
421
|
+
if (evt.data.toolName) {
|
|
422
|
+
output += chalk_1.default.cyan(` ${evt.data.toolName}`);
|
|
423
|
+
}
|
|
424
|
+
if (evt.data.currentState) {
|
|
425
|
+
output += chalk_1.default.yellow(` → ${evt.data.currentState}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
console.log(output);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
console.log('');
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
// Show session info and recent activity
|
|
435
|
+
console.log(chalk_1.default.bold(`\nSession: ${found.remoteSession.name}`));
|
|
360
436
|
console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
|
|
361
|
-
console.log(chalk_1.default.dim('
|
|
362
|
-
|
|
437
|
+
console.log(chalk_1.default.dim('─'.repeat(70)));
|
|
438
|
+
// Session status
|
|
439
|
+
const statusIcon = daemonSession.status === 'active' ? chalk_1.default.green('●') :
|
|
440
|
+
daemonSession.status === 'idle' ? chalk_1.default.yellow('●') : chalk_1.default.green('●');
|
|
441
|
+
console.log(` Status: ${statusIcon} ${daemonSession.status || 'running'}`);
|
|
442
|
+
if (daemonSession.currentState) {
|
|
443
|
+
const stateLabel = daemonSession.currentState.replace(/_/g, ' ');
|
|
444
|
+
console.log(` State: ${stateLabel}${daemonSession.currentToolName ? ` (${daemonSession.currentToolName})` : ''}`);
|
|
445
|
+
}
|
|
446
|
+
console.log(` Messages: ${daemonSession.messageCount || 0} total (${daemonSession.userPromptCount || 0} user, ${daemonSession.assistantResponseCount || 0} assistant)`);
|
|
447
|
+
console.log(` Tools: ${daemonSession.toolUseCount || 0} calls`);
|
|
448
|
+
if (daemonSession.totalInputTokens || daemonSession.totalOutputTokens) {
|
|
449
|
+
console.log(` Tokens: ${daemonSession.totalInputTokens || 0} in / ${daemonSession.totalOutputTokens || 0} out`);
|
|
450
|
+
}
|
|
451
|
+
if (daemonSession.estimatedCostUsd) {
|
|
452
|
+
console.log(` Cost: $${daemonSession.estimatedCostUsd.toFixed(2)}`);
|
|
453
|
+
}
|
|
454
|
+
// Show recent events
|
|
455
|
+
if (events.length > 0) {
|
|
456
|
+
console.log(chalk_1.default.dim('\n─'.repeat(70)));
|
|
457
|
+
console.log(chalk_1.default.dim('Recent activity:'));
|
|
458
|
+
for (const evt of events.slice(0, 10).reverse()) {
|
|
459
|
+
const icon = getEventIcon(evt.type);
|
|
460
|
+
const time = chalk_1.default.dim(`[${formatTime(evt.createdAt)}]`);
|
|
461
|
+
const eventLabel = (evt.type || '').replace(/_/g, ' ');
|
|
462
|
+
let output = ` ${time} ${icon} ${eventLabel}`;
|
|
463
|
+
if (evt.data?.toolName)
|
|
464
|
+
output += chalk_1.default.cyan(` ${evt.data.toolName}`);
|
|
465
|
+
console.log(output);
|
|
466
|
+
}
|
|
467
|
+
if (events.length > 10) {
|
|
468
|
+
console.log(chalk_1.default.dim(` ... and ${events.length - 10} more events`));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
console.log('');
|
|
472
|
+
console.log(chalk_1.default.dim('Commands:'));
|
|
473
|
+
const provider = found.remoteSession.name.split('-')[0];
|
|
474
|
+
console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session logs ${found.remoteSession.name} --events`) + chalk_1.default.dim(' # Full event log'));
|
|
475
|
+
console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb ${provider} attach ${found.remoteSession.name}`) + chalk_1.default.dim(' # Attach to session'));
|
|
476
|
+
console.log('');
|
|
363
477
|
process.exit(0);
|
|
364
478
|
}
|
|
365
479
|
const session = found.session;
|
|
@@ -16,8 +16,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.sessionShowCommand = void 0;
|
|
17
17
|
const commander_1 = require("commander");
|
|
18
18
|
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
const child_process_1 = require("child_process");
|
|
19
20
|
const unified_session_1 = require("../../lib/unified-session");
|
|
20
21
|
const api_1 = require("../../api");
|
|
22
|
+
/**
|
|
23
|
+
* Query genbox daemon directly via SSH to get session data
|
|
24
|
+
*/
|
|
25
|
+
async function queryGenboxDaemon(genboxName, sessionName) {
|
|
26
|
+
try {
|
|
27
|
+
const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
|
|
28
|
+
const data = JSON.parse(result);
|
|
29
|
+
const sessions = data.sessions || [];
|
|
30
|
+
if (sessionName && sessions.length > 0) {
|
|
31
|
+
// Find matching session by name prefix (e.g., "claude-e2e-test" matches provider "claude")
|
|
32
|
+
const provider = sessionName.split('-')[0];
|
|
33
|
+
return sessions.find((s) => s.provider === provider) || sessions[0];
|
|
34
|
+
}
|
|
35
|
+
return sessions[0] || null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Query genbox daemon for events
|
|
43
|
+
*/
|
|
44
|
+
async function queryGenboxDaemonEvents(genboxName, limit = 50) {
|
|
45
|
+
try {
|
|
46
|
+
const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s "http://127.0.0.1:47191/api/storage/events?limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47192/api/storage/events?limit=${limit}" 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
|
|
47
|
+
const data = JSON.parse(result);
|
|
48
|
+
return data.events || [];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
21
54
|
/**
|
|
22
55
|
* Find session by name or ID
|
|
23
56
|
*/
|
|
@@ -298,20 +331,86 @@ Examples:
|
|
|
298
331
|
process.exit(1);
|
|
299
332
|
}
|
|
300
333
|
if (found.remoteSession) {
|
|
301
|
-
// Remote session -
|
|
334
|
+
// Remote session - query daemon for live data
|
|
335
|
+
const daemonSession = await queryGenboxDaemon(found.remoteSession.genboxName, found.remoteSession.name);
|
|
302
336
|
if (options.json) {
|
|
303
|
-
console.log(JSON.stringify(
|
|
337
|
+
console.log(JSON.stringify({
|
|
338
|
+
...found.remoteSession,
|
|
339
|
+
daemonData: daemonSession || undefined,
|
|
340
|
+
}, null, 2));
|
|
341
|
+
process.exit(0);
|
|
304
342
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
console.log('');
|
|
344
|
+
// Header section
|
|
345
|
+
const headerContent = [''];
|
|
346
|
+
const statusIcon = daemonSession?.status === 'active' ? chalk_1.default.green('●') :
|
|
347
|
+
daemonSession?.status === 'idle' ? chalk_1.default.yellow('●') : chalk_1.default.green('●');
|
|
348
|
+
headerContent.push(` ${statusIcon} Status: ${chalk_1.default.bold(daemonSession?.status || 'running')}`);
|
|
349
|
+
if (daemonSession?.currentState) {
|
|
350
|
+
const stateLabel = daemonSession.currentState.replace(/_/g, ' ');
|
|
351
|
+
const stateIcon = getStateIcon(daemonSession.currentState);
|
|
352
|
+
headerContent.push(` ${stateIcon} State: ${chalk_1.default.cyan(stateLabel)}${daemonSession.currentToolName ? ` (${daemonSession.currentToolName})` : ''}`);
|
|
353
|
+
}
|
|
354
|
+
headerContent.push(` Provider: ${chalk_1.default.magenta(daemonSession?.provider || found.remoteSession.name.split('-')[0])}`);
|
|
355
|
+
headerContent.push(` Type: cloud`);
|
|
356
|
+
headerContent.push(` Genbox: ${chalk_1.default.cyan(found.remoteSession.genboxName)}`);
|
|
357
|
+
headerContent.push(` IP: ${found.remoteSession.ipAddress}`);
|
|
358
|
+
if (daemonSession?.projectPath) {
|
|
359
|
+
headerContent.push(` Project: ${chalk_1.default.dim(daemonSession.projectPath)}`);
|
|
360
|
+
}
|
|
361
|
+
headerContent.push('');
|
|
362
|
+
renderBox(`Session: ${found.remoteSession.name}`, headerContent);
|
|
363
|
+
// Metrics section if daemon data available
|
|
364
|
+
if (daemonSession) {
|
|
311
365
|
console.log('');
|
|
312
|
-
|
|
313
|
-
|
|
366
|
+
const metricsContent = [''];
|
|
367
|
+
// Duration
|
|
368
|
+
const duration = daemonSession.startedAt
|
|
369
|
+
? formatDuration(Date.now() - daemonSession.startedAt)
|
|
370
|
+
: '--';
|
|
371
|
+
metricsContent.push(` Duration: ${duration}`);
|
|
372
|
+
// Tokens
|
|
373
|
+
const inputTokens = daemonSession.totalInputTokens || 0;
|
|
374
|
+
const outputTokens = daemonSession.totalOutputTokens || 0;
|
|
375
|
+
const cacheTokens = daemonSession.totalCacheReadTokens || 0;
|
|
376
|
+
const cacheRate = calculateCacheHitRate(cacheTokens, inputTokens);
|
|
377
|
+
metricsContent.push(` Tokens: Input: ${formatTokens(inputTokens)} Output: ${formatTokens(outputTokens)} Cache: ${cacheRate}`);
|
|
378
|
+
// Context usage
|
|
379
|
+
if (daemonSession.contextTokens && daemonSession.maxContextTokens) {
|
|
380
|
+
const contextPercent = ((daemonSession.contextTokens / daemonSession.maxContextTokens) * 100).toFixed(0);
|
|
381
|
+
metricsContent.push(` Context: ${formatTokens(daemonSession.contextTokens)} / ${formatTokens(daemonSession.maxContextTokens)} (${contextPercent}%)`);
|
|
382
|
+
}
|
|
383
|
+
// Cost
|
|
384
|
+
if (daemonSession.estimatedCostUsd !== undefined) {
|
|
385
|
+
metricsContent.push(` Cost: $${daemonSession.estimatedCostUsd.toFixed(2)} USD`);
|
|
386
|
+
}
|
|
387
|
+
// Messages
|
|
388
|
+
metricsContent.push(` Messages: User: ${daemonSession.userPromptCount || 0} Assistant: ${daemonSession.assistantResponseCount || 0}`);
|
|
389
|
+
// Tools
|
|
390
|
+
if (daemonSession.toolUseCount) {
|
|
391
|
+
metricsContent.push(` Tools: ${daemonSession.toolUseCount} total`);
|
|
392
|
+
}
|
|
393
|
+
metricsContent.push('');
|
|
394
|
+
renderBox('Metrics', metricsContent);
|
|
395
|
+
}
|
|
396
|
+
// Last message preview
|
|
397
|
+
if (daemonSession?.lastMessagePreview) {
|
|
398
|
+
console.log('');
|
|
399
|
+
const previewContent = [
|
|
400
|
+
'',
|
|
401
|
+
` ${chalk_1.default.italic(daemonSession.lastMessagePreview.substring(0, 200))}${daemonSession.lastMessagePreview.length > 200 ? '...' : ''}`,
|
|
402
|
+
'',
|
|
403
|
+
];
|
|
404
|
+
renderBox('Last Message', previewContent);
|
|
314
405
|
}
|
|
406
|
+
// Actions hint
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log(chalk_1.default.dim(' Commands:'));
|
|
409
|
+
const provider = found.remoteSession.name.split('-')[0];
|
|
410
|
+
console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb ${provider} attach ${found.remoteSession.name}`) + chalk_1.default.dim(' # Attach to session'));
|
|
411
|
+
console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session logs ${found.remoteSession.name} -f`) + chalk_1.default.dim(' # Follow logs'));
|
|
412
|
+
console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session send ${found.remoteSession.name}`) + chalk_1.default.dim(' # Send prompt'));
|
|
413
|
+
console.log('');
|
|
315
414
|
process.exit(0);
|
|
316
415
|
}
|
|
317
416
|
const session = found.session;
|
|
@@ -98,17 +98,59 @@ async function fetchLiveState(sessionId) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
101
|
+
* Query genbox daemon directly via SSH to get live session data
|
|
102
|
+
* Uses /api/storage/sessions which returns sessions from SQLite with currentState, currentToolName, etc.
|
|
102
103
|
*/
|
|
103
|
-
async function
|
|
104
|
+
async function queryGenboxDaemon(genboxName) {
|
|
104
105
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Query the storage sessions endpoint for richer session data
|
|
107
|
+
const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
|
|
108
|
+
const data = JSON.parse(result);
|
|
109
|
+
return data.sessions || [];
|
|
107
110
|
}
|
|
108
111
|
catch {
|
|
109
112
|
return [];
|
|
110
113
|
}
|
|
111
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Fetch sessions for a specific genbox from API
|
|
117
|
+
* Queries both ClaudeSession (monitoring) and Session (sync) collections
|
|
118
|
+
*/
|
|
119
|
+
async function fetchGenboxSessions(genboxId) {
|
|
120
|
+
const allSessions = [];
|
|
121
|
+
try {
|
|
122
|
+
// Try ClaudeSession (monitoring) first
|
|
123
|
+
const claudeSessions = await (0, api_1.fetchApi)(`/claude/sessions/genbox/${genboxId}`);
|
|
124
|
+
if (Array.isArray(claudeSessions)) {
|
|
125
|
+
allSessions.push(...claudeSessions);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Ignore errors
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
// Query Session (sync) collection filtered by genboxId
|
|
133
|
+
const sessions = await (0, api_1.fetchApi)(`/sessions/v2?genboxId=${encodeURIComponent(genboxId)}&limit=50`);
|
|
134
|
+
if (sessions && Array.isArray(sessions.sessions)) {
|
|
135
|
+
allSessions.push(...sessions.sessions);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Ignore errors - try unfiltered if genboxId filter not supported
|
|
140
|
+
try {
|
|
141
|
+
const sessions = await (0, api_1.fetchApi)(`/sessions/v2?limit=50`);
|
|
142
|
+
if (sessions && Array.isArray(sessions.sessions)) {
|
|
143
|
+
// Fallback: filter locally by genboxId or type=cloud
|
|
144
|
+
const cloudSessions = sessions.sessions.filter((s) => s.genboxId === genboxId || (s.type === 'cloud' && !s.genboxId));
|
|
145
|
+
allSessions.push(...cloudSessions);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Ignore errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return allSessions;
|
|
153
|
+
}
|
|
112
154
|
/**
|
|
113
155
|
* Match a remote session name to an API session
|
|
114
156
|
* Remote session names are like "claude-abc123" (from socket file)
|
|
@@ -198,17 +240,22 @@ async function collectSessionStates(options) {
|
|
|
198
240
|
// Group remote sessions by genbox to batch API calls
|
|
199
241
|
const remoteByGenbox = new Map();
|
|
200
242
|
for (const remote of result.remoteSessions) {
|
|
201
|
-
const key = remote.genboxId;
|
|
243
|
+
const key = remote.genboxName || remote.genboxId; // Use genboxName as primary key
|
|
202
244
|
if (!remoteByGenbox.has(key)) {
|
|
203
245
|
remoteByGenbox.set(key, []);
|
|
204
246
|
}
|
|
205
247
|
remoteByGenbox.get(key).push(remote);
|
|
206
248
|
}
|
|
207
|
-
// Fetch API sessions for each genbox in parallel
|
|
249
|
+
// Fetch API sessions and daemon sessions for each genbox in parallel
|
|
208
250
|
const genboxSessionsMap = new Map();
|
|
251
|
+
const genboxDaemonSessionsMap = new Map();
|
|
209
252
|
await Promise.all(Array.from(remoteByGenbox.keys()).map(async (genboxId) => {
|
|
210
|
-
const apiSessions = await
|
|
253
|
+
const [apiSessions, daemonSessions] = await Promise.all([
|
|
254
|
+
fetchGenboxSessions(genboxId),
|
|
255
|
+
queryGenboxDaemon(genboxId),
|
|
256
|
+
]);
|
|
211
257
|
genboxSessionsMap.set(genboxId, apiSessions);
|
|
258
|
+
genboxDaemonSessionsMap.set(genboxId, daemonSessions);
|
|
212
259
|
}));
|
|
213
260
|
for (const remote of result.remoteSessions) {
|
|
214
261
|
// Filter by genbox if specified
|
|
@@ -220,25 +267,39 @@ async function collectSessionStates(options) {
|
|
|
220
267
|
// Remote sessions are running by definition (we found their socket)
|
|
221
268
|
if (options.status === 'stopped')
|
|
222
269
|
continue;
|
|
223
|
-
// Try to match this remote session to
|
|
224
|
-
const
|
|
270
|
+
// Try to match this remote session to API or daemon sessions
|
|
271
|
+
const genboxKey = remote.genboxName || remote.genboxId;
|
|
272
|
+
const apiSessions = genboxSessionsMap.get(genboxKey) || [];
|
|
273
|
+
const daemonSessions = genboxDaemonSessionsMap.get(genboxKey) || [];
|
|
225
274
|
const matchedApiSession = matchRemoteSessionToApiSession(remote.name, apiSessions);
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
const
|
|
275
|
+
// Find the most recent active daemon session for live state
|
|
276
|
+
// Daemon sessions have currentState and currentToolName which are most accurate
|
|
277
|
+
const activeDaemonSessions = daemonSessions.filter((s) => s.status === 'active' || s.status === 'idle' ||
|
|
278
|
+
s.currentState === 'executing_tool' || s.currentState === 'thinking' || s.currentState === 'waiting_for_input');
|
|
279
|
+
// Sort by lastActivityAt desc to get the most recent
|
|
280
|
+
activeDaemonSessions.sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0));
|
|
281
|
+
const daemonSession = activeDaemonSessions[0] || daemonSessions[0];
|
|
229
282
|
let displayStatus = 'running';
|
|
230
283
|
let displayState;
|
|
231
|
-
|
|
232
|
-
|
|
284
|
+
let currentTool;
|
|
285
|
+
// Prefer daemon data for live state (most accurate)
|
|
286
|
+
if (daemonSession?.currentState) {
|
|
287
|
+
displayState = daemonSession.currentState;
|
|
288
|
+
currentTool = daemonSession.currentToolName;
|
|
289
|
+
displayStatus = daemonSession.status === 'active' ? 'running' :
|
|
290
|
+
daemonSession.status === 'idle' ? 'idle' : 'running';
|
|
291
|
+
}
|
|
292
|
+
else if (matchedApiSession) {
|
|
293
|
+
// Fall back to API data
|
|
294
|
+
const apiStatus = matchedApiSession.status;
|
|
233
295
|
switch (apiStatus) {
|
|
234
296
|
case 'active':
|
|
297
|
+
case 'running':
|
|
235
298
|
displayStatus = 'running';
|
|
236
|
-
displayState = 'thinking';
|
|
299
|
+
displayState = matchedApiSession.currentState || 'thinking';
|
|
300
|
+
currentTool = matchedApiSession.currentToolName;
|
|
237
301
|
break;
|
|
238
302
|
case 'waiting_input':
|
|
239
|
-
displayStatus = 'idle';
|
|
240
|
-
displayState = 'waiting_for_input';
|
|
241
|
-
break;
|
|
242
303
|
case 'idle':
|
|
243
304
|
displayStatus = 'idle';
|
|
244
305
|
displayState = 'waiting_for_input';
|
|
@@ -247,6 +308,7 @@ async function collectSessionStates(options) {
|
|
|
247
308
|
displayStatus = 'error';
|
|
248
309
|
break;
|
|
249
310
|
case 'ended':
|
|
311
|
+
case 'stopped':
|
|
250
312
|
displayStatus = 'stopped';
|
|
251
313
|
break;
|
|
252
314
|
default:
|
|
@@ -254,9 +316,9 @@ async function collectSessionStates(options) {
|
|
|
254
316
|
}
|
|
255
317
|
}
|
|
256
318
|
else {
|
|
257
|
-
// No
|
|
319
|
+
// No data - session is running (we found socket) but state is unknown
|
|
258
320
|
displayStatus = 'running';
|
|
259
|
-
displayState = undefined;
|
|
321
|
+
displayState = undefined;
|
|
260
322
|
}
|
|
261
323
|
states.push({
|
|
262
324
|
id: matchedApiSession?.sessionId?.substring(0, 8) || remote.name.substring(0, 8),
|
|
@@ -266,12 +328,17 @@ async function collectSessionStates(options) {
|
|
|
266
328
|
genbox: remote.genboxName,
|
|
267
329
|
status: displayStatus,
|
|
268
330
|
state: displayState,
|
|
269
|
-
currentTool:
|
|
270
|
-
lastMessage: matchedApiSession?.lastPrompt?.substring(0, 50),
|
|
271
|
-
duration: matchedApiSession?.createdAt ? formatDuration(matchedApiSession.createdAt) :
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
currentTool: currentTool, // From daemon data
|
|
332
|
+
lastMessage: matchedApiSession?.lastPrompt?.substring(0, 50) || daemonSession?.lastMessagePreview?.substring(0, 50),
|
|
333
|
+
duration: matchedApiSession?.createdAt ? formatDuration(matchedApiSession.createdAt) :
|
|
334
|
+
daemonSession?.startedAt ? formatDuration(new Date(daemonSession.startedAt).toISOString()) : '?',
|
|
335
|
+
createdAt: matchedApiSession?.createdAt || (daemonSession?.startedAt ? new Date(daemonSession.startedAt).toISOString() : undefined),
|
|
336
|
+
tokens: daemonSession ? Math.round((daemonSession.totalInputTokens + daemonSession.totalOutputTokens) / 1000) : undefined,
|
|
337
|
+
inputTokens: daemonSession?.totalInputTokens,
|
|
338
|
+
outputTokens: daemonSession?.totalOutputTokens,
|
|
339
|
+
cost: daemonSession?.estimatedCostUsd,
|
|
340
|
+
toolCalls: daemonSession?.toolUseCount || matchedApiSession?.toolUseCount,
|
|
341
|
+
messagesCount: daemonSession?.messageCount || matchedApiSession?.promptCount,
|
|
275
342
|
});
|
|
276
343
|
}
|
|
277
344
|
// Also add cloud genboxes that might have sessions we didn't scan
|
|
@@ -288,8 +355,9 @@ async function collectSessionStates(options) {
|
|
|
288
355
|
continue;
|
|
289
356
|
if (genbox.status === 'running' && options.status === 'stopped')
|
|
290
357
|
continue;
|
|
358
|
+
const genboxId = genbox.id || genbox._id;
|
|
291
359
|
states.push({
|
|
292
|
-
id:
|
|
360
|
+
id: genboxId?.substring(0, 8) || 'unknown',
|
|
293
361
|
name: genbox.name,
|
|
294
362
|
provider: 'claude', // Default, could be any
|
|
295
363
|
type: 'cloud',
|
|
@@ -810,8 +878,14 @@ async function runTuiMode(options) {
|
|
|
810
878
|
process.stdin.setEncoding('utf8');
|
|
811
879
|
process.stdin.on('data', handleInput);
|
|
812
880
|
}
|
|
813
|
-
// Initial render
|
|
881
|
+
// Initial render with debug
|
|
882
|
+
if (process.env.DEBUG) {
|
|
883
|
+
console.log(`[DEBUG] Fetching sessions...`);
|
|
884
|
+
}
|
|
814
885
|
await render();
|
|
886
|
+
if (process.env.DEBUG) {
|
|
887
|
+
console.log(`[DEBUG] Loaded ${allSessionsCache.length} sessions, showing ${cachedSessions.length} after filters`);
|
|
888
|
+
}
|
|
815
889
|
// Setup refresh interval (event-loop friendly)
|
|
816
890
|
const refreshInterval = setInterval(async () => {
|
|
817
891
|
if (running && inputMode === 'normal') {
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ const validate_1 = require("./commands/validate");
|
|
|
61
61
|
const migrate_1 = require("./commands/migrate");
|
|
62
62
|
const ssh_setup_1 = require("./commands/ssh-setup");
|
|
63
63
|
const rebuild_1 = require("./commands/rebuild");
|
|
64
|
+
const rename_1 = require("./commands/rename");
|
|
64
65
|
const extend_1 = require("./commands/extend");
|
|
65
66
|
const cleanup_ssh_1 = require("./commands/cleanup-ssh");
|
|
66
67
|
const restart_1 = require("./commands/restart");
|
|
@@ -152,6 +153,7 @@ const commandCategories = {
|
|
|
152
153
|
{ name: 'start', aliases: ['resume'], description: 'Start a stopped Genbox' },
|
|
153
154
|
{ name: 'stop', aliases: [], description: 'Stop a running Genbox' },
|
|
154
155
|
{ name: 'restart', aliases: [], description: 'Restart services in a Genbox' },
|
|
156
|
+
{ name: 'rename', aliases: [], description: 'Rename a Genbox' },
|
|
155
157
|
{ name: 'extend', aliases: [], description: 'Extend Genbox lifetime' },
|
|
156
158
|
{ name: 'rebuild', aliases: [], description: 'Rebuild a Genbox with new config' },
|
|
157
159
|
],
|
|
@@ -297,6 +299,7 @@ program
|
|
|
297
299
|
.addCommand(migrate_1.deprecationsCommand, { hidden: true })
|
|
298
300
|
.addCommand(ssh_setup_1.sshSetupCommand, { hidden: true })
|
|
299
301
|
.addCommand(rebuild_1.rebuildCommand)
|
|
302
|
+
.addCommand(rename_1.renameCommand)
|
|
300
303
|
.addCommand(extend_1.extendCommand)
|
|
301
304
|
.addCommand(cleanup_ssh_1.cleanupSshCommand, { hidden: true })
|
|
302
305
|
.addCommand(restart_1.restartCommand)
|