gbos 1.2.0 → 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.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "GBOS - Command line interface for GBOS services",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -151,42 +151,75 @@ async function connectCommand(options) {
151
151
 
152
152
  console.log('\nFetching available applications...\n');
153
153
 
154
- // Fetch available nodes
155
- const nodesResponse = await api.listNodes();
156
- const nodes = nodesResponse.data || [];
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
+ }
157
178
 
158
- if (nodes.length === 0) {
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
+ }
192
+
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
+ }
213
+
214
+ if (appOptions.length === 0) {
159
215
  displayMessageBox(
160
- 'No Nodes Available',
161
- '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.',
162
218
  'warning'
163
219
  );
164
220
  process.exit(1);
165
221
  }
166
222
 
167
- // Group nodes by application
168
- const nodesByApp = {};
169
- nodes.forEach((node) => {
170
- const appId = node.application_id || 'unassigned';
171
- if (!nodesByApp[appId]) {
172
- nodesByApp[appId] = {
173
- application: node.application,
174
- nodes: [],
175
- };
176
- }
177
- nodesByApp[appId].nodes.push(node);
178
- });
179
-
180
- const appIds = Object.keys(nodesByApp);
181
-
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
- }));
189
-
190
223
  // Always show application selection (even if only one)
191
224
  const selectedApp = await selectWithArrows(
192
225
  'Select an application:',
@@ -200,8 +233,28 @@ async function connectCommand(options) {
200
233
  }
201
234
 
202
235
  // Get nodes for selected application
203
- const appNodes = nodesByApp[selectedApp.id].nodes;
204
- const selectedApplication = nodesByApp[selectedApp.id].application;
236
+ let appNodes;
237
+ let selectedApplication = selectedApp.application;
238
+
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
+ );
255
+ return;
256
+ }
257
+ }
205
258
 
206
259
  // Build node options
207
260
  const nodeOptions = appNodes.map((node) => ({
@@ -4,12 +4,16 @@ const { displayImage, getTerminalWidth } = require('../lib/display');
4
4
  async function logoCommand() {
5
5
  const logoPath = path.join(__dirname, '../../images/logo-2.png');
6
6
  const terminalWidth = getTerminalWidth();
7
- const targetWidth = Math.min(60, Math.max(30, terminalWidth - 10));
7
+ const targetWidth = Math.min(120, Math.max(40, terminalWidth - 4));
8
8
 
9
9
  await displayImage(logoPath, {
10
10
  width: targetWidth,
11
- fallbackWidth: 40,
12
- fallbackHeight: 12,
11
+ fallbackWidth: targetWidth,
12
+ fallbackHeight: null,
13
+ sharp: true,
14
+ crop: true,
15
+ alphaThreshold: 220,
16
+ cropAlphaThreshold: 220,
13
17
  });
14
18
  }
15
19
 
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,30 +261,90 @@ 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
+
180
287
  async function displayImage(imagePath, options = {}) {
181
- const fallbackWidth = options.fallbackWidth || 40;
182
- const fallbackHeight = options.fallbackHeight || 12;
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;
183
296
  const renderOptions = { ...options };
184
297
  delete renderOptions.fallbackWidth;
185
298
  delete renderOptions.fallbackHeight;
299
+ delete renderOptions.sharp;
300
+ delete renderOptions.alphaThreshold;
301
+ delete renderOptions.crop;
302
+ delete renderOptions.cropAlphaThreshold;
303
+ delete renderOptions.backgroundLuminance;
186
304
 
187
305
  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;
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;
198
315
  }
199
- console.log('');
200
- fallbackLines.forEach((line) => console.log(line));
201
- console.log('');
202
- return false;
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
203
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;
204
348
  }
205
349
 
206
350