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/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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"