forgedock 1.0.15 → 1.0.17

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.
Files changed (71) hide show
  1. package/README.md +82 -60
  2. package/bin/forgedock.mjs +1992 -540
  3. package/bin/github-app-manifest.json.license +2 -0
  4. package/bin/hooks/session-start.mjs +4 -5
  5. package/bin/registry.mjs +20 -0
  6. package/bin/tui.mjs +172 -96
  7. package/commands/adopt.md +433 -0
  8. package/commands/analytics.md +2 -0
  9. package/commands/audit-agents.md +2 -0
  10. package/commands/audit.md +2 -0
  11. package/commands/changelog.md +354 -0
  12. package/commands/cleanup.md +2 -0
  13. package/commands/compat-audit.md +330 -0
  14. package/commands/deploy-info.md +6 -5
  15. package/commands/diagnose.md +483 -0
  16. package/commands/explain.md +349 -0
  17. package/commands/forge-stats.md +2 -0
  18. package/commands/forgedock-init.md +2 -0
  19. package/commands/geo-audit.md +2 -0
  20. package/commands/incident-response.md +2 -0
  21. package/commands/issue.md +2 -0
  22. package/commands/milestone.md +2 -0
  23. package/commands/optimize.md +411 -0
  24. package/commands/orchestrate.md +118 -0
  25. package/commands/pipeline-health.md +60 -1
  26. package/commands/qa-sweep.md +2 -0
  27. package/commands/quality-gate.md +37 -0
  28. package/commands/replay.md +263 -0
  29. package/commands/resume.md +264 -0
  30. package/commands/review-pr-agents.md +5 -1
  31. package/commands/review-pr-staging.md +2 -0
  32. package/commands/review-pr.md +85 -3
  33. package/commands/rollback.md +2 -0
  34. package/commands/scope.md +280 -0
  35. package/commands/security-audit.md +2 -0
  36. package/commands/status.md +262 -0
  37. package/commands/sync-ecosystem.md +2 -0
  38. package/commands/validate.md +2 -0
  39. package/commands/work-on/build/architect.md +128 -18
  40. package/commands/work-on/build/context.md +130 -17
  41. package/commands/work-on/build/implement.md +2 -0
  42. package/commands/work-on/build/validate.md +2 -0
  43. package/commands/work-on/build.md +32 -7
  44. package/commands/work-on/close.md +85 -0
  45. package/commands/work-on/decompose.md +2 -0
  46. package/commands/work-on/investigate.md +30 -3
  47. package/commands/work-on/review.md +13 -1
  48. package/commands/work-on-monolithic.md +2 -0
  49. package/commands/work-on.md +624 -36
  50. package/docs/BRAND.md +51 -0
  51. package/docs/CONFIG.md +94 -0
  52. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
  53. package/docs/claude-breakpoints.json +47 -0
  54. package/docs/site/getting-started.md +21 -0
  55. package/package.json +10 -2
  56. package/scripts/classify-lane.sh +145 -0
  57. package/scripts/transition-label.sh +161 -0
  58. package/scripts/validate-pr-target.sh +30 -0
  59. package/templates/devdocs/README.md +72 -0
  60. package/templates/devdocs/agent/using-forgedock.md +3 -0
  61. package/templates/devdocs/agent/using-github.md +3 -0
  62. package/templates/devdocs/index.yaml +162 -0
  63. package/templates/devdocs/project/architecture.md +3 -0
  64. package/templates/devdocs/project/conventions.md +3 -0
  65. package/templates/devdocs/project/custom-instructions.md +3 -0
  66. package/templates/devdocs/project/glossary.md +3 -0
  67. package/templates/devdocs/project/stack.md +3 -0
  68. package/templates/devdocs/templates/commit-convention.md +3 -0
  69. package/templates/devdocs/templates/issue.md +3 -0
  70. package/templates/devdocs/templates/pull-request.md +3 -0
  71. package/templates/devdocs/templates/review-finding.md +3 -0
@@ -0,0 +1,2 @@
1
+ SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ SPDX-License-Identifier: AGPL-3.0-or-later
@@ -245,7 +245,7 @@ ${nameNote}${repoNote}${descNote}${milestoneNote}
245
245
  | /autopilot | Autonomous platform improvement cycle |
246
246
 
247
247
  Run \`/help\` to see the full command list.
248
- To disable ForgeDock in this directory: \`npx forgedock disable\``;
248
+ To remove ForgeDock commands: \`npx forgedock uninstall\``;
249
249
  }
250
250
 
251
251
  /**
@@ -282,12 +282,11 @@ function buildNudgeContext(dir) {
282
282
  <!-- ForgeDock: unmanaged nudge -->
283
283
  **ForgeDock** is installed but not active in this directory (${safeDir}).
284
284
 
285
- To enable the autonomous development pipeline here:
286
- \`npx forgedock enable\` — mark this directory and stay silent until you run init
287
- \`npx forgedock init\` — immediately generate forge.yaml with AI autopilot
285
+ To activate the autonomous development pipeline here:
286
+ \`npx forgedock init\` — generate forge.yaml and activate ForgeDock for this project
288
287
 
289
288
  This message will not appear again in this directory.
290
- To suppress it globally, run: \`npx forgedock disable\``;
289
+ To remove ForgeDock commands globally, run: \`npx forgedock uninstall\``;
291
290
  }
292
291
 
293
292
  // ---------------------------------------------------------------------------
package/bin/registry.mjs CHANGED
@@ -356,3 +356,23 @@ export async function markNudgeSeen(dir) {
356
356
  registry.nudgeSeen[absDir] = { at: new Date().toISOString() };
357
357
  });
358
358
  }
359
+
360
+ /**
361
+ * Remove the nudgeSeen entry for a directory from the registry.
362
+ *
363
+ * Called by `npx forgedock uninstall` to clean up per-directory state
364
+ * written by the session-start hook. After uninstall, the nudge will fire
365
+ * once more if ForgeDock is later reinstalled in the same directory —
366
+ * consistent with a fresh install experience.
367
+ *
368
+ * Best-effort: a failed write is non-fatal and leaves the registry as-is.
369
+ *
370
+ * @param {string} dir - Absolute path to the directory.
371
+ * @returns {Promise<void>}
372
+ */
373
+ export async function clearNudgeSeen(dir) {
374
+ const absDir = normalizeDir(dir);
375
+ await writeRegistry((registry) => {
376
+ delete registry.nudgeSeen[absDir];
377
+ });
378
+ }
package/bin/tui.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
1
4
  /**
2
5
  * bin/tui.mjs — Zero-dependency TUI primitives for ForgeDock
3
6
  *
@@ -1347,19 +1350,17 @@ export async function runSteps(steps, opts = {}) {
1347
1350
 
1348
1351
  drawActive();
1349
1352
 
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`);
1353
+ // Move back to bottom (the empty line AFTER the last step row).
1354
+ // drawActive() leaves the cursor on row i with no trailing newline,
1355
+ // so we need rowsFromBottom (not -1) to reach the line below row count-1.
1356
+ if (rowsFromBottom > 0) stream.write(`\x1b[${rowsFromBottom}B`);
1354
1357
  cursorAtBottom = true;
1355
1358
 
1356
1359
  // Spinner interval updates row i in-place
1357
1360
  const timer = setInterval(() => {
1358
- const savedCursorAtBottom = cursorAtBottom;
1359
1361
  cursorUp(rowsFromBottom);
1360
1362
  drawActive();
1361
- if (rowsBelow > 0) stream.write(`\x1b[${rowsBelow}B`);
1362
- cursorAtBottom = savedCursorAtBottom;
1363
+ if (rowsFromBottom > 0) stream.write(`\x1b[${rowsFromBottom}B`);
1363
1364
  }, spinnerInterval);
1364
1365
  timer.unref();
1365
1366
 
@@ -1407,8 +1408,9 @@ export async function runSteps(steps, opts = {}) {
1407
1408
  cursorAtBottom = false;
1408
1409
  overwriteRow(i);
1409
1410
  stream.write("\n");
1410
- // Move down to bottom (rowsBelow rows remaining after the newline)
1411
- if (rowsBelow > 0) stream.write(`\x1b[${rowsBelow}B`);
1411
+ // After the \n, cursor is on row i+1. Distance to bottom = rowsFromBottom - 1.
1412
+ const rowsBelowAfterNl = rowsFromBottom - 1;
1413
+ if (rowsBelowAfterNl > 0) stream.write(`\x1b[${rowsBelowAfterNl}B`);
1412
1414
  cursorAtBottom = true;
1413
1415
 
1414
1416
  if (stepError) {
@@ -1426,11 +1428,11 @@ export async function runSteps(steps, opts = {}) {
1426
1428
 
1427
1429
  if (result) {
1428
1430
  // Failed
1429
- stream.write(`${red("✖")} ${dim("Failed in")} ${elapsed}\n`);
1431
+ stream.write(`\r\x1b[K${red("✖")} ${dim("Failed in")} ${elapsed}\n`);
1430
1432
  return result;
1431
1433
  }
1432
1434
 
1433
- stream.write(`${green("✔")} ${dim("Done in")} ${elapsed}\n`);
1435
+ stream.write(`\r\x1b[K${green("✔")} ${dim("Done in")} ${elapsed}\n`);
1434
1436
  return { ok: true, elapsed: Date.now() - t0 };
1435
1437
  }
1436
1438
 
@@ -1447,105 +1449,152 @@ const USE_TRUECOLOR =
1447
1449
  USE_ANSI &&
1448
1450
  (process.env.COLORTERM === "truecolor" ||
1449
1451
  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
+ // Windows Terminal sets WT_SESSION but not always COLORTERM
1453
+ !!process.env.WT_SESSION ||
1452
1454
  process.env.TERM_PROGRAM === "iTerm.app" ||
1453
- process.env.TERM_PROGRAM === "vscode");
1455
+ process.env.TERM_PROGRAM === "vscode" ||
1456
+ process.env.TERM_PROGRAM === "WezTerm" ||
1457
+ process.env.TERM_PROGRAM === "Hyper" ||
1458
+ // Ghostty, Alacritty, kitty — modern GPU-accelerated terminals
1459
+ process.env.TERM_PROGRAM === "ghostty" ||
1460
+ process.env.TERM === "xterm-kitty");
1454
1461
 
1455
1462
  /**
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
1463
+ * Angular F-monogram pixel map — hand-crafted to match the ForgeDock brand mark.
1462
1464
  *
1463
- * Brand color: #58a6ff = RGB(88, 166, 255)
1464
- * Background: #0d1117 = RGB(13, 17, 23) (GitHub dark canvas — renders as "off" pixels)
1465
+ * Two forward-leaning parallelogram arms forming a stylized italic F:
1466
+ * - Upper arm: wide, sweeps from center-left to upper-right (rows 0-6)
1467
+ * - Notch: triangular gap opening right between the two arms (rows 7-8)
1468
+ * - Lower arm: shorter, same lean angle (rows 9-12)
1469
+ * - Stem: tapers to a point at bottom-left (rows 13-19)
1465
1470
  *
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>
1471
+ * 18 columns × 20 rows → 18 wide × 10 terminal rows (half-block pairs).
1472
+ * 1 = filled pixel, 0 = transparent (uses terminal default background).
1470
1473
  */
1471
1474
  // 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
- }
1475
+ const LOGO_PIXELS = [
1476
+ //0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
1477
+ [0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0], // 0: upper arm top
1478
+ [0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0], // 1
1479
+ [0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0], // 2
1480
+ [0,0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0], // 3
1481
+ [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0], // 4
1482
+ [0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0], // 5
1483
+ [0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0], // 6: upper arm bottom
1484
+ [0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0], // 7: stem only (notch)
1485
+ [0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 8: stem only (notch)
1486
+ [1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0], // 9: lower arm top
1487
+ [1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0], // 10
1488
+ [1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0], // 11
1489
+ [1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0], // 12: lower arm bottom
1490
+ [1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0], // 13: stem
1491
+ [1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 14
1492
+ [1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 15
1493
+ [1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 16
1494
+ [1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 17
1495
+ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 18
1496
+ [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], // 19
1497
+ ];
1485
1498
 
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
- ];
1499
+ /**
1500
+ * Gradient palette for the F-monogram — interpolates from bright ice-blue
1501
+ * at the top to deep ocean-blue at the bottom, creating a metallic sheen.
1502
+ *
1503
+ * Each entry is [R, G, B] for the corresponding pixel row pair.
1504
+ */
1505
+ const LOGO_GRADIENT = [
1506
+ [150, 215, 255], // rows 0-1: ice blue (highlight)
1507
+ [140, 208, 255], // rows 2-3
1508
+ [128, 198, 255], // rows 4-5
1509
+ [115, 185, 255], // rows 6-7
1510
+ [100, 172, 250], // rows 8-9: brand blue
1511
+ [85, 158, 240], // rows 10-11
1512
+ [68, 140, 228], // rows 12-13
1513
+ [52, 122, 215], // rows 14-15
1514
+ [40, 108, 200], // rows 16-17
1515
+ [35, 95, 190], // rows 18-19: deep blue
1516
+ ];
1525
1517
 
1518
+ /**
1519
+ * Render the F-monogram as half-block art with per-row gradient colors.
1520
+ * Uses transparent background (no bg color set — inherits terminal default).
1521
+ *
1522
+ * @returns {string[]} Array of ANSI-decorated terminal lines (10 lines)
1523
+ */
1524
+ function renderLogoArt() {
1525
+ const RST = "\x1b[0m";
1526
1526
  const lines = [];
1527
- for (let r = 0; r < rows.length; r += 2) {
1528
- const top = rows[r];
1529
- const bot = rows[r + 1];
1527
+
1528
+ for (let r = 0; r < LOGO_PIXELS.length; r += 2) {
1529
+ const top = LOGO_PIXELS[r];
1530
+ const bot = LOGO_PIXELS[r + 1];
1531
+ const pairIdx = r / 2; // 0..7
1532
+ const [tR, tG, tB] = LOGO_GRADIENT[pairIdx];
1533
+
1534
+ // Slightly darker shade for the lower pixel row (depth effect)
1535
+ const bR = Math.max(0, tR - 15);
1536
+ const bG = Math.max(0, tG - 15);
1537
+ const bB = Math.max(0, tB - 15);
1538
+
1530
1539
  let line = "";
1531
1540
  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);
1541
+ const hasTop = top[c];
1542
+ const hasBot = bot[c];
1543
+
1544
+ if (hasTop && hasBot) {
1545
+ // Both filled: fg=top color, bg=bot color, print ▀
1546
+ line += `\x1b[38;2;${tR};${tG};${tB}m\x1b[48;2;${bR};${bG};${bB}m▀${RST}`;
1547
+ } else if (hasTop && !hasBot) {
1548
+ // Only top filled: fg=top color, no bg (transparent), print ▀
1549
+ line += `\x1b[38;2;${tR};${tG};${tB}m▀${RST}`;
1550
+ } else if (!hasTop && hasBot) {
1551
+ // Only bottom filled: fg=bot color, no bg, print ▄ (lower half block)
1552
+ line += `\x1b[38;2;${bR};${bG};${bB}m▄${RST}`;
1553
+ } else {
1554
+ // Both empty: space
1555
+ line += " ";
1556
+ }
1535
1557
  }
1536
1558
  lines.push(line);
1537
1559
  }
1538
1560
  return lines;
1539
- })();
1561
+ }
1562
+
1563
+ /**
1564
+ * Render text with a horizontal gradient (per-character 24-bit color).
1565
+ *
1566
+ * @param {string} text
1567
+ * @param {[number,number,number]} start - Start RGB
1568
+ * @param {[number,number,number]} end - End RGB
1569
+ * @returns {string} ANSI-colored string
1570
+ */
1571
+ function gradientText(text, start, end) {
1572
+ const RST = "\x1b[0m";
1573
+ let result = "\x1b[1m"; // bold
1574
+ const len = text.length;
1575
+ for (let i = 0; i < len; i++) {
1576
+ if (text[i] === " ") {
1577
+ result += " ";
1578
+ continue;
1579
+ }
1580
+ const t = len === 1 ? 0 : i / (len - 1);
1581
+ const r = Math.round(start[0] + (end[0] - start[0]) * t);
1582
+ const g = Math.round(start[1] + (end[1] - start[1]) * t);
1583
+ const b = Math.round(start[2] + (end[2] - start[2]) * t);
1584
+ result += `\x1b[38;2;${r};${g};${b}m${text[i]}`;
1585
+ }
1586
+ return result + RST;
1587
+ }
1540
1588
 
1541
1589
  /**
1542
1590
  * Render the ForgeDock logo for display in the terminal.
1543
1591
  *
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.
1592
+ * On truecolor TTY: angular F-monogram with gradient, brand name with gradient
1593
+ * text, and tagline — rendered side-by-side (logo left, text right).
1594
+ * On non-TTY / NO_COLOR / 256-color: plain text.
1546
1595
  *
1547
1596
  * @param {object} [opts]
1548
- * @param {string} [opts.version] - Package version string (e.g. "1.0.8")
1597
+ * @param {string} [opts.version] - Package version string (e.g. "1.0.14")
1549
1598
  * @returns {string} Rendered logo string (may contain ANSI sequences)
1550
1599
  */
1551
1600
  export function renderLogo({ version = "" } = {}) {
@@ -1557,14 +1606,41 @@ export function renderLogo({ version = "" } = {}) {
1557
1606
  return `${versionStr}\n${tagline}`;
1558
1607
  }
1559
1608
 
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
- "",
1609
+ // Truecolor: side-by-side layout — logo on left, text on right
1610
+ const art = renderLogoArt(); // 10 lines, each 18 chars wide (visible)
1611
+ const pad = " "; // gap between logo and text
1612
+
1613
+ // Text lines to appear to the right of the logo (vertically centered)
1614
+ const brandLine = gradientText(
1615
+ "FORGEDOCK",
1616
+ [150, 215, 255], // ice blue
1617
+ [35, 95, 190], // deep blue
1618
+ );
1619
+ const versionLine = version
1620
+ ? `\x1b[2m\x1b[38;2;88;166;255mv${version}\x1b[0m`
1621
+ : "";
1622
+ const taglineLine = `\x1b[2m${tagline}\x1b[0m`;
1623
+
1624
+ // Place text starting at row 3 (0-indexed) for vertical centering
1625
+ // art has 10 rows; text block occupies rows 3-6
1626
+ const textRows = [
1627
+ "", // row 0
1628
+ "", // row 1
1629
+ "", // row 2
1630
+ brandLine, // row 3: FORGEDOCK
1631
+ versionLine, // row 4: v1.0.14
1632
+ "", // row 5
1633
+ taglineLine, // row 6: tagline
1634
+ "", // row 7
1635
+ "", // row 8
1636
+ "", // row 9
1568
1637
  ];
1638
+
1639
+ const lines = [""];
1640
+ for (let i = 0; i < art.length; i++) {
1641
+ const textPart = textRows[i] || "";
1642
+ lines.push(` ${art[i]}${pad}${textPart}`);
1643
+ }
1644
+ lines.push("");
1569
1645
  return lines.join("\n");
1570
1646
  }