gbos 1.2.0 → 1.2.6
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/images/logo-2-clean.png +0 -0
- package/package.json +2 -3
- package/src/commands/connect.js +84 -31
- package/src/commands/logo.js +8 -4
- package/src/lib/api.js +5 -0
- package/src/lib/display.js +137 -45
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbos",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "GBOS - Command line interface for GBOS services",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
|
-
"pngjs": "^7.0.0"
|
|
43
|
-
"terminal-image": "^4.2.0"
|
|
42
|
+
"pngjs": "^7.0.0"
|
|
44
43
|
}
|
|
45
44
|
}
|
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
|
@@ -2,14 +2,18 @@ const path = require('path');
|
|
|
2
2
|
const { displayImage, getTerminalWidth } = require('../lib/display');
|
|
3
3
|
|
|
4
4
|
async function logoCommand() {
|
|
5
|
-
const logoPath = path.join(__dirname, '../../images/logo
|
|
5
|
+
const logoPath = path.join(__dirname, '../../images/logo.png');
|
|
6
6
|
const terminalWidth = getTerminalWidth();
|
|
7
|
-
const targetWidth = Math.
|
|
7
|
+
const targetWidth = Math.max(16, Math.floor(terminalWidth * 0.2));
|
|
8
8
|
|
|
9
9
|
await displayImage(logoPath, {
|
|
10
10
|
width: targetWidth,
|
|
11
|
-
fallbackWidth:
|
|
12
|
-
fallbackHeight:
|
|
11
|
+
fallbackWidth: targetWidth,
|
|
12
|
+
fallbackHeight: 7,
|
|
13
|
+
sharp: false,
|
|
14
|
+
crop: true,
|
|
15
|
+
alphaThreshold: 200,
|
|
16
|
+
cropAlphaThreshold: 200,
|
|
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;
|
|
@@ -178,29 +262,27 @@ function imageToPixels(imagePath, targetWidth = 24, targetHeight = 5) {
|
|
|
178
262
|
}
|
|
179
263
|
|
|
180
264
|
async function displayImage(imagePath, options = {}) {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
console.log('');
|
|
200
|
-
fallbackLines.forEach((line) => console.log(line));
|
|
201
|
-
console.log('');
|
|
202
|
-
return false;
|
|
265
|
+
const width = options.fallbackWidth ?? options.width ?? 40;
|
|
266
|
+
const height = options.fallbackHeight ?? options.height ?? 12;
|
|
267
|
+
const sharp = options.sharp ?? false;
|
|
268
|
+
const alphaThreshold = options.alphaThreshold ?? 50;
|
|
269
|
+
const crop = options.crop ?? false;
|
|
270
|
+
const cropAlphaThreshold = options.cropAlphaThreshold;
|
|
271
|
+
const backgroundLuminance = options.backgroundLuminance ?? 240;
|
|
272
|
+
|
|
273
|
+
const lines = imageToPixels(imagePath, width, height, {
|
|
274
|
+
sampleMode: sharp ? 'nearest' : 'coverage',
|
|
275
|
+
alphaThreshold,
|
|
276
|
+
backgroundLuminance,
|
|
277
|
+
crop,
|
|
278
|
+
cropAlphaThreshold: cropAlphaThreshold ?? alphaThreshold,
|
|
279
|
+
});
|
|
280
|
+
if (!lines) {
|
|
281
|
+
throw new Error('Unable to render image.');
|
|
203
282
|
}
|
|
283
|
+
console.log('');
|
|
284
|
+
lines.forEach((line) => console.log(line));
|
|
285
|
+
console.log('');
|
|
204
286
|
}
|
|
205
287
|
|
|
206
288
|
|
|
@@ -213,20 +295,25 @@ const COMPACT_LOGO = [
|
|
|
213
295
|
|
|
214
296
|
// Display logo with connection details (Claude Code style - clean, minimal)
|
|
215
297
|
function displayLogoWithDetails(details = null) {
|
|
216
|
-
const logoPath = path.join(__dirname, '../../images/logo
|
|
298
|
+
const logoPath = path.join(__dirname, '../../images/logo.png');
|
|
217
299
|
const version = require('../../package.json').version;
|
|
218
300
|
|
|
219
|
-
// Render logo at ~
|
|
220
|
-
let logoLines = imageToPixels(logoPath,
|
|
301
|
+
// Render logo at ~16 chars wide, 7 rows tall with smooth edges
|
|
302
|
+
let logoLines = imageToPixels(logoPath, 16, 7, {
|
|
303
|
+
alphaThreshold: 200,
|
|
304
|
+
crop: true,
|
|
305
|
+
cropAlphaThreshold: 200,
|
|
306
|
+
sampleMode: 'coverage',
|
|
307
|
+
});
|
|
221
308
|
if (!logoLines) logoLines = COMPACT_LOGO;
|
|
222
309
|
|
|
223
|
-
const logoWidth =
|
|
310
|
+
const logoWidth = 22; // Account for escape codes
|
|
224
311
|
|
|
225
312
|
// Build right side - Claude Code style (clean lines, no boxes)
|
|
226
313
|
const rightLines = [];
|
|
227
314
|
|
|
228
315
|
if (details) {
|
|
229
|
-
rightLines.push(`${BOLD}${colors.purple5}
|
|
316
|
+
rightLines.push(`${BOLD}${colors.purple5}gbos.io${RESET} ${DIM}v${version}${RESET}`);
|
|
230
317
|
rightLines.push(`${colors.white}${details.accountName || 'N/A'}${RESET} ${DIM}·${RESET} ${colors.purple5}${details.applicationName || 'N/A'}${RESET}`);
|
|
231
318
|
rightLines.push(`${DIM}${details.nodeName || 'N/A'}${RESET}`);
|
|
232
319
|
}
|
|
@@ -252,16 +339,21 @@ function displayLogo() {
|
|
|
252
339
|
}
|
|
253
340
|
|
|
254
341
|
function displayAuthSuccess(data) {
|
|
255
|
-
const logoPath = path.join(__dirname, '../../images/logo
|
|
342
|
+
const logoPath = path.join(__dirname, '../../images/logo.png');
|
|
256
343
|
const version = require('../../package.json').version;
|
|
257
344
|
|
|
258
|
-
let logoLines = imageToPixels(logoPath,
|
|
345
|
+
let logoLines = imageToPixels(logoPath, 16, 7, {
|
|
346
|
+
alphaThreshold: 200,
|
|
347
|
+
crop: true,
|
|
348
|
+
cropAlphaThreshold: 200,
|
|
349
|
+
sampleMode: 'coverage',
|
|
350
|
+
});
|
|
259
351
|
if (!logoLines) logoLines = COMPACT_LOGO;
|
|
260
352
|
|
|
261
|
-
const logoWidth =
|
|
353
|
+
const logoWidth = 22;
|
|
262
354
|
|
|
263
355
|
const rightLines = [];
|
|
264
|
-
rightLines.push(`${BOLD}${colors.purple5}
|
|
356
|
+
rightLines.push(`${BOLD}${colors.purple5}gbos.io${RESET} ${DIM}v${version}${RESET}`);
|
|
265
357
|
rightLines.push(`${colors.purple5}✓${RESET} ${colors.white}Authenticated${RESET}`);
|
|
266
358
|
rightLines.push(`${colors.white}${data.userName || 'N/A'}${RESET} ${DIM}·${RESET} ${colors.purple5}${data.accountName || 'N/A'}${RESET}`);
|
|
267
359
|
rightLines.push('');
|