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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbos",
3
- "version": "1.1.8",
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 ID: ${String(session.user_id).padEnd(42)}│`);
84
- console.log(`│ Account ID: ${String(session.account_id).padEnd(42)}│`);
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')
@@ -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
- `User ID: ${session.user_id}, Account ID: ${session.account_id}. Use --force to re-authenticate or "gbos logout" first.`,
41
+ `${userName} · ${accountName}. Use --force to re-authenticate or "gbos logout" first.`,
40
42
  'info'
41
43
  );
42
44
  return;
@@ -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
- // Simple selection prompt
10
- async function selectOption(message, options) {
11
- const rl = readline.createInterface({
12
- input: process.stdin,
13
- output: process.stdout,
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
- console.log(`\n${message}\n`);
30
+ console.log(`\n${PURPLE}${title}${RESET}\n`);
17
31
 
18
- options.forEach((opt, index) => {
19
- const status = opt.status ? ` [${opt.status}]` : '';
20
- const connected = opt.is_connected ? ' (connected by another user)' : '';
21
- console.log(` ${index + 1}. ${opt.name}${status}${connected}`);
22
- if (opt.description) {
23
- console.log(` ${opt.description}`);
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
- console.log('');
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
- return new Promise((resolve) => {
30
- rl.question('Enter number (or q to quit): ', (answer) => {
31
- rl.close();
32
- if (answer.toLowerCase() === 'q') {
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
- const index = parseInt(answer, 10) - 1;
37
- if (index >= 0 && index < options.length) {
38
- resolve(options[index]);
39
- } else {
40
- resolve(null);
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 nodes...\n');
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
- if (appIds.length > 1) {
116
- const appOptions = appIds.map((appId) => ({
117
- id: appId,
118
- name: nodesByApp[appId].application?.name || `Application ${appId}`,
119
- description: `${nodesByApp[appId].nodes.length} node(s) available`,
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
- selectedApp = await selectOption('Select an application:', appOptions);
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
- if (!selectedApp) {
125
- console.log('Connection cancelled.\n');
126
- return;
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
- // Let user select a node
206
+ // Build node options
137
207
  const nodeOptions = appNodes.map((node) => ({
138
208
  ...node,
139
- name: node.name,
140
- description: node.node_type || '',
209
+ displayName: node.name,
210
+ nodeType: node.node_type || '',
211
+ isBusy: node.is_connected && node.active_connection,
141
212
  }));
142
213
 
143
- const selectedNode = await selectOption('Select a development node:', nodeOptions);
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('Connection cancelled.\n');
227
+ console.log('\nConnection cancelled.\n');
147
228
  return;
148
229
  }
149
230
 
150
231
  // Check if node is busy
151
- if (selectedNode.is_connected && selectedNode.active_connection) {
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;
@@ -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
  };