gbos 1.1.9 → 1.2.1

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.9",
3
+ "version": "1.2.1",
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;
@@ -117,6 +118,11 @@ program
117
118
  .option('-a, --all', 'Clear all stored data including machine ID')
118
119
  .action(logoutCommand);
119
120
 
121
+ program
122
+ .command('logo')
123
+ .description('Print the GBOS logo image')
124
+ .action(logoCommand);
125
+
120
126
  program
121
127
  .command('help [command]')
122
128
  .description('Display help for a specific command')
@@ -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`);
29
+
30
+ console.log(`\n${PURPLE}${title}${RESET}\n`);
31
+
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;
15
36
 
16
- console.log(`\n${message}\n`);
37
+ if (isSelected) {
38
+ console.log(` ${prefix} ${BOLD}${WHITE}${text}${RESET}`);
39
+ } else {
40
+ console.log(` ${prefix} ${DIM}${text}${RESET}`);
41
+ }
42
+ });
17
43
 
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}`);
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,75 +149,140 @@ 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');
153
+
154
+ // Try to fetch applications directly first
155
+ let appOptions = [];
156
+ let nodesByApp = {};
157
+
158
+ try {
159
+ // Try the applications endpoint first
160
+ const appsResponse = await api.listApplications();
161
+ const applications = appsResponse.data || [];
162
+
163
+ if (applications.length > 0) {
164
+ appOptions = applications.map((app) => ({
165
+ id: app.id,
166
+ name: app.name || `Application ${app.id}`,
167
+ description: app.description,
168
+ nodeCount: app.nodes_count || app.nodesCount || '?',
169
+ application: app,
170
+ }));
171
+ }
172
+ } catch (err) {
173
+ // Applications endpoint not available, fall back to deriving from nodes
174
+ if (process.env.DEBUG) {
175
+ console.log('Note: /cli/applications endpoint not available, falling back to nodes');
176
+ }
177
+ }
178
+
179
+ // If no applications from direct endpoint, derive from nodes
180
+ if (appOptions.length === 0) {
181
+ const nodesResponse = await api.listNodes();
182
+ const nodes = nodesResponse.data || [];
183
+
184
+ if (nodes.length === 0) {
185
+ displayMessageBox(
186
+ 'No Nodes Available',
187
+ 'No development nodes available. Please create a development node in the GBOS web interface.',
188
+ 'warning'
189
+ );
190
+ process.exit(1);
191
+ }
84
192
 
85
- // Fetch available nodes
86
- const nodesResponse = await api.listNodes();
87
- const nodes = nodesResponse.data || [];
193
+ // Group nodes by application
194
+ nodes.forEach((node) => {
195
+ const appId = node.application_id || 'unassigned';
196
+ if (nodesByApp[appId] === undefined) {
197
+ nodesByApp[appId] = {
198
+ application: node.application,
199
+ nodes: [],
200
+ };
201
+ }
202
+ nodesByApp[appId].nodes.push(node);
203
+ });
204
+
205
+ const appIds = Object.keys(nodesByApp);
206
+ appOptions = appIds.map((appId) => ({
207
+ id: appId,
208
+ name: nodesByApp[appId].application?.name || `Application ${appId}`,
209
+ nodeCount: nodesByApp[appId].nodes.length,
210
+ application: nodesByApp[appId].application,
211
+ }));
212
+ }
88
213
 
89
- if (nodes.length === 0) {
214
+ if (appOptions.length === 0) {
90
215
  displayMessageBox(
91
- 'No Nodes Available',
92
- 'No development nodes available. Please create a development node in the GBOS web interface.',
216
+ 'No Applications Available',
217
+ 'No applications found. Please create an application in the GBOS web interface.',
93
218
  'warning'
94
219
  );
95
220
  process.exit(1);
96
221
  }
97
222
 
98
- // Group nodes by application
99
- const nodesByApp = {};
100
- nodes.forEach((node) => {
101
- const appId = node.application_id || 'unassigned';
102
- if (!nodesByApp[appId]) {
103
- nodesByApp[appId] = {
104
- application: node.application,
105
- nodes: [],
106
- };
107
- }
108
- nodesByApp[appId].nodes.push(node);
109
- });
110
-
111
- // If multiple applications, let user select one first
112
- let selectedApp = null;
113
- const appIds = Object.keys(nodesByApp);
223
+ // Always show application selection (even if only one)
224
+ const selectedApp = await selectWithArrows(
225
+ 'Select an application:',
226
+ appOptions,
227
+ (opt) => `${opt.name} ${DIM}(${opt.nodeCount} node${opt.nodeCount > 1 ? 's' : ''})${RESET}`
228
+ );
114
229
 
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
- }));
230
+ if (!selectedApp) {
231
+ console.log('\nConnection cancelled.\n');
232
+ return;
233
+ }
121
234
 
122
- selectedApp = await selectOption('Select an application:', appOptions);
235
+ // Get nodes for selected application
236
+ let appNodes;
237
+ let selectedApplication = selectedApp.application;
123
238
 
124
- if (!selectedApp) {
125
- console.log('Connection cancelled.\n');
239
+ if (nodesByApp[selectedApp.id]) {
240
+ // We already have nodes from the fallback path
241
+ appNodes = nodesByApp[selectedApp.id].nodes;
242
+ selectedApplication = nodesByApp[selectedApp.id].application || selectedApp.application;
243
+ } else {
244
+ // Fetch nodes for the selected application
245
+ console.log(`\nFetching nodes for ${selectedApp.name}...\n`);
246
+ const nodesResponse = await api.listNodes(selectedApp.id);
247
+ appNodes = nodesResponse.data || [];
248
+
249
+ if (appNodes.length === 0) {
250
+ displayMessageBox(
251
+ 'No Nodes Available',
252
+ `No development nodes found for "${selectedApp.name}". Please create a development node in the GBOS web interface.`,
253
+ 'warning'
254
+ );
126
255
  return;
127
256
  }
128
- } else {
129
- selectedApp = { id: appIds[0] };
130
257
  }
131
258
 
132
- // Get nodes for selected application
133
- const appNodes = nodesByApp[selectedApp.id].nodes;
134
- const selectedApplication = nodesByApp[selectedApp.id].application;
135
-
136
- // Let user select a node
259
+ // Build node options
137
260
  const nodeOptions = appNodes.map((node) => ({
138
261
  ...node,
139
- name: node.name,
140
- description: node.node_type || '',
262
+ displayName: node.name,
263
+ nodeType: node.node_type || '',
264
+ isBusy: node.is_connected && node.active_connection,
141
265
  }));
142
266
 
143
- const selectedNode = await selectOption('Select a development node:', nodeOptions);
267
+ // Select a node
268
+ const selectedNode = await selectWithArrows(
269
+ 'Select a development node:',
270
+ nodeOptions,
271
+ (opt) => {
272
+ let text = opt.displayName;
273
+ if (opt.nodeType) text += ` ${DIM}[${opt.nodeType}]${RESET}`;
274
+ if (opt.isBusy) text += ` ${DIM}(busy)${RESET}`;
275
+ return text;
276
+ }
277
+ );
144
278
 
145
279
  if (!selectedNode) {
146
- console.log('Connection cancelled.\n');
280
+ console.log('\nConnection cancelled.\n');
147
281
  return;
148
282
  }
149
283
 
150
284
  // Check if node is busy
151
- if (selectedNode.is_connected && selectedNode.active_connection) {
285
+ if (selectedNode.isBusy) {
152
286
  displayMessageBox(
153
287
  'Node Busy',
154
288
  `Node "${selectedNode.name}" is already connected by another user. Please select a different node.`,
@@ -0,0 +1,20 @@
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(120, Math.max(40, terminalWidth - 4));
8
+
9
+ await displayImage(logoPath, {
10
+ width: targetWidth,
11
+ fallbackWidth: targetWidth,
12
+ fallbackHeight: null,
13
+ sharp: true,
14
+ crop: true,
15
+ alphaThreshold: 220,
16
+ cropAlphaThreshold: 220,
17
+ });
18
+ }
19
+
20
+ module.exports = logoCommand;
package/src/lib/api.js CHANGED
@@ -81,6 +81,11 @@ class GbosApiClient {
81
81
  });
82
82
  }
83
83
 
84
+ // Application endpoints
85
+ async listApplications() {
86
+ return this.request('/cli/applications', { method: 'GET' });
87
+ }
88
+
84
89
  // Node endpoints
85
90
  async listNodes(applicationId = null) {
86
91
  let endpoint = '/cli/nodes';
@@ -57,15 +57,18 @@ function samplePixel(png, x, y) {
57
57
  }
58
58
 
59
59
  // Check if pixel is transparent or white (background)
60
- function isBackground(pixel) {
61
- return pixel.a < 50 || (pixel.r > 240 && pixel.g > 240 && pixel.b > 240);
60
+ function isBackground(pixel, options = {}) {
61
+ const alphaThreshold = options.alphaThreshold ?? 50;
62
+ const backgroundLuminance = options.backgroundLuminance ?? 240;
63
+ return pixel.a < alphaThreshold ||
64
+ (pixel.r > backgroundLuminance && pixel.g > backgroundLuminance && pixel.b > backgroundLuminance);
62
65
  }
63
66
 
64
67
  // Shading characters for smooth edges (from light to full)
65
68
  const SHADE_CHARS = [' ', '░', '▒', '▓', '█'];
66
69
 
67
70
  // Get average alpha/coverage for a region (for anti-aliasing)
68
- function getRegionCoverage(png, startX, startY, width, height) {
71
+ function getRegionCoverage(png, startX, startY, width, height, backgroundOptions = {}) {
69
72
  let totalAlpha = 0;
70
73
  let totalR = 0, totalG = 0, totalB = 0;
71
74
  let samples = 0;
@@ -73,7 +76,7 @@ function getRegionCoverage(png, startX, startY, width, height) {
73
76
  for (let y = startY; y < startY + height; y++) {
74
77
  for (let x = startX; x < startX + width; x++) {
75
78
  const pixel = samplePixel(png, x, y);
76
- if (!isBackground(pixel)) {
79
+ if (!isBackground(pixel, backgroundOptions)) {
77
80
  totalAlpha += pixel.a;
78
81
  totalR += pixel.r;
79
82
  totalG += pixel.g;
@@ -94,30 +97,111 @@ function getRegionCoverage(png, startX, startY, width, height) {
94
97
  };
95
98
  }
96
99
 
97
- // Convert PNG to true-color pixel art with smooth edges
98
- // Uses shading characters for anti-aliasing on edges
99
- function imageToPixels(imagePath, targetWidth = 24, targetHeight = 5) {
100
+ function cropPng(png, alphaThreshold = 1) {
101
+ let minX = png.width;
102
+ let minY = png.height;
103
+ let maxX = -1;
104
+ let maxY = -1;
105
+
106
+ for (let y = 0; y < png.height; y++) {
107
+ for (let x = 0; x < png.width; x++) {
108
+ const idx = (png.width * y + x) << 2;
109
+ if (png.data[idx + 3] >= alphaThreshold) {
110
+ minX = Math.min(minX, x);
111
+ minY = Math.min(minY, y);
112
+ maxX = Math.max(maxX, x);
113
+ maxY = Math.max(maxY, y);
114
+ }
115
+ }
116
+ }
117
+
118
+ if (maxX < minX || maxY < minY) return png;
119
+
120
+ const width = maxX - minX + 1;
121
+ const height = maxY - minY + 1;
122
+ const cropped = new PNG({ width, height });
123
+
124
+ for (let y = 0; y < height; y++) {
125
+ const srcStart = ((minY + y) * png.width + minX) << 2;
126
+ const srcEnd = srcStart + (width << 2);
127
+ const destStart = (width * y) << 2;
128
+ png.data.copy(cropped.data, destStart, srcStart, srcEnd);
129
+ }
130
+
131
+ return cropped;
132
+ }
133
+
134
+ // Convert PNG to true-color pixel art
135
+ // Uses shading characters for anti-aliasing on edges when sampling coverage
136
+ function imageToPixels(imagePath, targetWidth = 24, targetHeight = 5, options = {}) {
137
+ const {
138
+ alphaThreshold = 50,
139
+ backgroundLuminance = 240,
140
+ sampleMode = 'coverage',
141
+ crop = false,
142
+ cropAlphaThreshold = 1,
143
+ } = options;
144
+
100
145
  try {
101
146
  const data = fs.readFileSync(imagePath);
102
- const png = PNG.sync.read(data);
147
+ let png = PNG.sync.read(data);
148
+ if (crop) {
149
+ png = cropPng(png, cropAlphaThreshold);
150
+ }
151
+
152
+ const resolvedWidth = Math.max(1, Math.round(targetWidth));
153
+ const resolvedHeight = targetHeight == null
154
+ ? Math.max(1, Math.round((resolvedWidth * png.height) / (png.width * 2)))
155
+ : Math.max(1, Math.round(targetHeight));
103
156
 
104
157
  // Each row of output = 2 rows of pixels (using half-blocks)
105
- const pixelRows = targetHeight * 2;
106
- const cellWidth = png.width / targetWidth;
158
+ const pixelRows = resolvedHeight * 2;
159
+ const cellWidth = png.width / resolvedWidth;
107
160
  const cellHeight = png.height / pixelRows;
161
+ const backgroundOptions = { alphaThreshold, backgroundLuminance };
108
162
 
109
163
  const lines = [];
110
164
 
111
- for (let row = 0; row < targetHeight; row++) {
165
+ for (let row = 0; row < resolvedHeight; row++) {
112
166
  let line = '';
113
- for (let col = 0; col < targetWidth; col++) {
167
+ for (let col = 0; col < resolvedWidth; col++) {
168
+ if (sampleMode === 'nearest') {
169
+ const topStartY = row * 2 * cellHeight;
170
+ const bottomStartY = (row * 2 + 1) * cellHeight;
171
+ const startX = col * cellWidth;
172
+
173
+ const topPixel = samplePixel(png, startX + cellWidth * 0.5, topStartY + cellHeight * 0.5);
174
+ const bottomPixel = samplePixel(png, startX + cellWidth * 0.5, bottomStartY + cellHeight * 0.5);
175
+ const topBg = isBackground(topPixel, backgroundOptions);
176
+ const bottomBg = isBackground(bottomPixel, backgroundOptions);
177
+
178
+ if (topBg && bottomBg) {
179
+ line += ' ';
180
+ continue;
181
+ }
182
+
183
+ if (!topBg && !bottomBg) {
184
+ line += fg(topPixel.r, topPixel.g, topPixel.b) +
185
+ bg(bottomPixel.r, bottomPixel.g, bottomPixel.b) +
186
+ UPPER_HALF + RESET;
187
+ continue;
188
+ }
189
+
190
+ if (!topBg) {
191
+ line += fg(topPixel.r, topPixel.g, topPixel.b) + UPPER_HALF + RESET;
192
+ } else {
193
+ line += fg(bottomPixel.r, bottomPixel.g, bottomPixel.b) + LOWER_HALF + RESET;
194
+ }
195
+ continue;
196
+ }
197
+
114
198
  // Get coverage for top and bottom halves of this cell
115
199
  const topStartY = row * 2 * cellHeight;
116
200
  const bottomStartY = (row * 2 + 1) * cellHeight;
117
201
  const startX = col * cellWidth;
118
202
 
119
- const topRegion = getRegionCoverage(png, startX, topStartY, cellWidth, cellHeight);
120
- const bottomRegion = getRegionCoverage(png, startX, bottomStartY, cellWidth, cellHeight);
203
+ const topRegion = getRegionCoverage(png, startX, topStartY, cellWidth, cellHeight, backgroundOptions);
204
+ const bottomRegion = getRegionCoverage(png, startX, bottomStartY, cellWidth, cellHeight, backgroundOptions);
121
205
 
122
206
  const topCov = topRegion.coverage;
123
207
  const bottomCov = bottomRegion.coverage;
@@ -177,6 +261,92 @@ function imageToPixels(imagePath, targetWidth = 24, targetHeight = 5) {
177
261
  }
178
262
  }
179
263
 
264
+ function applyAlphaThreshold(png, alphaThreshold) {
265
+ if (!alphaThreshold) return png;
266
+ for (let i = 0; i < png.data.length; i += 4) {
267
+ if (png.data[i + 3] < alphaThreshold) {
268
+ png.data[i + 3] = 0;
269
+ }
270
+ }
271
+ return png;
272
+ }
273
+
274
+ function preparePngForRender(imagePath, options = {}) {
275
+ const { alphaThreshold, crop, cropAlphaThreshold } = options;
276
+ const data = fs.readFileSync(imagePath);
277
+ let png = PNG.sync.read(data);
278
+ if (alphaThreshold) {
279
+ png = applyAlphaThreshold(png, alphaThreshold);
280
+ }
281
+ if (crop) {
282
+ png = cropPng(png, cropAlphaThreshold ?? alphaThreshold ?? 1);
283
+ }
284
+ return PNG.sync.write(png);
285
+ }
286
+
287
+ async function displayImage(imagePath, options = {}) {
288
+ const fallbackWidth = options.fallbackWidth === undefined ? 40 : options.fallbackWidth;
289
+ const fallbackHeight = options.fallbackHeight === undefined ? 12 : options.fallbackHeight;
290
+ const sharp = options.sharp ?? false;
291
+ const alphaThreshold = options.alphaThreshold ?? 50;
292
+ const crop = options.crop ?? false;
293
+ const cropAlphaThreshold = options.cropAlphaThreshold;
294
+ const backgroundLuminance = options.backgroundLuminance ?? 240;
295
+ const useProcessedBuffer = crop || sharp;
296
+ const renderOptions = { ...options };
297
+ delete renderOptions.fallbackWidth;
298
+ delete renderOptions.fallbackHeight;
299
+ delete renderOptions.sharp;
300
+ delete renderOptions.alphaThreshold;
301
+ delete renderOptions.crop;
302
+ delete renderOptions.cropAlphaThreshold;
303
+ delete renderOptions.backgroundLuminance;
304
+
305
+ try {
306
+ let supportsGraphics = false;
307
+ try {
308
+ const supportsTerminalGraphics = await import('supports-terminal-graphics');
309
+ const graphics = supportsTerminalGraphics.default || supportsTerminalGraphics;
310
+ supportsGraphics = Boolean(
311
+ graphics?.stdout?.kitty || graphics?.stdout?.iterm2 || graphics?.stdout?.sixel,
312
+ );
313
+ } catch {
314
+ supportsGraphics = false;
315
+ }
316
+
317
+ if (supportsGraphics) {
318
+ const terminalImage = await import('terminal-image');
319
+ const renderer = terminalImage.default || terminalImage;
320
+ const buffer = useProcessedBuffer
321
+ ? preparePngForRender(imagePath, { alphaThreshold, crop, cropAlphaThreshold })
322
+ : null;
323
+ const output = buffer
324
+ ? await renderer.buffer(buffer, renderOptions)
325
+ : await renderer.file(imagePath, renderOptions);
326
+ process.stdout.write(output);
327
+ if (!output.endsWith('\n')) process.stdout.write('\n');
328
+ return true;
329
+ }
330
+ } catch (error) {
331
+ // Fall through to ANSI fallback
332
+ }
333
+
334
+ const fallbackLines = imageToPixels(imagePath, fallbackWidth, fallbackHeight, {
335
+ sampleMode: sharp ? 'nearest' : 'coverage',
336
+ alphaThreshold,
337
+ backgroundLuminance,
338
+ crop,
339
+ cropAlphaThreshold: cropAlphaThreshold ?? alphaThreshold,
340
+ });
341
+ if (!fallbackLines) {
342
+ throw new Error('Unable to render image.');
343
+ }
344
+ console.log('');
345
+ fallbackLines.forEach((line) => console.log(line));
346
+ console.log('');
347
+ return false;
348
+ }
349
+
180
350
 
181
351
  // Fallback compact logo
182
352
  const COMPACT_LOGO = [
@@ -187,7 +357,7 @@ const COMPACT_LOGO = [
187
357
 
188
358
  // Display logo with connection details (Claude Code style - clean, minimal)
189
359
  function displayLogoWithDetails(details = null) {
190
- const logoPath = path.join(__dirname, '../../images/logo.png');
360
+ const logoPath = path.join(__dirname, '../../images/logo-2.png');
191
361
  const version = require('../../package.json').version;
192
362
 
193
363
  // Render logo at ~20 chars wide, 5 rows tall (smooth edges)
@@ -226,7 +396,7 @@ function displayLogo() {
226
396
  }
227
397
 
228
398
  function displayAuthSuccess(data) {
229
- const logoPath = path.join(__dirname, '../../images/logo.png');
399
+ const logoPath = path.join(__dirname, '../../images/logo-2.png');
230
400
  const version = require('../../package.json').version;
231
401
 
232
402
  let logoLines = imageToPixels(logoPath, 20, 5);
@@ -300,6 +470,7 @@ module.exports = {
300
470
  displayAuthSuccess,
301
471
  displayConnectSuccess,
302
472
  displayMessageBox,
473
+ displayImage,
303
474
  imageToPixels,
304
475
  getTerminalWidth,
305
476
  };