forgedock 1.0.13 → 1.0.14
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/bin/forgedock.mjs +485 -410
- package/bin/tui.mjs +400 -0
- package/package.json +1 -1
- package/scripts/gen-logo.mjs +391 -0
package/bin/tui.mjs
CHANGED
|
@@ -1168,3 +1168,403 @@ export async function annotatedReviewScreen(
|
|
|
1168
1168
|
).map((fd) => fd.key),
|
|
1169
1169
|
};
|
|
1170
1170
|
}
|
|
1171
|
+
|
|
1172
|
+
// ---------------------------------------------------------------------------
|
|
1173
|
+
// runSteps — live-checklist orchestrator
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Run an ordered list of async steps and render a live animated checklist.
|
|
1178
|
+
*
|
|
1179
|
+
* @param {Array<{label: string, run: (step: StepAPI) => Promise<void>}>} steps
|
|
1180
|
+
* Each entry has a human-readable `label` and an async `run` function that
|
|
1181
|
+
* receives a {@link StepAPI} object.
|
|
1182
|
+
*
|
|
1183
|
+
* @param {object} [opts]
|
|
1184
|
+
* @param {NodeJS.WritableStream} [opts.stream=process.stderr]
|
|
1185
|
+
* Output stream. Defaults to stderr (consistent with spinner / progressBar).
|
|
1186
|
+
* @param {number} [opts.spinnerInterval=80]
|
|
1187
|
+
* Braille frame interval in milliseconds (TTY only).
|
|
1188
|
+
* @param {boolean} [opts._forceNoAnsi]
|
|
1189
|
+
* Internal override for testing: treat output as non-TTY / no-ANSI regardless
|
|
1190
|
+
* of the runtime environment.
|
|
1191
|
+
*
|
|
1192
|
+
* @returns {Promise<RunStepsResult>}
|
|
1193
|
+
*
|
|
1194
|
+
* @typedef {{
|
|
1195
|
+
* progress(current: number, total: number): void,
|
|
1196
|
+
* note(text: string): void,
|
|
1197
|
+
* skip(reason?: string): void,
|
|
1198
|
+
* }} StepAPI
|
|
1199
|
+
*
|
|
1200
|
+
* @typedef {{
|
|
1201
|
+
* ok: true,
|
|
1202
|
+
* elapsed: number,
|
|
1203
|
+
* } | {
|
|
1204
|
+
* ok: false,
|
|
1205
|
+
* failedStep: number,
|
|
1206
|
+
* error: Error,
|
|
1207
|
+
* elapsed: number,
|
|
1208
|
+
* }} RunStepsResult
|
|
1209
|
+
*/
|
|
1210
|
+
export async function runSteps(steps, opts = {}) {
|
|
1211
|
+
const {
|
|
1212
|
+
stream = process.stderr,
|
|
1213
|
+
spinnerInterval = 80,
|
|
1214
|
+
_forceNoAnsi = false,
|
|
1215
|
+
} = opts;
|
|
1216
|
+
|
|
1217
|
+
const t0 = Date.now();
|
|
1218
|
+
const count = steps.length;
|
|
1219
|
+
|
|
1220
|
+
// ── Helper: format elapsed seconds to 1 dp ────────────────────────────────
|
|
1221
|
+
function elapsedStr() {
|
|
1222
|
+
return ((Date.now() - t0) / 1000).toFixed(1) + "s";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ── Non-TTY / no-ANSI path ─────────────────────────────────────────────────
|
|
1226
|
+
const useAnsi = !_forceNoAnsi && USE_ANSI && stream.isTTY;
|
|
1227
|
+
|
|
1228
|
+
if (!useAnsi) {
|
|
1229
|
+
for (let i = 0; i < count; i++) {
|
|
1230
|
+
const step = steps[i];
|
|
1231
|
+
let skipped = false;
|
|
1232
|
+
let skipReason = "";
|
|
1233
|
+
|
|
1234
|
+
const stepApi = {
|
|
1235
|
+
progress(_current, _total) {
|
|
1236
|
+
// No-op in non-TTY mode
|
|
1237
|
+
},
|
|
1238
|
+
note(_text) {
|
|
1239
|
+
// No-op in non-TTY mode
|
|
1240
|
+
},
|
|
1241
|
+
skip(reason = "") {
|
|
1242
|
+
skipped = true;
|
|
1243
|
+
skipReason = reason;
|
|
1244
|
+
},
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
await step.run(stepApi);
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
const elapsed = elapsedStr();
|
|
1251
|
+
stream.write(`✖ ${step.label} — ${err.message}\n`);
|
|
1252
|
+
stream.write(`✖ Failed in ${elapsed}\n`);
|
|
1253
|
+
return { ok: false, failedStep: i, error: err, elapsed: Date.now() - t0 };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (skipped) {
|
|
1257
|
+
const suffix = skipReason ? ` — ${skipReason}` : "";
|
|
1258
|
+
stream.write(`— ${step.label}${suffix}\n`);
|
|
1259
|
+
} else {
|
|
1260
|
+
stream.write(`✔ ${step.label}\n`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const elapsed = elapsedStr();
|
|
1265
|
+
stream.write(`✔ Done in ${elapsed}\n`);
|
|
1266
|
+
return { ok: true, elapsed: Date.now() - t0 };
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// ── TTY / ANSI path ────────────────────────────────────────────────────────
|
|
1270
|
+
|
|
1271
|
+
// Track per-step state
|
|
1272
|
+
const statuses = steps.map(() => "pending"); // 'pending'|'active'|'done'|'failed'|'skipped'
|
|
1273
|
+
const annotations = steps.map(() => ""); // inline note or progress bar string
|
|
1274
|
+
|
|
1275
|
+
// Render a single step row (no newline — caller moves cursor)
|
|
1276
|
+
function renderRow(i) {
|
|
1277
|
+
const status = statuses[i];
|
|
1278
|
+
const icon =
|
|
1279
|
+
status === "pending" ? dim("○") :
|
|
1280
|
+
status === "done" ? green("✔") :
|
|
1281
|
+
status === "failed" ? red("✖") :
|
|
1282
|
+
status === "skipped" ? dim("—") :
|
|
1283
|
+
/* active spinner handled separately */ dim("○");
|
|
1284
|
+
const label =
|
|
1285
|
+
status === "done" || status === "skipped" ? dim(steps[i].label) :
|
|
1286
|
+
status === "failed" ? red(steps[i].label) :
|
|
1287
|
+
steps[i].label;
|
|
1288
|
+
const ann = annotations[i] ? ` ${dim(annotations[i])}` : "";
|
|
1289
|
+
return `${icon} ${label}${ann}`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Print all rows initially (pending)
|
|
1293
|
+
for (let i = 0; i < count; i++) {
|
|
1294
|
+
stream.write(renderRow(i) + "\n");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Hide cursor
|
|
1298
|
+
stream.write("\x1b[?25l");
|
|
1299
|
+
|
|
1300
|
+
let cursorAtBottom = true; // after the last row
|
|
1301
|
+
|
|
1302
|
+
// Move cursor up N rows (relative)
|
|
1303
|
+
function cursorUp(n) {
|
|
1304
|
+
if (n > 0) stream.write(`\x1b[${n}A`);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Move cursor back to the bottom (below all rows)
|
|
1308
|
+
function cursorToBottom() {
|
|
1309
|
+
if (!cursorAtBottom) {
|
|
1310
|
+
// We're somewhere in the middle; go to end
|
|
1311
|
+
stream.write(`\x1b[${count}B`);
|
|
1312
|
+
cursorAtBottom = true;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Rewrite a single row in-place (cursor must be on that row already)
|
|
1317
|
+
function overwriteRow(i) {
|
|
1318
|
+
stream.write(`\r\x1b[K${renderRow(i)}`);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
let result;
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
for (let i = 0; i < count; i++) {
|
|
1325
|
+
statuses[i] = "active";
|
|
1326
|
+
annotations[i] = "";
|
|
1327
|
+
|
|
1328
|
+
// Move cursor up from bottom to row i, overwrite, return to bottom
|
|
1329
|
+
const rowsFromBottom = count - i; // rows below row i (including row i itself)
|
|
1330
|
+
cursorUp(rowsFromBottom);
|
|
1331
|
+
cursorAtBottom = false;
|
|
1332
|
+
|
|
1333
|
+
let frame = 0;
|
|
1334
|
+
let currentProgressBar = "";
|
|
1335
|
+
let currentNote = "";
|
|
1336
|
+
|
|
1337
|
+
// Draw the active spinner frame
|
|
1338
|
+
function drawActive() {
|
|
1339
|
+
const f = BRAILLE_FRAMES[frame % BRAILLE_FRAMES.length];
|
|
1340
|
+
frame++;
|
|
1341
|
+
const label = bold(steps[i].label);
|
|
1342
|
+
const ann = (currentProgressBar || currentNote)
|
|
1343
|
+
? ` ${dim(currentProgressBar || currentNote)}`
|
|
1344
|
+
: "";
|
|
1345
|
+
stream.write(`\r\x1b[K${cyan(f)} ${label}${ann}`);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
drawActive();
|
|
1349
|
+
|
|
1350
|
+
// Move back to bottom
|
|
1351
|
+
// rowsFromBottom - 1 rows below the current row i
|
|
1352
|
+
const rowsBelow = rowsFromBottom - 1;
|
|
1353
|
+
if (rowsBelow > 0) stream.write(`\x1b[${rowsBelow}B`);
|
|
1354
|
+
cursorAtBottom = true;
|
|
1355
|
+
|
|
1356
|
+
// Spinner interval updates row i in-place
|
|
1357
|
+
const timer = setInterval(() => {
|
|
1358
|
+
const savedCursorAtBottom = cursorAtBottom;
|
|
1359
|
+
cursorUp(rowsFromBottom);
|
|
1360
|
+
drawActive();
|
|
1361
|
+
if (rowsBelow > 0) stream.write(`\x1b[${rowsBelow}B`);
|
|
1362
|
+
cursorAtBottom = savedCursorAtBottom;
|
|
1363
|
+
}, spinnerInterval);
|
|
1364
|
+
timer.unref();
|
|
1365
|
+
|
|
1366
|
+
let skipped = false;
|
|
1367
|
+
let skipReason = "";
|
|
1368
|
+
|
|
1369
|
+
const stepApi = {
|
|
1370
|
+
progress(current, total) {
|
|
1371
|
+
currentProgressBar = progressBar(current, total, { width: 12 });
|
|
1372
|
+
currentNote = "";
|
|
1373
|
+
},
|
|
1374
|
+
note(text) {
|
|
1375
|
+
currentNote = text;
|
|
1376
|
+
currentProgressBar = "";
|
|
1377
|
+
},
|
|
1378
|
+
skip(reason = "") {
|
|
1379
|
+
skipped = true;
|
|
1380
|
+
skipReason = reason;
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
let stepError = null;
|
|
1385
|
+
try {
|
|
1386
|
+
await steps[i].run(stepApi);
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
stepError = err;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
clearInterval(timer);
|
|
1392
|
+
|
|
1393
|
+
// Final state for this step
|
|
1394
|
+
if (stepError) {
|
|
1395
|
+
statuses[i] = "failed";
|
|
1396
|
+
annotations[i] = stepError.message;
|
|
1397
|
+
} else if (skipped) {
|
|
1398
|
+
statuses[i] = "skipped";
|
|
1399
|
+
annotations[i] = skipReason;
|
|
1400
|
+
} else {
|
|
1401
|
+
statuses[i] = "done";
|
|
1402
|
+
annotations[i] = "";
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Redraw final state for row i
|
|
1406
|
+
cursorUp(rowsFromBottom);
|
|
1407
|
+
cursorAtBottom = false;
|
|
1408
|
+
overwriteRow(i);
|
|
1409
|
+
stream.write("\n");
|
|
1410
|
+
// Move down to bottom (rowsBelow rows remaining after the newline)
|
|
1411
|
+
if (rowsBelow > 0) stream.write(`\x1b[${rowsBelow}B`);
|
|
1412
|
+
cursorAtBottom = true;
|
|
1413
|
+
|
|
1414
|
+
if (stepError) {
|
|
1415
|
+
// Failure: stop run
|
|
1416
|
+
result = { ok: false, failedStep: i, error: stepError, elapsed: Date.now() - t0 };
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
// Always restore cursor
|
|
1422
|
+
stream.write("\x1b[?25h");
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const elapsed = elapsedStr();
|
|
1426
|
+
|
|
1427
|
+
if (result) {
|
|
1428
|
+
// Failed
|
|
1429
|
+
stream.write(`${red("✖")} ${dim("Failed in")} ${elapsed}\n`);
|
|
1430
|
+
return result;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
stream.write(`${green("✔")} ${dim("Done in")} ${elapsed}\n`);
|
|
1434
|
+
return { ok: true, elapsed: Date.now() - t0 };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// ---------------------------------------------------------------------------
|
|
1438
|
+
// Logo rendering — ForgeDock F-monogram in half-block ANSI art
|
|
1439
|
+
// ---------------------------------------------------------------------------
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Truecolor detection — distinct from USE_ANSI (which only checks for any ANSI support).
|
|
1443
|
+
* Half-block art requires 24-bit color for the RGB fg/bg per-cell approach.
|
|
1444
|
+
* Falls back to plain text on 256-color or less terminals.
|
|
1445
|
+
*/
|
|
1446
|
+
const USE_TRUECOLOR =
|
|
1447
|
+
USE_ANSI &&
|
|
1448
|
+
(process.env.COLORTERM === "truecolor" ||
|
|
1449
|
+
process.env.COLORTERM === "24bit" ||
|
|
1450
|
+
// Windows Terminal and most modern Linux terminals set COLORTERM=truecolor
|
|
1451
|
+
// iTerm2 sets TERM_PROGRAM=iTerm.app (supports 24-bit)
|
|
1452
|
+
process.env.TERM_PROGRAM === "iTerm.app" ||
|
|
1453
|
+
process.env.TERM_PROGRAM === "vscode");
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Brand-blue ForgeDock F-monogram, pre-rendered as Unicode half-block art.
|
|
1457
|
+
*
|
|
1458
|
+
* Each line uses ▀ (U+2580 UPPER HALF BLOCK) with ANSI 24-bit color:
|
|
1459
|
+
* \x1b[38;2;R;G;Bm — foreground (upper pixel pair)
|
|
1460
|
+
* \x1b[48;2;R;G;Bm — background (lower pixel pair)
|
|
1461
|
+
* \x1b[0m — reset
|
|
1462
|
+
*
|
|
1463
|
+
* Brand color: #58a6ff = RGB(88, 166, 255)
|
|
1464
|
+
* Background: #0d1117 = RGB(13, 17, 23) (GitHub dark canvas — renders as "off" pixels)
|
|
1465
|
+
*
|
|
1466
|
+
* The art is 20 characters wide × 10 rows tall (represents a 20×20 pixel F monogram
|
|
1467
|
+
* at 2:1 aspect, each row pair compressed into one terminal row via ▀).
|
|
1468
|
+
*
|
|
1469
|
+
* To regenerate this art from a source PNG: scripts/gen-logo.mjs <input.png>
|
|
1470
|
+
*/
|
|
1471
|
+
// prettier-ignore
|
|
1472
|
+
const LOGO_ART_TRUECOLOR = (() => {
|
|
1473
|
+
// Brand blue: RGB(88, 166, 255) — #58a6ff
|
|
1474
|
+
// Dark bg: RGB(13, 17, 23) — #0d1117
|
|
1475
|
+
const B = "38;2;88;166;255"; // fg: brand blue
|
|
1476
|
+
const bg = "48;2;88;166;255"; // bg: brand blue
|
|
1477
|
+
const X = "38;2;13;17;23"; // fg: dark (off)
|
|
1478
|
+
const Xb = "48;2;13;17;23"; // bg: dark (off)
|
|
1479
|
+
const R = "\x1b[0m"; // reset
|
|
1480
|
+
|
|
1481
|
+
// half-block helper: upper=fg, lower=bg
|
|
1482
|
+
function px(upper, lower) {
|
|
1483
|
+
return `\x1b[${upper};${lower}m▀${R}`;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// F-monogram pixel map (20 wide × 20 tall, represented as 10 rows of ▀ pairs)
|
|
1487
|
+
// 1 = brand-blue pixel, 0 = dark background
|
|
1488
|
+
//
|
|
1489
|
+
// Row layout (each entry = [upper_row, lower_row] of a ▀ pair):
|
|
1490
|
+
//
|
|
1491
|
+
// ████████████████████ (full top border)
|
|
1492
|
+
// ██ ██ (left bar)
|
|
1493
|
+
// ██ ██
|
|
1494
|
+
// ██████████████ (top crossbar — full width minus right margin)
|
|
1495
|
+
// ██████████████
|
|
1496
|
+
// ██ (left bar only)
|
|
1497
|
+
// ██
|
|
1498
|
+
// ██████ (short lower crossbar)
|
|
1499
|
+
// ██████
|
|
1500
|
+
// ██ (left bar only — bottom)
|
|
1501
|
+
|
|
1502
|
+
// Pixel rows (each row is 20 booleans)
|
|
1503
|
+
const rows = [
|
|
1504
|
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // 0: top border
|
|
1505
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1], // 1: top inner
|
|
1506
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 2: left bar
|
|
1507
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 3: left bar
|
|
1508
|
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0], // 4: top crossbar
|
|
1509
|
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0], // 5: top crossbar (repeat)
|
|
1510
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 6: left bar
|
|
1511
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 7: left bar
|
|
1512
|
+
[1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0], // 8: lower crossbar
|
|
1513
|
+
[1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0], // 9: lower crossbar (repeat)
|
|
1514
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 10: left bar
|
|
1515
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 11: left bar
|
|
1516
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 12: left bar
|
|
1517
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 13: left bar
|
|
1518
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 14: left bar
|
|
1519
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 15: left bar
|
|
1520
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 16: left bar
|
|
1521
|
+
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 17: left bar
|
|
1522
|
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // 18: bottom border
|
|
1523
|
+
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // 19: bottom border (repeat)
|
|
1524
|
+
];
|
|
1525
|
+
|
|
1526
|
+
const lines = [];
|
|
1527
|
+
for (let r = 0; r < rows.length; r += 2) {
|
|
1528
|
+
const top = rows[r];
|
|
1529
|
+
const bot = rows[r + 1];
|
|
1530
|
+
let line = "";
|
|
1531
|
+
for (let c = 0; c < top.length; c++) {
|
|
1532
|
+
const upper = top[c] ? B : X;
|
|
1533
|
+
const lower = bot[c] ? bg : Xb;
|
|
1534
|
+
line += px(upper, lower);
|
|
1535
|
+
}
|
|
1536
|
+
lines.push(line);
|
|
1537
|
+
}
|
|
1538
|
+
return lines;
|
|
1539
|
+
})();
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Render the ForgeDock logo for display in the terminal.
|
|
1543
|
+
*
|
|
1544
|
+
* Returns a multi-line string. On truecolor TTY: F-monogram half-block art
|
|
1545
|
+
* followed by a caption line. On non-TTY / NO_COLOR / 256-color: plain text.
|
|
1546
|
+
*
|
|
1547
|
+
* @param {object} [opts]
|
|
1548
|
+
* @param {string} [opts.version] - Package version string (e.g. "1.0.8")
|
|
1549
|
+
* @returns {string} Rendered logo string (may contain ANSI sequences)
|
|
1550
|
+
*/
|
|
1551
|
+
export function renderLogo({ version = "" } = {}) {
|
|
1552
|
+
const tagline = "GitHub as a knowledge graph for AI agents";
|
|
1553
|
+
const versionStr = version ? `ForgeDock · v${version}` : "ForgeDock";
|
|
1554
|
+
|
|
1555
|
+
if (!USE_TRUECOLOR) {
|
|
1556
|
+
// Plain text fallback — NO_COLOR, non-TTY, or 256-color terminal
|
|
1557
|
+
return `${versionStr}\n${tagline}`;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Truecolor: half-block art + caption
|
|
1561
|
+
const lines = [
|
|
1562
|
+
"",
|
|
1563
|
+
...LOGO_ART_TRUECOLOR.map((l) => " " + l),
|
|
1564
|
+
"",
|
|
1565
|
+
` \x1b[1m\x1b[38;2;88;166;255m${versionStr}\x1b[0m`,
|
|
1566
|
+
` \x1b[2m${tagline}\x1b[0m`,
|
|
1567
|
+
"",
|
|
1568
|
+
];
|
|
1569
|
+
return lines.join("\n");
|
|
1570
|
+
}
|