gbos 1.1.8 → 1.2.0
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/package.json +3 -2
- package/src/cli.js +11 -2
- package/src/commands/auth.js +3 -1
- package/src/commands/connect.js +130 -49
- package/src/commands/logo.js +16 -0
- package/src/lib/display.js +29 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "GBOS - Command line interface for GBOS services",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
|
-
"pngjs": "^7.0.0"
|
|
42
|
+
"pngjs": "^7.0.0",
|
|
43
|
+
"terminal-image": "^4.2.0"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const program = new Command();
|
|
|
6
6
|
const authCommand = require('./commands/auth');
|
|
7
7
|
const connectCommand = require('./commands/connect');
|
|
8
8
|
const logoutCommand = require('./commands/logout');
|
|
9
|
+
const logoCommand = require('./commands/logo');
|
|
9
10
|
const config = require('./lib/config');
|
|
10
11
|
|
|
11
12
|
const VERSION = require('../package.json').version;
|
|
@@ -76,12 +77,15 @@ program
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const userName = session.user_name || session.user_email || `User ${session.user_id}`;
|
|
81
|
+
const accountName = session.account_name || `Account ${session.account_id}`;
|
|
82
|
+
|
|
79
83
|
console.log('\n┌─────────────────────────────────────────────────────────────┐');
|
|
80
84
|
console.log('│ GBOS Status │');
|
|
81
85
|
console.log('├─────────────────────────────────────────────────────────────┤');
|
|
82
86
|
console.log(`│ Authenticated: ✓ │`);
|
|
83
|
-
console.log(`│ User
|
|
84
|
-
console.log(`│ Account
|
|
87
|
+
console.log(`│ User: ${userName.substring(0, 42).padEnd(42)}│`);
|
|
88
|
+
console.log(`│ Account: ${accountName.substring(0, 42).padEnd(42)}│`);
|
|
85
89
|
|
|
86
90
|
const connection = session.connection;
|
|
87
91
|
if (connection) {
|
|
@@ -114,6 +118,11 @@ program
|
|
|
114
118
|
.option('-a, --all', 'Clear all stored data including machine ID')
|
|
115
119
|
.action(logoutCommand);
|
|
116
120
|
|
|
121
|
+
program
|
|
122
|
+
.command('logo')
|
|
123
|
+
.description('Print the GBOS logo image')
|
|
124
|
+
.action(logoCommand);
|
|
125
|
+
|
|
117
126
|
program
|
|
118
127
|
.command('help [command]')
|
|
119
128
|
.description('Display help for a specific command')
|
package/src/commands/auth.js
CHANGED
|
@@ -34,9 +34,11 @@ async function authCommand(options) {
|
|
|
34
34
|
// Check if already authenticated
|
|
35
35
|
if (config.isAuthenticated() && !options.force) {
|
|
36
36
|
const session = config.loadSession();
|
|
37
|
+
const userName = session.user_name || session.user_email || `User ${session.user_id}`;
|
|
38
|
+
const accountName = session.account_name || `Account ${session.account_id}`;
|
|
37
39
|
displayMessageBox(
|
|
38
40
|
'Already Authenticated',
|
|
39
|
-
|
|
41
|
+
`${userName} · ${accountName}. Use --force to re-authenticate or "gbos logout" first.`,
|
|
40
42
|
'info'
|
|
41
43
|
);
|
|
42
44
|
return;
|
package/src/commands/connect.js
CHANGED
|
@@ -1,45 +1,114 @@
|
|
|
1
1
|
const api = require('../lib/api');
|
|
2
2
|
const config = require('../lib/config');
|
|
3
3
|
const { checkForUpdates } = require('../lib/version');
|
|
4
|
-
const { displayConnectSuccess, displayMessageBox } = require('../lib/display');
|
|
4
|
+
const { displayConnectSuccess, displayMessageBox, colors } = require('../lib/display');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
const path = require('path');
|
|
7
6
|
const { execSync } = require('child_process');
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const ESC = '\x1b';
|
|
9
|
+
const RESET = `${ESC}[0m`;
|
|
10
|
+
const BOLD = `${ESC}[1m`;
|
|
11
|
+
const DIM = `${ESC}[2m`;
|
|
12
|
+
const PURPLE = `${ESC}[38;5;99m`;
|
|
13
|
+
const WHITE = `${ESC}[37m`;
|
|
14
|
+
const CYAN = `${ESC}[36m`;
|
|
15
|
+
|
|
16
|
+
// Interactive arrow key selector
|
|
17
|
+
async function selectWithArrows(title, options, displayFn) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let selectedIndex = 0;
|
|
20
|
+
const stdin = process.stdin;
|
|
21
|
+
const stdout = process.stdout;
|
|
22
|
+
|
|
23
|
+
// Save cursor position and hide cursor
|
|
24
|
+
stdout.write(`${ESC}[?25l`);
|
|
25
|
+
|
|
26
|
+
function render() {
|
|
27
|
+
// Move cursor to start and clear
|
|
28
|
+
stdout.write(`${ESC}[${options.length + 3}A${ESC}[J`);
|
|
15
29
|
|
|
16
|
-
|
|
30
|
+
console.log(`\n${PURPLE}${title}${RESET}\n`);
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
options.forEach((opt, index) => {
|
|
33
|
+
const isSelected = index === selectedIndex;
|
|
34
|
+
const prefix = isSelected ? `${CYAN}❯${RESET}` : ' ';
|
|
35
|
+
const text = displayFn ? displayFn(opt, isSelected) : opt.name;
|
|
36
|
+
|
|
37
|
+
if (isSelected) {
|
|
38
|
+
console.log(` ${prefix} ${BOLD}${WHITE}${text}${RESET}`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` ${prefix} ${DIM}${text}${RESET}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(`\n ${DIM}↑/↓ to navigate, Enter to select, q to cancel${RESET}`);
|
|
24
45
|
}
|
|
25
|
-
});
|
|
26
46
|
|
|
27
|
-
|
|
47
|
+
// Initial render
|
|
48
|
+
console.log(`\n${PURPLE}${title}${RESET}\n`);
|
|
49
|
+
options.forEach((opt, index) => {
|
|
50
|
+
const isSelected = index === selectedIndex;
|
|
51
|
+
const prefix = isSelected ? `${CYAN}❯${RESET}` : ' ';
|
|
52
|
+
const text = displayFn ? displayFn(opt, isSelected) : opt.name;
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
if (isSelected) {
|
|
55
|
+
console.log(` ${prefix} ${BOLD}${WHITE}${text}${RESET}`);
|
|
56
|
+
} else {
|
|
57
|
+
console.log(` ${prefix} ${DIM}${text}${RESET}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
console.log(`\n ${DIM}↑/↓ to navigate, Enter to select, q to cancel${RESET}`);
|
|
61
|
+
|
|
62
|
+
// Enable raw mode
|
|
63
|
+
if (stdin.isTTY) {
|
|
64
|
+
stdin.setRawMode(true);
|
|
65
|
+
}
|
|
66
|
+
stdin.resume();
|
|
67
|
+
stdin.setEncoding('utf8');
|
|
68
|
+
|
|
69
|
+
function cleanup() {
|
|
70
|
+
if (stdin.isTTY) {
|
|
71
|
+
stdin.setRawMode(false);
|
|
72
|
+
}
|
|
73
|
+
stdin.removeListener('data', onKeypress);
|
|
74
|
+
// Show cursor
|
|
75
|
+
stdout.write(`${ESC}[?25h`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onKeypress(key) {
|
|
79
|
+
// Ctrl+C
|
|
80
|
+
if (key === '\u0003') {
|
|
81
|
+
cleanup();
|
|
82
|
+
process.exit();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// q or Q to quit
|
|
86
|
+
if (key === 'q' || key === 'Q') {
|
|
87
|
+
cleanup();
|
|
33
88
|
resolve(null);
|
|
34
89
|
return;
|
|
35
90
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
resolve(
|
|
91
|
+
|
|
92
|
+
// Enter
|
|
93
|
+
if (key === '\r' || key === '\n') {
|
|
94
|
+
cleanup();
|
|
95
|
+
resolve(options[selectedIndex]);
|
|
96
|
+
return;
|
|
41
97
|
}
|
|
42
|
-
|
|
98
|
+
|
|
99
|
+
// Arrow keys (escape sequences)
|
|
100
|
+
if (key === `${ESC}[A` || key === 'k') {
|
|
101
|
+
// Up
|
|
102
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
|
|
103
|
+
render();
|
|
104
|
+
} else if (key === `${ESC}[B` || key === 'j') {
|
|
105
|
+
// Down
|
|
106
|
+
selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
|
|
107
|
+
render();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stdin.on('data', onKeypress);
|
|
43
112
|
});
|
|
44
113
|
}
|
|
45
114
|
|
|
@@ -80,7 +149,7 @@ async function connectCommand(options) {
|
|
|
80
149
|
return;
|
|
81
150
|
}
|
|
82
151
|
|
|
83
|
-
console.log('\nFetching available
|
|
152
|
+
console.log('\nFetching available applications...\n');
|
|
84
153
|
|
|
85
154
|
// Fetch available nodes
|
|
86
155
|
const nodesResponse = await api.listNodes();
|
|
@@ -108,47 +177,59 @@ async function connectCommand(options) {
|
|
|
108
177
|
nodesByApp[appId].nodes.push(node);
|
|
109
178
|
});
|
|
110
179
|
|
|
111
|
-
// If multiple applications, let user select one first
|
|
112
|
-
let selectedApp = null;
|
|
113
180
|
const appIds = Object.keys(nodesByApp);
|
|
114
181
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
182
|
+
// Build application options
|
|
183
|
+
const appOptions = appIds.map((appId) => ({
|
|
184
|
+
id: appId,
|
|
185
|
+
name: nodesByApp[appId].application?.name || `Application ${appId}`,
|
|
186
|
+
nodeCount: nodesByApp[appId].nodes.length,
|
|
187
|
+
application: nodesByApp[appId].application,
|
|
188
|
+
}));
|
|
121
189
|
|
|
122
|
-
|
|
190
|
+
// Always show application selection (even if only one)
|
|
191
|
+
const selectedApp = await selectWithArrows(
|
|
192
|
+
'Select an application:',
|
|
193
|
+
appOptions,
|
|
194
|
+
(opt) => `${opt.name} ${DIM}(${opt.nodeCount} node${opt.nodeCount > 1 ? 's' : ''})${RESET}`
|
|
195
|
+
);
|
|
123
196
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
selectedApp = { id: appIds[0] };
|
|
197
|
+
if (!selectedApp) {
|
|
198
|
+
console.log('\nConnection cancelled.\n');
|
|
199
|
+
return;
|
|
130
200
|
}
|
|
131
201
|
|
|
132
202
|
// Get nodes for selected application
|
|
133
203
|
const appNodes = nodesByApp[selectedApp.id].nodes;
|
|
134
204
|
const selectedApplication = nodesByApp[selectedApp.id].application;
|
|
135
205
|
|
|
136
|
-
//
|
|
206
|
+
// Build node options
|
|
137
207
|
const nodeOptions = appNodes.map((node) => ({
|
|
138
208
|
...node,
|
|
139
|
-
|
|
140
|
-
|
|
209
|
+
displayName: node.name,
|
|
210
|
+
nodeType: node.node_type || '',
|
|
211
|
+
isBusy: node.is_connected && node.active_connection,
|
|
141
212
|
}));
|
|
142
213
|
|
|
143
|
-
|
|
214
|
+
// Select a node
|
|
215
|
+
const selectedNode = await selectWithArrows(
|
|
216
|
+
'Select a development node:',
|
|
217
|
+
nodeOptions,
|
|
218
|
+
(opt) => {
|
|
219
|
+
let text = opt.displayName;
|
|
220
|
+
if (opt.nodeType) text += ` ${DIM}[${opt.nodeType}]${RESET}`;
|
|
221
|
+
if (opt.isBusy) text += ` ${DIM}(busy)${RESET}`;
|
|
222
|
+
return text;
|
|
223
|
+
}
|
|
224
|
+
);
|
|
144
225
|
|
|
145
226
|
if (!selectedNode) {
|
|
146
|
-
console.log('
|
|
227
|
+
console.log('\nConnection cancelled.\n');
|
|
147
228
|
return;
|
|
148
229
|
}
|
|
149
230
|
|
|
150
231
|
// Check if node is busy
|
|
151
|
-
if (selectedNode.
|
|
232
|
+
if (selectedNode.isBusy) {
|
|
152
233
|
displayMessageBox(
|
|
153
234
|
'Node Busy',
|
|
154
235
|
`Node "${selectedNode.name}" is already connected by another user. Please select a different node.`,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { displayImage, getTerminalWidth } = require('../lib/display');
|
|
3
|
+
|
|
4
|
+
async function logoCommand() {
|
|
5
|
+
const logoPath = path.join(__dirname, '../../images/logo-2.png');
|
|
6
|
+
const terminalWidth = getTerminalWidth();
|
|
7
|
+
const targetWidth = Math.min(60, Math.max(30, terminalWidth - 10));
|
|
8
|
+
|
|
9
|
+
await displayImage(logoPath, {
|
|
10
|
+
width: targetWidth,
|
|
11
|
+
fallbackWidth: 40,
|
|
12
|
+
fallbackHeight: 12,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = logoCommand;
|
package/src/lib/display.js
CHANGED
|
@@ -177,6 +177,32 @@ function imageToPixels(imagePath, targetWidth = 24, targetHeight = 5) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
async function displayImage(imagePath, options = {}) {
|
|
181
|
+
const fallbackWidth = options.fallbackWidth || 40;
|
|
182
|
+
const fallbackHeight = options.fallbackHeight || 12;
|
|
183
|
+
const renderOptions = { ...options };
|
|
184
|
+
delete renderOptions.fallbackWidth;
|
|
185
|
+
delete renderOptions.fallbackHeight;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const terminalImage = await import('terminal-image');
|
|
189
|
+
const renderer = terminalImage.default || terminalImage;
|
|
190
|
+
const output = await renderer.file(imagePath, renderOptions);
|
|
191
|
+
process.stdout.write(output);
|
|
192
|
+
if (!output.endsWith('\n')) process.stdout.write('\n');
|
|
193
|
+
return true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const fallbackLines = imageToPixels(imagePath, fallbackWidth, fallbackHeight);
|
|
196
|
+
if (!fallbackLines) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
console.log('');
|
|
200
|
+
fallbackLines.forEach((line) => console.log(line));
|
|
201
|
+
console.log('');
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
180
206
|
|
|
181
207
|
// Fallback compact logo
|
|
182
208
|
const COMPACT_LOGO = [
|
|
@@ -187,7 +213,7 @@ const COMPACT_LOGO = [
|
|
|
187
213
|
|
|
188
214
|
// Display logo with connection details (Claude Code style - clean, minimal)
|
|
189
215
|
function displayLogoWithDetails(details = null) {
|
|
190
|
-
const logoPath = path.join(__dirname, '../../images/logo.png');
|
|
216
|
+
const logoPath = path.join(__dirname, '../../images/logo-2.png');
|
|
191
217
|
const version = require('../../package.json').version;
|
|
192
218
|
|
|
193
219
|
// Render logo at ~20 chars wide, 5 rows tall (smooth edges)
|
|
@@ -226,7 +252,7 @@ function displayLogo() {
|
|
|
226
252
|
}
|
|
227
253
|
|
|
228
254
|
function displayAuthSuccess(data) {
|
|
229
|
-
const logoPath = path.join(__dirname, '../../images/logo.png');
|
|
255
|
+
const logoPath = path.join(__dirname, '../../images/logo-2.png');
|
|
230
256
|
const version = require('../../package.json').version;
|
|
231
257
|
|
|
232
258
|
let logoLines = imageToPixels(logoPath, 20, 5);
|
|
@@ -300,6 +326,7 @@ module.exports = {
|
|
|
300
326
|
displayAuthSuccess,
|
|
301
327
|
displayConnectSuccess,
|
|
302
328
|
displayMessageBox,
|
|
329
|
+
displayImage,
|
|
303
330
|
imageToPixels,
|
|
304
331
|
getTerminalWidth,
|
|
305
332
|
};
|