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 +1 -1
- package/src/commands/connect.js +84 -31
- package/src/commands/logo.js +7 -3
- package/src/lib/api.js +5 -0
- package/src/lib/display.js +174 -30
package/package.json
CHANGED
package/src/commands/connect.js
CHANGED
|
@@ -151,42 +151,75 @@ async function connectCommand(options) {
|
|
|
151
151
|
|
|
152
152
|
console.log('\nFetching available applications...\n');
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
'No
|
|
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
|
-
|
|
204
|
-
|
|
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) => ({
|
package/src/commands/logo.js
CHANGED
|
@@ -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(
|
|
7
|
+
const targetWidth = Math.min(120, Math.max(40, terminalWidth - 4));
|
|
8
8
|
|
|
9
9
|
await displayImage(logoPath, {
|
|
10
10
|
width: targetWidth,
|
|
11
|
-
fallbackWidth:
|
|
12
|
-
fallbackHeight:
|
|
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';
|
package/src/lib/display.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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 =
|
|
106
|
-
const cellWidth = png.width /
|
|
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 <
|
|
165
|
+
for (let row = 0; row < resolvedHeight; row++) {
|
|
112
166
|
let line = '';
|
|
113
|
-
for (let col = 0; 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
|
|
182
|
-
const fallbackHeight = options.fallbackHeight
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|