@launchsecure/launch-kit 0.0.25 → 0.0.27

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 (132) hide show
  1. package/README.md +50 -0
  2. package/dist/beacon/beacon.mjs +1016 -0
  3. package/dist/beacon/beacon.mjs.map +1 -0
  4. package/dist/beacon/beacon.umd.js +87 -0
  5. package/dist/beacon/beacon.umd.js.map +1 -0
  6. package/dist/beacon/index-DAIDnjfR.mjs +513 -0
  7. package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
  8. package/dist/beacon/types/capture/element.d.ts +3 -0
  9. package/dist/beacon/types/capture/element.d.ts.map +1 -0
  10. package/dist/beacon/types/capture/framework.d.ts +3 -0
  11. package/dist/beacon/types/capture/framework.d.ts.map +1 -0
  12. package/dist/beacon/types/capture/metadata.d.ts +3 -0
  13. package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
  14. package/dist/beacon/types/capture/overlay.d.ts +7 -0
  15. package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
  16. package/dist/beacon/types/capture/picker.d.ts +12 -0
  17. package/dist/beacon/types/capture/picker.d.ts.map +1 -0
  18. package/dist/beacon/types/capture/screenshot.d.ts +7 -0
  19. package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
  20. package/dist/beacon/types/capture/selector.d.ts +2 -0
  21. package/dist/beacon/types/capture/selector.d.ts.map +1 -0
  22. package/dist/beacon/types/element.d.ts +50 -0
  23. package/dist/beacon/types/element.d.ts.map +1 -0
  24. package/dist/beacon/types/index.d.ts +4 -0
  25. package/dist/beacon/types/index.d.ts.map +1 -0
  26. package/dist/beacon/types/transport/submit.d.ts +3 -0
  27. package/dist/beacon/types/transport/submit.d.ts.map +1 -0
  28. package/dist/beacon/types/types.d.ts +88 -0
  29. package/dist/beacon/types/types.d.ts.map +1 -0
  30. package/dist/beacon/types/ui/button.d.ts +2 -0
  31. package/dist/beacon/types/ui/button.d.ts.map +1 -0
  32. package/dist/beacon/types/ui/drawer.d.ts +31 -0
  33. package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
  34. package/dist/beacon/types/ui/icons.d.ts +9 -0
  35. package/dist/beacon/types/ui/icons.d.ts.map +1 -0
  36. package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
  37. package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
  38. package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
  39. package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
  40. package/dist/chart-client/assets/index-CJ4mgRRF.css +1 -0
  41. package/dist/chart-client/assets/{index-C8ANseEa.js → index-Ccy-DpI-.js} +82 -73
  42. package/dist/chart-client/index.html +2 -2
  43. package/dist/client/assets/index-DI5qSR_w.css +32 -0
  44. package/dist/client/assets/index-Dp0_okva.js +294 -0
  45. package/dist/client/index.html +2 -2
  46. package/dist/council-client/assets/index-C_-vAM9L.css +1 -0
  47. package/dist/council-client/assets/{index-Dc41S-R2.js → index-Dt4zWKSj.js} +14 -14
  48. package/dist/council-client/index.html +2 -2
  49. package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-W2JQDmje.js} +1 -1
  50. package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-DIBWAId9.js} +1 -1
  51. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-CAIRMvJK.js} +1 -1
  52. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-BeNaNiOi.js} +1 -1
  53. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-B9Ozi62h.js} +1 -1
  54. package/dist/deck-client/assets/channel-CRdozqbp.js +1 -0
  55. package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-D7AZ47dt.js} +1 -1
  56. package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-DnVnNPcI.js} +1 -1
  57. package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-UKYs-YNd.js} +1 -1
  58. package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-D43b-SKn.js} +1 -1
  59. package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-QzBAoyyW.js} +1 -1
  60. package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-Cjif4r6W.js} +1 -1
  61. package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-CqLDirEI.js} +1 -1
  62. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-_FQvmMs4.js} +1 -1
  63. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-lIZMp57W.js +1 -0
  64. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-lIZMp57W.js +1 -0
  65. package/dist/deck-client/assets/clone-BtWeSTyJ.js +1 -0
  66. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-rfrocesE.js} +1 -1
  67. package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bv_7DJat.js} +1 -1
  68. package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-4F1414G5.js} +1 -1
  69. package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-C4-Pszqm.js} +1 -1
  70. package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-B647TIx9.js} +1 -1
  71. package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-BFAqpezd.js} +1 -1
  72. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-BfBfrJOC.js} +1 -1
  73. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-DX9YAYes.js} +1 -1
  74. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-DCuiy7wF.js} +1 -1
  75. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-CGp1IXUh.js} +1 -1
  76. package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-B7g8aoxv.js} +1 -1
  77. package/dist/deck-client/assets/{index-BfIfJXmS.js → index-Dg1r-WSN.js} +68 -68
  78. package/dist/deck-client/assets/index-DsIZ3LqL.css +1 -0
  79. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-L3fahMkF.js} +1 -1
  80. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-aS_EjWBZ.js} +1 -1
  81. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-djTSQZF9.js} +1 -1
  82. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-CcTHo4CM.js} +1 -1
  83. package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-mEJiadb7.js} +1 -1
  84. package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-XgTKqyRu.js} +1 -1
  85. package/dist/deck-client/assets/{min-CbUksbuI.js → min-Ct9jZdpd.js} +1 -1
  86. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-BaFxCGNU.js} +1 -1
  87. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-CIbYYjtw.js} +1 -1
  88. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-D9EtCOvh.js} +1 -1
  89. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-xeni9eVG.js} +1 -1
  90. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-LYeknz9h.js} +1 -1
  91. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-RDbsKFZf.js} +1 -1
  92. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-BH1Zjglk.js} +1 -1
  93. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BrV78NDR.js +1 -0
  94. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-IFXxKptt.js} +1 -1
  95. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-D-sLkQs9.js} +1 -1
  96. package/dist/deck-client/assets/wardley-RL74JXVD-C010F8l4.js +162 -0
  97. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BTjjuDU3.js} +1 -1
  98. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-AYbv92n-.js} +1 -1
  99. package/dist/deck-client/index.html +2 -2
  100. package/dist/server/chart-serve.js +4524 -3564
  101. package/dist/server/cli.js +27351 -5398
  102. package/dist/server/council-entry.js +17 -5
  103. package/dist/server/council-serve.js +8 -3
  104. package/dist/server/deck-mcp-entry.js +354 -13
  105. package/dist/server/deck-serve.js +298 -7
  106. package/dist/server/graph/queries/classify.scm +8 -0
  107. package/dist/server/graph/queries/exports.scm +7 -0
  108. package/dist/server/graph-mcp-entry.js +5943 -4361
  109. package/dist/server/init-entry.js +609 -0
  110. package/dist/server/orbit-entry.js +2272 -0
  111. package/dist/server/{server/chart-serve.js → parse-worker-entry.js} +1900 -1822
  112. package/dist/server/recall-entry.js +1450 -0
  113. package/package.json +40 -8
  114. package/scaffolds/migrate-safety/.github/workflows/backup-on-migration.yml +72 -0
  115. package/scaffolds/migrate-safety/docs/migrations-runbook.md +172 -0
  116. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +294 -0
  117. package/dist/chart-client/assets/index--120d9P9.css +0 -1
  118. package/dist/client/assets/index-Bf8zdL3x.css +0 -32
  119. package/dist/client/assets/index-Ds9UP_cj.js +0 -291
  120. package/dist/council-client/assets/index-CofZh7pS.css +0 -1
  121. package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
  122. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
  123. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
  124. package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
  125. package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
  126. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
  127. package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
  128. package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
  129. package/dist/server/deck-server/deck-serve.js +0 -1275
  130. package/dist/server/server/cli.js +0 -13360
  131. package/dist/server/server/fb-wizard.js +0 -136
  132. package/dist/server/server/graph-mcp-entry.js +0 -6776
@@ -1,1789 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
- var __export = (target, all) => {
13
- for (var name in all)
14
- __defProp(target, name, { get: all[name], enumerable: true });
15
- };
16
- var __copyProps = (to, from, except, desc) => {
17
- if (from && typeof from === "object" || typeof from === "function") {
18
- for (let key of __getOwnPropNames(from))
19
- if (!__hasOwnProp.call(to, key) && key !== except)
20
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
- }
22
- return to;
23
- };
24
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
- // If the importer is in node compatibility mode or this is not an ESM
26
- // file that has been converted to a CommonJS file using a Babel-
27
- // compatible transform (i.e. "__esModule" has not been set), then set
28
- // "default" to the CommonJS "module.exports" for node compatibility.
29
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
- mod
31
- ));
32
-
33
- // src/deck-server/lockfile.ts
34
- function lockDir(projectRoot) {
35
- if (projectRoot) {
36
- return (0, import_node_path.join)(projectRoot, ".launchsecure");
37
- }
38
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
39
- }
40
- function lockPath(projectRoot) {
41
- return (0, import_node_path.join)(lockDir(projectRoot), "launch-deck.lock");
42
- }
43
- function setProjectRoot(root) {
44
- _activeProjectRoot = root;
45
- }
46
- function readLock(projectRoot) {
47
- const root = projectRoot ?? _activeProjectRoot;
48
- const p = lockPath(root);
49
- if (!(0, import_node_fs.existsSync)(p)) return null;
50
- try {
51
- const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
52
- if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
53
- return data;
54
- } catch {
55
- return null;
56
- }
57
- }
58
- function isPidAlive(pid) {
59
- try {
60
- process.kill(pid, 0);
61
- return true;
62
- } catch {
63
- return false;
64
- }
65
- }
66
- function getListenerPid(port) {
67
- try {
68
- const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
69
- encoding: "utf-8",
70
- stdio: ["ignore", "pipe", "ignore"],
71
- timeout: 500
72
- }).trim();
73
- if (!out) return null;
74
- const pid = parseInt(out.split("\n")[0], 10);
75
- return Number.isFinite(pid) ? pid : null;
76
- } catch {
77
- return null;
78
- }
79
- }
80
- function getLiveLock(projectRoot) {
81
- const root = projectRoot ?? _activeProjectRoot;
82
- const lock = readLock(root);
83
- if (!lock) return null;
84
- const listenerPid = getListenerPid(lock.port);
85
- const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
86
- if (!live) {
87
- try {
88
- (0, import_node_fs.unlinkSync)(lockPath(root));
89
- } catch {
90
- }
91
- return null;
92
- }
93
- return lock;
94
- }
95
- function writeLock(data, projectRoot) {
96
- const root = projectRoot ?? _activeProjectRoot;
97
- (0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
98
- (0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
99
- if (root) _activeProjectRoot = root;
100
- }
101
- function clearLock(projectRoot) {
102
- const root = projectRoot ?? _activeProjectRoot;
103
- try {
104
- (0, import_node_fs.unlinkSync)(lockPath(root));
105
- } catch {
106
- }
107
- }
108
- var import_node_child_process, import_node_fs, import_node_os, import_node_path, _activeProjectRoot;
109
- var init_lockfile = __esm({
110
- "src/deck-server/lockfile.ts"() {
111
- "use strict";
112
- import_node_child_process = require("node:child_process");
113
- import_node_fs = require("node:fs");
114
- import_node_os = require("node:os");
115
- import_node_path = require("node:path");
116
- }
117
- });
118
-
119
- // src/deck-server/config.ts
120
- function loadDeckConfig(rootDir) {
121
- const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
122
- if (!(0, import_node_fs2.existsSync)(configPath)) return {};
123
- try {
124
- return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
125
- } catch {
126
- return {};
127
- }
128
- }
129
- function saveDeckConfig(rootDir, config) {
130
- const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
131
- (0, import_node_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
132
- }
133
- var import_node_fs2, import_node_path2, CONFIG_FILENAME;
134
- var init_config = __esm({
135
- "src/deck-server/config.ts"() {
136
- "use strict";
137
- import_node_fs2 = require("node:fs");
138
- import_node_path2 = require("node:path");
139
- CONFIG_FILENAME = ".launchdeck.json";
140
- }
141
- });
142
-
143
- // src/deck-server/blast-radius-render.ts
144
- function generateBlastRadiusHtml(manifest, baseUrl) {
145
- const data = JSON.stringify(manifest);
146
- const dlBase = baseUrl ? baseUrl.replace("/deck-files/", "/deck-download/") : "";
147
- return `<!DOCTYPE html>
148
- <html lang="en">
149
- <head>
150
- <meta charset="UTF-8">
151
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
152
- <title>Blast Radius \u2014 ${escHtml(manifest.title)}</title>
153
- <script src="https://d3js.org/d3.v7.min.js"></script>
154
- <script src="https://unpkg.com/lucide@latest"></script>
155
- <style>
156
- *{margin:0;padding:0;box-sizing:border-box}
157
- body{background:#08090e;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;overflow:hidden;height:100vh;width:100vw}
158
- .header{position:fixed;top:0;left:0;right:0;z-index:20;padding:16px 24px 12px;background:linear-gradient(180deg,rgba(8,9,14,.98) 60%,rgba(8,9,14,0) 100%);pointer-events:none}
159
- .header>*{pointer-events:auto}
160
- .header h1{font-size:10px;font-weight:600;color:#64748b;letter-spacing:1.2px;text-transform:uppercase}
161
- .header h2{font-size:18px;font-weight:700;color:#f1f5f9;margin-top:4px}
162
- .header h2 span{color:#b91c1c}
163
- .stats{display:flex;gap:24px;margin-top:10px;flex-wrap:wrap;align-items:center}
164
- .stat-val{font-size:22px;font-weight:700;line-height:1}
165
- .stat-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:.6px;margin-top:2px}
166
- .stat-sep{width:1px;height:28px;background:#334155}
167
- .filter-panel{position:fixed;top:16px;right:24px;z-index:20;display:flex;flex-direction:column;gap:3px;background:rgba(8,9,14,.92);padding:14px;border-radius:12px;backdrop-filter:blur(12px);border:1px solid rgba(100,116,139,.12);min-width:160px}
168
- .fp-section{font-size:8px;color:#475569;text-transform:uppercase;letter-spacing:1.2px;margin-top:10px;padding:0 6px}
169
- .fp-section:first-child{margin-top:0}
170
- .fp-btn{display:flex;align-items:center;gap:10px;font-size:12px;color:#64748b;background:none;border:1px solid transparent;border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
171
- .fp-btn:hover{background:rgba(51,65,85,.5);color:#cbd5e1}
172
- .fp-btn.active{background:rgba(99,102,241,.12);color:#e2e8f0;border-color:rgba(99,102,241,.2)}
173
- .fp-toggle{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#64748b;background:rgba(30,41,59,.5);border:1px solid rgba(100,116,139,.15);border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
174
- .fp-toggle:hover{background:rgba(51,65,85,.6);color:#cbd5e1}
175
- .fp-toggle .toggle-dot{width:32px;height:16px;border-radius:8px;background:#334155;position:relative;transition:all .2s;flex-shrink:0}
176
- .fp-toggle .toggle-dot::after{content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#64748b;transition:all .2s}
177
- .fp-toggle.on .toggle-dot{background:rgba(99,102,241,.4)}
178
- .fp-toggle.on .toggle-dot::after{left:18px;background:#818cf8}
179
- .fp-toggle.on{color:#e2e8f0}
180
- .fp-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
181
- .fp-ring{width:10px;height:10px;border-radius:50%;border:2px solid;flex-shrink:0;background:transparent}
182
- .fp-icon{width:16px;height:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
183
- .fp-icon svg{width:16px;height:16px}
184
- .fp-edge-line{width:16px;height:2px;border-radius:1px;flex-shrink:0}
185
- .detail-panel{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:none;width:340px;max-height:70vh;overflow-y:auto;background:rgba(15,23,42,.97);border:1px solid rgba(100,116,139,.2);border-radius:12px;padding:0;backdrop-filter:blur(16px);box-shadow:0 16px 48px rgba(0,0,0,.7)}
186
- .dp-header{padding:16px 18px 12px;border-bottom:1px solid rgba(100,116,139,.1)}
187
- .dp-name{font-weight:700;font-size:16px}
188
- .dp-badges{display:flex;gap:5px;margin-top:8px;flex-wrap:wrap}
189
- .dp-badge{font-size:10px;padding:3px 9px;border-radius:4px;font-weight:600;letter-spacing:.3px}
190
- .dp-body{padding:14px 18px 16px}
191
- .dp-section{margin-top:12px}
192
- .dp-section:first-child{margin-top:0}
193
- .dp-section-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;font-weight:600}
194
- .dp-path{color:#64748b;font-size:11px;word-break:break-all;font-family:'SF Mono','Fira Code',monospace;background:rgba(100,116,139,.08);padding:6px 10px;border-radius:6px}
195
- .dp-reason{color:#cbd5e1;font-size:12px;line-height:1.6}
196
- .dp-connections{list-style:none;padding:0}
197
- .dp-connections li{font-size:11px;color:#94a3b8;padding:3px 0;display:flex;align-items:center;gap:6px}
198
- .dp-close{position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;cursor:pointer;font-size:18px;line-height:1;padding:4px}
199
- .dp-close:hover{color:#e2e8f0}
200
- .controls{display:none}
201
- .ring-label{font-size:8px;fill:#475569;text-anchor:start;font-weight:600;letter-spacing:1.5px;text-transform:uppercase}
202
- .edge-tooltip{position:fixed;z-index:200;pointer-events:none;background:rgba(15,23,42,.95);border:1px solid rgba(100,116,139,.25);border-radius:8px;padding:8px 12px;backdrop-filter:blur(8px);font-size:11px;line-height:1.5;max-width:260px;display:none}
203
- .edge-tooltip .et-action{font-weight:700;font-size:10px;letter-spacing:.5px;text-transform:uppercase}
204
- .edge-tooltip .et-nodes{color:#94a3b8}
205
- .edge-tooltip .et-label{color:#cbd5e1;margin-top:2px;font-style:italic}
206
- </style>
207
- </head>
208
- <body>
209
- <div class="header">
210
- <h1>${escHtml(manifest.subtitle || (manifest.mode === "structural" ? "Structural Blast Radius" : "Feature Blast Radius"))}</h1>
211
- <h2><span>${escHtml(manifest.title)}</span></h2>
212
- <div class="stats" id="stats"></div>
213
- </div>
214
- <div class="filter-panel" id="filterPanel"></div>
215
- <div class="controls" id="controls"></div>
216
- <div class="detail-panel" id="detail">
217
- <button class="dp-close" id="dpClose">&times;</button>
218
- <div class="dp-header">
219
- <div class="dp-name" id="dpName"></div>
220
- <div class="dp-badges" id="dpBadges"></div>
221
- </div>
222
- <div class="dp-body">
223
- <div class="dp-section" id="dpPathSection">
224
- <div class="dp-section-label">File</div>
225
- <div class="dp-path" id="dpPath"></div>
226
- </div>
227
- <div class="dp-section" id="dpReasonSection">
228
- <div class="dp-section-label">Why is this affected?</div>
229
- <div class="dp-reason" id="dpReason"></div>
230
- </div>
231
- <div class="dp-section" id="dpAcceptanceSection">
232
- <div class="dp-section-label">Acceptance Criteria</div>
233
- <ul class="dp-connections" id="dpAcceptance"></ul>
234
- </div>
235
- <div class="dp-section" id="dpConnSection">
236
- <div class="dp-section-label">Connected to</div>
237
- <ul class="dp-connections" id="dpConn"></ul>
238
- </div>
239
- </div>
240
- </div>
241
- <div class="edge-tooltip" id="edgeTooltip"></div>
242
- <svg id="graph"></svg>
243
- <script>
244
- var M = ${data};
245
- var DL_BASE = '${dlBase}';
246
-
247
- var layerMap = {};
248
- M.layers.forEach(function(l) { layerMap[l.id] = l; });
249
- var ringMap = {};
250
- M.rings.forEach(function(r) { ringMap[r.id] = r; });
251
- var centerColor = '#b91c1c';
252
-
253
- // \u2500\u2500 Edge Action Styles \u2500\u2500
254
- var edgeActionStyles = {
255
- create: {color:'#22c55e', dash:'', opacity:0.55, width:2},
256
- modify: {color:'#f59e0b', dash:'7,4', opacity:0.55, width:2},
257
- remove: {color:'#ef4444', dash:'4,4', opacity:0.5, width:1.8},
258
- existing: {color:'#1e293b', dash:'', opacity:0.25, width:1}
259
- };
260
- var defaultEdgeStyle = edgeActionStyles.existing;
261
- function edgeStyle(d){return edgeActionStyles[d.action]||defaultEdgeStyle;}
262
- var hasActionEdges = M.edges.some(function(e){return e.action && e.action!=='existing';});
263
-
264
- // \u2500\u2500 Stats \u2500\u2500
265
- var statsEl = document.getElementById('stats');
266
- M.rings.forEach(function(r) {
267
- var count = M.nodes.filter(function(n){return n.ring===r.id}).length;
268
- var d = document.createElement('div');
269
- d.innerHTML = '<div class="stat-val" style="color:'+r.color+'">'+count+'</div><div class="stat-label">'+r.name+'</div>';
270
- statsEl.appendChild(d);
271
- });
272
- (function(){var d=document.createElement('div');d.innerHTML='<div class="stat-val" style="color:#e2e8f0">'+M.layers.length+'</div><div class="stat-label">Layers</div>';statsEl.appendChild(d);})();
273
-
274
- // Edge action stats (only when action edges exist)
275
- if(hasActionEdges){
276
- var sep=document.createElement('div');sep.className='stat-sep';statsEl.appendChild(sep);
277
- ['create','modify','remove'].forEach(function(a){
278
- var count=M.edges.filter(function(e){return e.action===a;}).length;
279
- if(count===0)return;
280
- var s=edgeActionStyles[a];
281
- var d=document.createElement('div');
282
- d.innerHTML='<div class="stat-val" style="color:'+s.color+'">'+count+'</div><div class="stat-label">'+a.toUpperCase()+' edges</div>';
283
- statsEl.appendChild(d);
284
- });
285
- }
286
-
287
- // \u2500\u2500 Filter Panel (left vertical) \u2500\u2500
288
- var fpEl = document.getElementById('filterPanel');
289
- var fpHtml = '';
290
- fpHtml += '<div class="fp-section">Toggles</div>';
291
- fpHtml += '<button class="fp-toggle on" id="btnLabels">Aa Labels<div class="toggle-dot"></div></button>';
292
- fpHtml += '<button class="fp-toggle on" id="btnRings">Ring colors<div class="toggle-dot"></div></button>';
293
- fpHtml += '<div class="fp-section" style="margin-top:4px"></div>';
294
- fpHtml += '<button class="fp-btn" id="btnReset">\\u21ba Reset zoom</button>';
295
- fpHtml += '<div class="fp-section">Ring</div>';
296
- fpHtml += '<button class="fp-btn active" id="rAll" data-ring="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
297
- M.rings.forEach(function(r){fpHtml+='<button class="fp-btn" id="r_'+r.id+'" data-ring="'+r.id+'"><div class="fp-ring" style="border-color:'+r.color+'"></div> '+r.name+'</button>';});
298
- fpHtml += '<div class="fp-section">Layer</div>';
299
- fpHtml += '<button class="fp-btn active" id="lAll" data-layer="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
300
- M.layers.forEach(function(l){fpHtml+='<button class="fp-btn" id="l_'+l.id+'" data-layer="'+l.id+'"><div class="fp-icon" id="fpi_'+l.id+'" style="color:'+l.color+'"></div> '+l.name+'</button>';});
301
-
302
- // Edge Action filter (only when action edges exist)
303
- if(hasActionEdges){
304
- fpHtml += '<div class="fp-section">Edge Action</div>';
305
- fpHtml += '<button class="fp-btn active" id="eAll" data-edge="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
306
- ['create','modify','remove','existing'].forEach(function(a){
307
- var count=M.edges.filter(function(e){return(e.action||'existing')===a;}).length;
308
- if(count===0)return;
309
- var s=edgeActionStyles[a];
310
- var dashStyle=s.dash?'background:repeating-linear-gradient(90deg,'+s.color+' 0,'+s.color+' 4px,transparent 4px,transparent 7px)':'background:'+s.color;
311
- fpHtml+='<button class="fp-btn" id="e_'+a+'" data-edge="'+a+'"><div class="fp-edge-line" style="'+dashStyle+'"></div> '+a.charAt(0).toUpperCase()+a.slice(1)+' <span style="color:#475569;font-size:10px">('+count+')</span></button>';
312
- });
313
- }
314
-
315
- fpEl.innerHTML = fpHtml;
316
-
317
- var ringsOn = true;
318
-
319
- // Attach filter panel handlers via event delegation
320
- fpEl.addEventListener('click', function(e) {
321
- var btn = e.target.closest('.fp-btn,.fp-toggle');
322
- if (!btn) return;
323
- if (btn.id === 'btnReset') { svg.transition().duration(400).call(zoomBehavior.transform, d3.zoomIdentity); return; }
324
- if (btn.id === 'btnLabels') { labelsOn=!labelsOn; labelEls.transition().duration(200).attr('opacity',labelsOn?1:0); btn.classList.toggle('on',labelsOn); return; }
325
- if (btn.id === 'btnRings') {
326
- ringsOn=!ringsOn;
327
- btn.classList.toggle('on',ringsOn);
328
- // Hide/show node circles (not center)
329
- nodeEls.select('.node-bg')
330
- .transition().duration(300)
331
- .attr('stroke',function(d){
332
- if(d.hop===0) return centerColor;
333
- return ringsOn?(ringMap[d.ring]?ringMap[d.ring].color:'#666'):'transparent';
334
- })
335
- .attr('fill',function(d){
336
- if(d.hop===0) return '#1f0a0a';
337
- return ringsOn?'rgba(8,9,14,.85)':'transparent';
338
- })
339
- .attr('stroke-width',function(d){if(d.hop===0) return 3; return ringsOn?3:0;});
340
- // Concentric ring circles: colored when on, grey when off
341
- g.selectAll('.ring-circle').transition().duration(300)
342
- .attr('stroke',function(){return ringsOn?d3.select(this).attr('data-color'):'#334155';})
343
- .attr('stroke-opacity',ringsOn?0.3:0.15);
344
- // Hide/show edges \u2014 restore action-based opacity when turning back on
345
- edgePaths.transition().duration(300).attr('opacity',function(d){return ringsOn?edgeStyle(d).opacity:0;});
346
- // Scale icons (not center)
347
- nodeEls.each(function(d){
348
- if(d.hop===0) return;
349
- var fo=d3.select(this).select('foreignObject');
350
- if(fo.empty()) return;
351
- var r=nR(d);
352
- var s=ringsOn?Math.round(r*1.1):Math.round(r*2.5);
353
- fo.attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s);
354
- fo.select('div').style('width',s+'px').style('height',s+'px');
355
- var icon=fo.select('svg');
356
- if(!icon.empty()){
357
- icon.attr('width',s).attr('height',s)
358
- .style('width',s+'px').style('height',s+'px')
359
- .style('color','#cbd5e1')
360
- .style('fill',ringsOn?'rgba(203,213,225,0.15)':'rgba(203,213,225,0.3)')
361
- .style('stroke-width',ringsOn?'1.5':'1');
362
- }
363
- });
364
- return;
365
- }
366
- if (btn.dataset && btn.dataset.ring !== undefined) { filterRing(btn.dataset.ring); return; }
367
- if (btn.dataset && btn.dataset.layer !== undefined) { filterLayer(btn.dataset.layer); return; }
368
- if (btn.dataset && btn.dataset.edge !== undefined) { filterEdgeAction(btn.dataset.edge); return; }
369
- });
370
-
371
- // \u2500\u2500 Layout \u2500\u2500
372
- var W = window.innerWidth, H = window.innerHeight;
373
- var cx = W/2, cy = H/2 + 25;
374
- var R = Math.min(W, H);
375
- var ringIds = M.rings.map(function(r){return r.id});
376
- var ringRadii = {};
377
- ringIds.forEach(function(id, i){ ringRadii[id] = R * (0.14 + i * 0.13); });
378
-
379
- var centerNode = {id:'__center__',name:M.center.name,layer:M.layers[0]?M.layers[0].id:'ui',ring:'__center__',path:'',reason:M.center.description,type:'feature',hop:0};
380
- var allNodes = [centerNode].concat(M.nodes.map(function(n){var copy={};for(var k in n)copy[k]=n[k];copy.hop=ringIds.indexOf(n.ring)+1;return copy;}));
381
-
382
- centerNode.fx = cx; centerNode.fy = cy;
383
- ringIds.forEach(function(ringId){
384
- var ring = allNodes.filter(function(n){return n.ring===ringId});
385
- var layerOrder = {};
386
- M.layers.forEach(function(l,i){layerOrder[l.id]=i;});
387
- ring.sort(function(a,b){return(layerOrder[a.layer]||99)-(layerOrder[b.layer]||99);});
388
- var r = ringRadii[ringId];
389
- ring.forEach(function(n,i){var angle=-Math.PI/2+(i/ring.length)*Math.PI*2;n.fx=cx+Math.cos(angle)*r;n.fy=cy+Math.sin(angle)*r;});
390
- });
391
-
392
- // \u2500\u2500 D3 Render \u2500\u2500
393
- var svg = d3.select('#graph').attr('width',W).attr('height',H);
394
- var g = svg.append('g');
395
- var zoomBehavior = d3.zoom().scaleExtent([0.3,5])
396
- .filter(function(event){
397
- // Don't zoom on clicks \u2014 only on wheel, drag, pinch
398
- if (event.type === 'mousedown' || event.type === 'touchstart') return true;
399
- if (event.type === 'wheel') return true;
400
- if (event.type === 'dblclick') return true;
401
- return true;
402
- })
403
- .on('zoom',function(e){g.attr('transform',e.transform);});
404
- svg.call(zoomBehavior);
405
-
406
- ringIds.forEach(function(id){
407
- var r = ringRadii[id];
408
- g.append('circle').attr('class','ring-circle').attr('data-color',ringMap[id].color).attr('cx',cx).attr('cy',cy).attr('r',r).attr('fill','none').attr('stroke',ringMap[id].color).attr('stroke-opacity',0.3).attr('stroke-width',1);
409
- var angle = -Math.PI*0.18;
410
- g.append('text').attr('class','ring-label').attr('x',cx+Math.cos(angle)*r+10).attr('y',cy+Math.sin(angle)*r).attr('fill',ringMap[id].color).attr('fill-opacity',1).text(ringMap[id].name.toUpperCase());
411
- });
412
-
413
- function nR(d){if(d.hop===0)return 30;return Math.max(18-d.hop*3,12);}
414
- function isRemoveNode(d){return d.ring==='remove';}
415
-
416
- var edgeGroup = g.append('g');
417
- var edgePaths = edgeGroup.selectAll('path').data(M.edges).join('path')
418
- .attr('fill','none')
419
- .attr('stroke',function(d){return edgeStyle(d).color;})
420
- .attr('stroke-width',function(d){return edgeStyle(d).width;})
421
- .attr('opacity',function(d){return edgeStyle(d).opacity;})
422
- .attr('stroke-dasharray',function(d){return edgeStyle(d).dash;})
423
- .attr('d',function(d){
424
- var src=allNodes.find(function(n){return n.id===(d.source==='center'?'__center__':d.source)});
425
- var tgt=allNodes.find(function(n){return n.id===d.target});
426
- if(!src||!tgt)return'';
427
- var dx=tgt.fx-src.fx,dy=tgt.fy-src.fy,dist=Math.sqrt(dx*dx+dy*dy)||1;
428
- var ux=dx/dist,uy=dy/dist;
429
- return'M'+(src.fx+ux*nR(src))+','+(src.fy+uy*nR(src))+' A'+(dist*1.1)+','+(dist*1.1)+' 0 0,1 '+(tgt.fx-ux*nR(tgt))+','+(tgt.fy-uy*nR(tgt));
430
- });
431
-
432
- // Edge hover tooltip
433
- var edgeTooltipEl = document.getElementById('edgeTooltip');
434
- edgePaths.on('mouseenter',function(event,d){
435
- if(selectedNodeId)return;
436
- var s=edgeStyle(d);
437
- var srcId=d.source==='center'?'__center__':d.source;
438
- var srcNode=allNodes.find(function(n){return n.id===srcId});
439
- var tgtNode=allNodes.find(function(n){return n.id===d.target});
440
- if(!srcNode||!tgtNode)return;
441
- var action=(d.action||'existing').toUpperCase();
442
- var html='<div class="et-action" style="color:'+s.color+'">'+action+'</div>';
443
- html+='<div class="et-nodes">'+srcNode.name+' \\u2192 '+tgtNode.name+'</div>';
444
- if(d.label)html+='<div class="et-label">'+d.label+'</div>';
445
- edgeTooltipEl.innerHTML=html;
446
- edgeTooltipEl.style.display='block';
447
- edgeTooltipEl.style.left=(event.clientX+12)+'px';
448
- edgeTooltipEl.style.top=(event.clientY-10)+'px';
449
- // Highlight this edge
450
- d3.select(this).attr('opacity',Math.min(s.opacity+0.3,1)).attr('stroke-width',s.width+1);
451
- }).on('mousemove',function(event){
452
- edgeTooltipEl.style.left=(event.clientX+12)+'px';
453
- edgeTooltipEl.style.top=(event.clientY-10)+'px';
454
- }).on('mouseleave',function(event,d){
455
- edgeTooltipEl.style.display='none';
456
- var s=edgeStyle(d);
457
- d3.select(this).attr('opacity',s.opacity).attr('stroke-width',s.width);
458
- });
459
- // Make edges easier to hover
460
- edgePaths.style('pointer-events','stroke').attr('cursor','pointer');
461
-
462
- var nodeGroup = g.append('g');
463
- var nodeEls = nodeGroup.selectAll('.node').data(allNodes).join('g')
464
- .attr('class','node').attr('transform',function(d){return'translate('+d.fx+','+d.fy+')';}).style('cursor','pointer');
465
-
466
- nodeEls.append('circle').attr('class','node-glow').attr('r',function(d){return nR(d)+12})
467
- .attr('fill',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')}).attr('opacity',0);
468
-
469
- nodeEls.append('circle').attr('class','node-bg').attr('r',function(d){return nR(d)})
470
- .attr('fill',function(d){if(d.hop===0)return'#1f0a0a';if(isRemoveNode(d))return'rgba(127,29,29,.6)';return'rgba(8,9,14,.85)';})
471
- .attr('stroke',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')})
472
- .attr('stroke-width',function(d){return d.hop===0?3:3});
473
-
474
- nodeEls.each(function(d){
475
- var el=d3.select(this),r=nR(d);
476
- if(d.hop===0){var cs=Math.round(r*1.3);var cfo=el.append('foreignObject').attr('x',-cs/2).attr('y',-cs/2).attr('width',cs).attr('height',cs).attr('style','overflow:visible;pointer-events:none;');cfo.append('xhtml:div').attr('style','width:'+cs+'px;height:'+cs+'px;display:flex;align-items:center;justify-content:center;').html('<i data-lucide="radiation" style="width:'+cs+'px;height:'+cs+'px;color:#fca5a5;fill:rgba(252,165,165,0.25);stroke-width:1.5;"></i>');return;}
477
- var layer=layerMap[d.layer];if(!layer)return;
478
- var s=Math.round(r*1.1);
479
- var fo=el.append('foreignObject').attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s).attr('style','overflow:visible;pointer-events:none;');
480
- var iconColor=isRemoveNode(d)?'#fca5a5':'#cbd5e1';
481
- var iconFill=isRemoveNode(d)?'rgba(252,165,165,0.15)':'rgba(203,213,225,0.15)';
482
- fo.append('xhtml:div').attr('style','width:'+s+'px;height:'+s+'px;display:flex;align-items:center;justify-content:center;')
483
- .html('<i data-lucide="'+layer.icon+'" style="width:'+s+'px;height:'+s+'px;color:'+iconColor+';fill:'+iconFill+';stroke-width:1.5;"></i>');
484
- });
485
-
486
- var pulseEl=nodeEls.filter(function(d){return d.hop===0}).append('circle').attr('r',30).attr('fill','none').attr('stroke',centerColor).attr('stroke-width',1.5).attr('opacity',0.4);
487
- (function pulse(){pulseEl.attr('r',30).attr('opacity',0.4).transition().duration(2200).ease(d3.easeCubicOut).attr('r',55).attr('opacity',0).on('end',pulse);})();
488
-
489
- var labelsOn = true;
490
- var labelEls = nodeEls.append('text').attr('text-anchor','middle').attr('y',function(d){return nR(d)+15})
491
- .attr('font-size',function(d){return d.hop<=1?11:10}).attr('fill',function(d){if(d.hop===0)return centerColor;if(isRemoveNode(d))return'#f87171';return'#94a3b8';}).attr('font-weight',function(d){return d.hop===0?700:500})
492
- .attr('text-decoration',function(d){return isRemoveNode(d)?'line-through':'none';})
493
- .text(function(d){var m=d.hop>=2?16:20;return d.name.length>m?d.name.slice(0,m-1)+'\\u2026':d.name;});
494
-
495
- lucide.createIcons();
496
- M.layers.forEach(function(l){var el=document.getElementById('fpi_'+l.id);if(el){el.innerHTML='<i data-lucide="'+l.icon+'" style="width:12px;height:12px;color:#cbd5e1;fill:rgba(203,213,225,0.15);stroke-width:2;"></i>';}});
497
- lucide.createIcons();
498
-
499
- // \u2500\u2500 State \u2500\u2500
500
- var activeRing = 'all', activeLayer = 'all', activeEdgeAction = 'all', selectedNodeId = null;
501
-
502
- function isNodeVisible(d) {
503
- if(d.hop===0)return true;
504
- return(activeRing==='all'||d.ring===activeRing)&&(activeLayer==='all'||d.layer===activeLayer);
505
- }
506
- function isEdgeActionVisible(e) {
507
- if(activeEdgeAction==='all')return true;
508
- return(e.action||'existing')===activeEdgeAction;
509
- }
510
- function getRelated(nid) {
511
- var s = {};s[nid]=true;
512
- M.edges.forEach(function(e){var src=e.source==='center'?'__center__':e.source;if(src===nid||e.target===nid){s[src]=true;s[e.target]=true;}});
513
- return s;
514
- }
515
- function getConnections(nid) {
516
- var conns=[];
517
- M.edges.forEach(function(e){
518
- var src=e.source==='center'?'__center__':e.source;
519
- if(src===nid){var t=allNodes.find(function(n){return n.id===e.target});if(t)conns.push({dir:'to',node:t,action:e.action,label:e.label});}
520
- if(e.target===nid){var s=allNodes.find(function(n){return n.id===src});if(s)conns.push({dir:'from',node:s,action:e.action,label:e.label});}
521
- });
522
- return conns;
523
- }
524
-
525
- // \u2500\u2500 Hover: highlight edges only (no panel) \u2500\u2500
526
- nodeEls.on('mouseenter',function(event,d){
527
- if(selectedNodeId)return;
528
- var related=d.hop===0?allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}):getRelated(d.id);
529
- nodeEls.transition().duration(150).style('opacity',function(n){
530
- if(!isNodeVisible(n))return 0.06;
531
- return related[n.id]?1:0.12;
532
- });
533
- edgePaths.transition().duration(150)
534
- .attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var sN=allNodes.find(function(n){return n.id===s});var tN=allNodes.find(function(n){return n.id===e.target});if(sN&&!isNodeVisible(sN)&&sN.hop!==0)return 0.02;if(tN&&!isNodeVisible(tN)&&tN.hop!==0)return 0.02;var isRelated=related[s]&&related[e.target];if(!isRelated)return 0.03;return Math.min(edgeStyle(e).opacity+0.25,0.85);})
535
- .attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
536
- .attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+0.5:edgeStyle(e).width;});
537
- d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.15);
538
- });
539
- nodeEls.on('mouseleave',function(){
540
- if(selectedNodeId)return;
541
- applyFilters();
542
- edgePaths.transition().duration(200)
543
- .attr('stroke',function(d){return edgeStyle(d).color;})
544
- .attr('stroke-width',function(d){return edgeStyle(d).width;});
545
- d3.select(this).select('.node-glow').transition().duration(200).attr('opacity',0);
546
- });
547
-
548
- // \u2500\u2500 Click: use pointerdown/pointerup with delta check to avoid zoom interference \u2500\u2500
549
- var ptrDownPos = null;
550
- nodeEls.on('pointerdown',function(event){
551
- ptrDownPos = {x:event.clientX, y:event.clientY};
552
- });
553
- nodeEls.on('pointerup',function(event,d){
554
- if(!ptrDownPos) return;
555
- var dx=event.clientX-ptrDownPos.x, dy=event.clientY-ptrDownPos.y;
556
- ptrDownPos = null;
557
- if(Math.sqrt(dx*dx+dy*dy) > 5) return; // was a drag, not a click
558
-
559
- event.stopPropagation();
560
- selectedNodeId = d.id;
561
-
562
- var related = d.hop===0 ? allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}) : getRelated(d.id);
563
- nodeEls.transition().duration(200).style('opacity',function(n){
564
- if(!isNodeVisible(n))return 0.06;
565
- return related[n.id]?1:0.08;
566
- });
567
- edgePaths.transition().duration(200)
568
- .attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?Math.min(edgeStyle(e).opacity+0.3,0.9):0.03;})
569
- .attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
570
- .attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+1:edgeStyle(e).width;});
571
- nodeEls.select('.node-glow').attr('opacity',0);
572
- d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.25);
573
-
574
- // Fill detail panel
575
- var panel=document.getElementById('detail');
576
- var nameEl=document.getElementById('dpName');
577
- nameEl.textContent=d.name;
578
- nameEl.style.color=d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#e2e8f0');
579
-
580
- var layer=layerMap[d.layer],ring=ringMap[d.ring];
581
- document.getElementById('dpBadges').innerHTML=
582
- (ring?'<span class="dp-badge" style="background:'+ring.color+'22;color:'+ring.color+'">'+ring.name+'</span>':'')+
583
- (d.hop===0?'<span class="dp-badge" style="background:'+centerColor+'22;color:'+centerColor+'">Center</span>':'')+
584
- (layer?'<span class="dp-badge" style="background:'+layer.color+'22;color:'+layer.color+';border:1px solid'+layer.color+'44">'+layer.name+'</span>':'')+
585
- (d.type?'<span class="dp-badge" style="background:rgba(100,116,139,.12);color:#94a3b8">'+d.type+'</span>':'');
586
-
587
- if(d.path){document.getElementById('dpPathSection').style.display='';document.getElementById('dpPath').textContent=d.path;}
588
- else{document.getElementById('dpPathSection').style.display='none';}
589
-
590
- if(d.reason){document.getElementById('dpReasonSection').style.display='';document.getElementById('dpReason').textContent=d.reason;}
591
- else{document.getElementById('dpReasonSection').style.display='none';}
592
-
593
- var accSection=document.getElementById('dpAcceptanceSection');
594
- var accEl=document.getElementById('dpAcceptance');
595
- if(d.acceptance&&d.acceptance.length>0){
596
- accSection.style.display='';
597
- accEl.innerHTML=d.acceptance.map(function(a){return '<li style="color:#10b981;font-size:11px;">\\u2713 '+a+'</li>';}).join('');
598
- } else {accSection.style.display='none';}
599
-
600
- var conns=getConnections(d.id);
601
- var connSection=document.getElementById('dpConnSection');
602
- var connEl=document.getElementById('dpConn');
603
- if(conns.length>0){
604
- connSection.style.display='';
605
- connEl.innerHTML=conns.map(function(c){
606
- var col=c.node.hop===0?centerColor:(ringMap[c.node.ring]?ringMap[c.node.ring].color:'#666');
607
- var arrow=c.dir==='to'?'\\u2192':'\\u2190';
608
- var actionBadge='';
609
- if(c.action&&c.action!=='existing'){
610
- var ac=edgeActionStyles[c.action];
611
- actionBadge=' <span style="font-size:9px;padding:1px 5px;border-radius:3px;background:'+ac.color+'22;color:'+ac.color+';font-weight:600;letter-spacing:.3px">'+c.action.toUpperCase()+'</span>';
612
- }
613
- var labelText=c.label?' <span style="color:#64748b;font-style:italic">'+c.label+'</span>':'';
614
- return '<li><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+col+'"></span> '+arrow+' '+c.node.name+actionBadge+labelText+'</li>';
615
- }).join('');
616
- } else {connSection.style.display='none';}
617
-
618
- panel.style.display='block';
619
- });
620
-
621
- // Close detail on SVG background click
622
- svg.on('pointerup',function(event){
623
- if(!ptrDownPos){closeDetail();}
624
- ptrDownPos=null;
625
- });
626
-
627
- document.getElementById('dpClose').addEventListener('click',function(){closeDetail();});
628
-
629
- function closeDetail(){
630
- selectedNodeId=null;
631
- document.getElementById('detail').style.display='none';
632
- applyFilters();
633
- edgePaths.transition().duration(200)
634
- .attr('stroke',function(d){return edgeStyle(d).color;})
635
- .attr('stroke-width',function(d){return edgeStyle(d).width;});
636
- nodeEls.select('.node-glow').transition().duration(200).attr('opacity',0);
637
- }
638
-
639
- // \u2500\u2500 Filters \u2500\u2500
640
- function applyFilters(){
641
- nodeEls.transition().duration(300).style('opacity',function(d){
642
- if(d.hop===0)return 1;
643
- return isNodeVisible(d)?1:0.06;
644
- });
645
- edgePaths.transition().duration(300)
646
- .attr('opacity',function(e){
647
- if(!isEdgeActionVisible(e))return 0.02;
648
- if(activeRing==='all'&&activeLayer==='all')return edgeStyle(e).opacity;
649
- var srcId=e.source==='center'?'__center__':e.source;
650
- var s=allNodes.find(function(n){return n.id===srcId}),t=allNodes.find(function(n){return n.id===e.target});
651
- if(!s||!t)return 0.02;
652
- return((s.hop===0||isNodeVisible(s))&&(t.hop===0||isNodeVisible(t)))?edgeStyle(e).opacity:0.02;
653
- })
654
- .attr('stroke',function(e){return edgeStyle(e).color;})
655
- .attr('stroke-width',function(e){return edgeStyle(e).width;});
656
- }
657
-
658
- function filterRing(id){
659
- activeRing=id;closeDetail();
660
- document.querySelectorAll('[data-ring]').forEach(function(b){b.classList.remove('active');});
661
- var el=document.getElementById(id==='all'?'rAll':'r_'+id);if(el)el.classList.add('active');
662
- applyFilters();
663
- }
664
- function filterLayer(id){
665
- activeLayer=id;closeDetail();
666
- document.querySelectorAll('[data-layer]').forEach(function(b){b.classList.remove('active');});
667
- var el=document.getElementById(id==='all'?'lAll':'l_'+id);if(el)el.classList.add('active');
668
- applyFilters();
669
- }
670
- function filterEdgeAction(id){
671
- activeEdgeAction=id;closeDetail();
672
- document.querySelectorAll('[data-edge]').forEach(function(b){b.classList.remove('active');});
673
- var el=document.getElementById(id==='all'?'eAll':'e_'+id);if(el)el.classList.add('active');
674
- applyFilters();
675
- }
676
- </script>
677
- </body>
678
- </html>`;
679
- }
680
- function generateBlastRadiusReport(manifest) {
681
- const layerMap = {};
682
- for (const l of manifest.layers) layerMap[l.id] = l.name;
683
- let md = `# Blast Radius: ${manifest.title}
684
- `;
685
- md += `Mode: ${manifest.mode} | Layers: ${manifest.layers.length} | Total nodes: ${manifest.nodes.length}
686
-
687
- `;
688
- md += `## Center
689
- `;
690
- md += `**${manifest.center.name}** \u2014 ${manifest.center.description}
691
-
692
- `;
693
- for (const r of manifest.rings) {
694
- const nodes = manifest.nodes.filter((n) => n.ring === r.id);
695
- if (nodes.length === 0) continue;
696
- md += `## ${r.name} (${nodes.length})
697
-
698
- `;
699
- md += `| Node | Layer | Type | Path | Why |
700
- `;
701
- md += `|------|-------|------|------|-----|
702
- `;
703
- for (const n of nodes) {
704
- const ln = layerMap[n.layer] || n.layer;
705
- const path2 = (n.path || "-").replace(/\|/g, "/");
706
- const reason = (n.reason || "-").replace(/\|/g, "/");
707
- md += `| ${n.name} | ${ln} | ${n.type || "-"} | ${path2} | ${reason} |
708
- `;
709
- }
710
- md += `
711
- `;
712
- }
713
- md += `## Edges
714
-
715
- `;
716
- for (const e of manifest.edges) {
717
- const action = e.action ? ` [${e.action.toUpperCase()}]` : "";
718
- const label = e.label ? ` \u2014 ${e.label}` : "";
719
- md += `- ${e.source} \u2192 ${e.target}${action}${label}
720
- `;
721
- }
722
- return md;
723
- }
724
- function escHtml(s) {
725
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
726
- }
727
- var init_blast_radius_render = __esm({
728
- "src/deck-server/blast-radius-render.ts"() {
729
- "use strict";
730
- }
731
- });
732
-
733
- // src/deck-shared/contract-generator.ts
734
- function generateContract(manifest) {
735
- const layerMap = {};
736
- for (const l of manifest.layers) layerMap[l.id] = l.name;
737
- const safeRingIds = manifest.rings.filter((r) => /safe|unchanged|no.?change/i.test(r.name) || /safe/i.test(r.id)).map((r) => r.id);
738
- const actionableNodes = manifest.nodes.filter((n) => !safeRingIds.includes(n.ring));
739
- const safeNodes = manifest.nodes.filter((n) => safeRingIds.includes(n.ring));
740
- const createRingIds = manifest.rings.filter((r) => /create|new|build/i.test(r.name) || /create/i.test(r.id)).map((r) => r.id);
741
- const items = actionableNodes.map((n, i) => ({
742
- index: i + 1,
743
- id: n.id,
744
- name: n.name,
745
- action: createRingIds.includes(n.ring) ? "CREATE" : "MODIFY",
746
- layer: layerMap[n.layer] || n.layer,
747
- layerId: n.layer,
748
- type: n.type || "unknown",
749
- file: n.path || null,
750
- spec: n.reason || "No specification provided",
751
- acceptance: n.acceptance || [],
752
- dependsOn: manifest.edges.filter((e) => e.target === n.id && e.source !== "center").map((e) => e.source),
753
- blocks: manifest.edges.filter((e) => e.source === n.id).map((e) => e.target),
754
- status: "pending",
755
- disposition: null
756
- }));
757
- const verify = safeNodes.map((n) => ({
758
- id: n.id,
759
- name: n.name,
760
- layer: layerMap[n.layer] || n.layer,
761
- type: n.type || "unknown",
762
- note: "Classified as unchanged \u2014 verify after implementation"
763
- }));
764
- const layerIds = [...new Set(actionableNodes.map((n) => n.layer))];
765
- return {
766
- title: manifest.title,
767
- description: manifest.center.description,
768
- mode: manifest.mode,
769
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
770
- summary: {
771
- createCount: items.filter((i) => i.action === "CREATE").length,
772
- modifyCount: items.filter((i) => i.action === "MODIFY").length,
773
- totalItems: items.length,
774
- verifyCount: verify.length,
775
- layers: layerIds.map((id) => layerMap[id] || id)
776
- },
777
- items,
778
- verify,
779
- edges: manifest.edges
780
- };
781
- }
782
- function contractToMarkdown(contract) {
783
- let md = `# Contract: ${contract.title}
784
- `;
785
- md += `> ${contract.description}
786
-
787
- `;
788
- md += `Generated: ${contract.generatedAt}
789
-
790
- `;
791
- md += `## Summary
792
- `;
793
- md += `- **Create:** ${contract.summary.createCount}
794
- `;
795
- md += `- **Modify:** ${contract.summary.modifyCount}
796
- `;
797
- md += `- **Total items:** ${contract.summary.totalItems}
798
- `;
799
- md += `- **Verify unchanged:** ${contract.summary.verifyCount}
800
- `;
801
- md += `- **Layers:** ${contract.summary.layers.join(", ")}
802
-
803
- `;
804
- const byLayer = /* @__PURE__ */ new Map();
805
- for (const item of contract.items) {
806
- const group = byLayer.get(item.layer) || [];
807
- group.push(item);
808
- byLayer.set(item.layer, group);
809
- }
810
- md += `## Contract Items
811
-
812
- `;
813
- for (const [layer, items] of byLayer) {
814
- md += `### ${layer}
815
-
816
- `;
817
- md += `| # | Action | Name | Type | Spec | Status |
818
- `;
819
- md += `|---|--------|------|------|------|--------|
820
- `;
821
- for (const item of items) {
822
- const spec = item.spec.replace(/\|/g, "/").replace(/\n/g, " ");
823
- md += `| ${item.index} | ${item.action} | ${item.name} | ${item.type} | ${spec} | [ ] ${item.status} |
824
- `;
825
- }
826
- md += `
827
- `;
828
- const withAcceptance = items.filter((i) => i.acceptance.length > 0);
829
- if (withAcceptance.length > 0) {
830
- md += `#### Acceptance Criteria
831
-
832
- `;
833
- for (const item of withAcceptance) {
834
- md += `**${item.index}. ${item.name}**
835
- `;
836
- for (const a of item.acceptance) {
837
- md += `- [ ] ${a}
838
- `;
839
- }
840
- md += `
841
- `;
842
- }
843
- }
844
- }
845
- md += `## Dependencies
846
-
847
- `;
848
- for (const item of contract.items) {
849
- if (item.dependsOn.length > 0) {
850
- md += `- **${item.name}** depends on: ${item.dependsOn.join(", ")}
851
- `;
852
- }
853
- }
854
- md += `
855
- `;
856
- if (contract.verify.length > 0) {
857
- md += `## Verify Unchanged
858
-
859
- `;
860
- md += `These items were classified as "no change." Scan for misclassifications after implementation.
861
-
862
- `;
863
- md += `| Name | Layer | Type | Status |
864
- `;
865
- md += `|------|-------|------|--------|
866
- `;
867
- for (const v of contract.verify) {
868
- md += `| ${v.name} | ${v.layer} | ${v.type} | [ ] verified |
869
- `;
870
- }
871
- md += `
872
- `;
873
- }
874
- md += `## Disposition Legend
875
-
876
- `;
877
- md += `- \`pending\` \u2014 not started
878
- `;
879
- md += `- \`done\` \u2014 implemented and verified against acceptance criteria
880
- `;
881
- md += `- \`deferred\` \u2014 intentionally postponed (must include reason)
882
- `;
883
- md += `- \`cut\` \u2014 removed from scope (must include reason)
884
- `;
885
- md += `- \`superseded\` \u2014 replaced by different approach (must reference replacement)
886
- `;
887
- return md;
888
- }
889
- var init_contract_generator = __esm({
890
- "src/deck-shared/contract-generator.ts"() {
891
- "use strict";
892
- }
893
- });
894
-
895
- // src/deck-server/deck-serve.ts
896
- var deck_serve_exports = {};
897
- __export(deck_serve_exports, {
898
- broadcastToClients: () => broadcastToClients,
899
- consumeRenderError: () => consumeRenderError,
900
- createFeedbackWaiter: () => createFeedbackWaiter,
901
- resolveFeedback: () => resolveFeedback,
902
- runServeCli: () => runServeCli,
903
- startDeckServer: () => startDeckServer
904
- });
905
- function consumeRenderError() {
906
- const err = lastRenderError;
907
- lastRenderError = null;
908
- return err;
909
- }
910
- function createFeedbackWaiter(session) {
911
- const existing = pendingFeedback.get(session);
912
- if (existing) {
913
- existing.reject(new Error("Superseded by new await_feedback call"));
914
- }
915
- let resolve;
916
- let reject;
917
- const promise = new Promise((res, rej) => {
918
- resolve = res;
919
- reject = rej;
920
- });
921
- pendingFeedback.set(session, { resolve, reject });
922
- return promise;
923
- }
924
- function resolveFeedback(session, response) {
925
- const waiter = pendingFeedback.get(session);
926
- if (!waiter) return false;
927
- pendingFeedback.delete(session);
928
- waiter.resolve(response);
929
- return true;
930
- }
931
- function broadcastToClients(message) {
932
- if (!wss) return;
933
- const data = JSON.stringify(message);
934
- for (const client of wss.clients) {
935
- if (client.readyState === import_ws.WebSocket.OPEN) {
936
- client.send(data);
937
- }
938
- }
939
- }
940
- function serveStatic(res, filePath) {
941
- if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) return false;
942
- const ext = import_node_path3.default.extname(filePath).toLowerCase();
943
- const mime = MIME_TYPES[ext] ?? "application/octet-stream";
944
- res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
945
- import_node_fs3.default.createReadStream(filePath).pipe(res);
946
- return true;
947
- }
948
- function serveIndex(res, clientDir) {
949
- const indexPath = import_node_path3.default.join(clientDir, "index.html");
950
- if (!import_node_fs3.default.existsSync(indexPath)) {
951
- res.writeHead(500, { "Content-Type": "text/plain" });
952
- res.end(`LaunchDeck client bundle not found at ${clientDir}. Run 'npm run build:client'.`);
953
- return;
954
- }
955
- serveStatic(res, indexPath);
956
- }
957
- function readBody(req) {
958
- return new Promise((resolve) => {
959
- let body = "";
960
- req.on("data", (chunk) => {
961
- body += chunk.toString();
962
- });
963
- req.on("end", () => resolve(body));
964
- });
965
- }
966
- function jsonResponse(res, status, data) {
967
- res.writeHead(status, { "Content-Type": "application/json" });
968
- res.end(JSON.stringify(data));
969
- }
970
- function tryListen(server, port) {
971
- return new Promise((resolve, reject) => {
972
- const onError = (err) => {
973
- server.off("listening", onListening);
974
- reject(err);
975
- };
976
- const onListening = () => {
977
- server.off("error", onError);
978
- resolve(port);
979
- };
980
- server.once("error", onError);
981
- server.once("listening", onListening);
982
- server.listen(port, "127.0.0.1");
983
- });
984
- }
985
- async function bindWithFallback(server, startPort) {
986
- let lastErr = null;
987
- for (let i = 0; i < MAX_PORT_SCAN; i++) {
988
- const port = startPort + i;
989
- try {
990
- return await tryListen(server, port);
991
- } catch (err) {
992
- const code = err.code;
993
- if (code === "EADDRINUSE") {
994
- lastErr = err;
995
- continue;
996
- }
997
- throw err;
998
- }
999
- }
1000
- throw lastErr ?? new Error("Failed to bind any port");
1001
- }
1002
- async function startDeckServer(opts = {}) {
1003
- const cwd = opts.cwd ?? process.cwd();
1004
- const existing = getLiveLock(cwd);
1005
- if (existing) {
1006
- if (!opts.quiet) {
1007
- process.stderr.write(`[launch-deck] already running (pid ${existing.pid}) at ${existing.url}
1008
- `);
1009
- }
1010
- return { port: existing.port, url: existing.url };
1011
- }
1012
- const clientDir = opts.clientDir ?? import_node_path3.default.join(__dirname, "..", "deck-client");
1013
- const server = import_node_http.default.createServer(async (req, res) => {
1014
- try {
1015
- const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
1016
- res.setHeader("Access-Control-Allow-Origin", "*");
1017
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1018
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1019
- if (req.method === "OPTIONS") {
1020
- res.writeHead(204);
1021
- res.end();
1022
- return;
1023
- }
1024
- if (req.method === "GET" && url2.pathname === "/api/health") {
1025
- jsonResponse(res, 200, { ok: true, tool: "launch-deck" });
1026
- return;
1027
- }
1028
- if (req.method === "GET" && url2.pathname === "/api/deck/config") {
1029
- const cfg = loadDeckConfig(cwd);
1030
- jsonResponse(res, 200, { config: cfg });
1031
- return;
1032
- }
1033
- if (req.method === "POST" && url2.pathname === "/api/deck/config") {
1034
- try {
1035
- const body = JSON.parse(await readBody(req));
1036
- const existing2 = loadDeckConfig(cwd);
1037
- const merged = { ...existing2, ...body };
1038
- saveDeckConfig(cwd, merged);
1039
- jsonResponse(res, 200, { ok: true });
1040
- } catch (err) {
1041
- jsonResponse(res, 400, { ok: false, error: String(err) });
1042
- }
1043
- return;
1044
- }
1045
- if (req.method === "POST" && url2.pathname === "/api/deck") {
1046
- const body = JSON.parse(await readBody(req));
1047
- for (const block of body.blocks) {
1048
- if (block.type === "blast-radius" && block.manifest) {
1049
- try {
1050
- const sessionEncoded = encodeURIComponent(body.session);
1051
- const baseUrl = `/deck-files/${sessionEncoded}`;
1052
- const html = generateBlastRadiusHtml(block.manifest, baseUrl);
1053
- const report = generateBlastRadiusReport(block.manifest);
1054
- const dir = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", body.session);
1055
- import_node_fs3.default.mkdirSync(dir, { recursive: true });
1056
- import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius.html"), html);
1057
- import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-report.md"), report);
1058
- import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
1059
- const contractData = generateContract(block.manifest);
1060
- const contractMd = contractToMarkdown(contractData);
1061
- import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.json"), JSON.stringify(contractData, null, 2));
1062
- import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.md"), contractMd);
1063
- block.type = "iframe";
1064
- block.src = `${baseUrl}/blast-radius.html`;
1065
- delete block.manifest;
1066
- } catch (err) {
1067
- console.error("Failed to generate blast radius HTML:", err);
1068
- }
1069
- }
1070
- }
1071
- broadcastToClients({
1072
- type: "session",
1073
- session: body.session,
1074
- mode: body.mode,
1075
- blocks: body.blocks,
1076
- prompt: body.prompt
1077
- });
1078
- jsonResponse(res, 200, { ok: true });
1079
- return;
1080
- }
1081
- if (req.method === "POST" && url2.pathname === "/api/await-feedback") {
1082
- const { session } = JSON.parse(await readBody(req));
1083
- try {
1084
- const feedback = await createFeedbackWaiter(session);
1085
- jsonResponse(res, 200, feedback);
1086
- } catch (err) {
1087
- jsonResponse(res, 500, { error: String(err) });
1088
- }
1089
- return;
1090
- }
1091
- if (req.method === "POST" && url2.pathname === "/api/clear-session") {
1092
- const { session } = JSON.parse(await readBody(req));
1093
- broadcastToClients({
1094
- type: session === "all" ? "clear_all" : "clear_session",
1095
- session
1096
- });
1097
- if (session === "all") {
1098
- for (const [id, waiter] of pendingFeedback) {
1099
- waiter.resolve({ comment: "", closed: true });
1100
- pendingFeedback.delete(id);
1101
- }
1102
- } else {
1103
- resolveFeedback(session, { comment: "", closed: true });
1104
- }
1105
- try {
1106
- const deckFilesBase = import_node_path3.default.join(cwd, ".launchsecure", "deck-files");
1107
- if (session === "all") {
1108
- if (import_node_fs3.default.existsSync(deckFilesBase)) {
1109
- import_node_fs3.default.rmSync(deckFilesBase, { recursive: true, force: true });
1110
- }
1111
- } else {
1112
- const sessionDir = import_node_path3.default.join(deckFilesBase, session);
1113
- if (import_node_fs3.default.existsSync(sessionDir)) {
1114
- import_node_fs3.default.rmSync(sessionDir, { recursive: true, force: true });
1115
- }
1116
- }
1117
- } catch {
1118
- }
1119
- jsonResponse(res, 200, { ok: true });
1120
- return;
1121
- }
1122
- if (req.method === "GET" && url2.pathname === "/api/render-errors") {
1123
- const err = consumeRenderError();
1124
- jsonResponse(res, 200, err ?? null);
1125
- return;
1126
- }
1127
- if (req.method === "GET" && url2.pathname.startsWith("/deck-files/")) {
1128
- const relative = decodeURIComponent(url2.pathname.slice("/deck-files/".length));
1129
- if (relative.includes("..") || relative.startsWith("/")) {
1130
- res.writeHead(403);
1131
- res.end("Forbidden");
1132
- return;
1133
- }
1134
- const filePath = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", relative);
1135
- if (serveStatic(res, filePath)) return;
1136
- res.writeHead(404);
1137
- res.end("Not found");
1138
- return;
1139
- }
1140
- if (req.method === "GET" && url2.pathname.startsWith("/deck-download/")) {
1141
- const relative = decodeURIComponent(url2.pathname.slice("/deck-download/".length));
1142
- if (relative.includes("..") || relative.startsWith("/")) {
1143
- res.writeHead(403);
1144
- res.end("Forbidden");
1145
- return;
1146
- }
1147
- const filePath = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", relative);
1148
- if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) {
1149
- res.writeHead(404);
1150
- res.end("Not found");
1151
- return;
1152
- }
1153
- const ext = import_node_path3.default.extname(filePath).toLowerCase();
1154
- const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1155
- const filename = import_node_path3.default.basename(filePath);
1156
- res.writeHead(200, {
1157
- "Content-Type": mime,
1158
- "Content-Disposition": `attachment; filename="${filename}"`,
1159
- "Cache-Control": "no-cache"
1160
- });
1161
- import_node_fs3.default.createReadStream(filePath).pipe(res);
1162
- return;
1163
- }
1164
- if (url2.pathname !== "/") {
1165
- const staticPath = import_node_path3.default.join(clientDir, url2.pathname);
1166
- if (serveStatic(res, staticPath)) return;
1167
- }
1168
- serveIndex(res, clientDir);
1169
- } catch (err) {
1170
- jsonResponse(res, 500, { error: String(err) });
1171
- }
1172
- });
1173
- wss = new import_ws.WebSocketServer({ server });
1174
- wss.on("connection", (ws) => {
1175
- if (!opts.quiet) {
1176
- process.stderr.write("[launch-deck] browser connected\n");
1177
- }
1178
- ws.on("message", (raw) => {
1179
- try {
1180
- const msg = JSON.parse(String(raw));
1181
- if (msg.type === "feedback" && msg.session) {
1182
- resolveFeedback(msg.session, {
1183
- comment: msg.comment ?? "",
1184
- selections: msg.selections
1185
- });
1186
- }
1187
- if (msg.type === "session_closed" && msg.session) {
1188
- resolveFeedback(msg.session, {
1189
- comment: "",
1190
- closed: true
1191
- });
1192
- }
1193
- if (msg.type === "render_error") {
1194
- lastRenderError = {
1195
- error: msg.error ?? "Unknown render error",
1196
- source: msg.source ?? ""
1197
- };
1198
- }
1199
- } catch {
1200
- }
1201
- });
1202
- ws.on("close", () => {
1203
- if (!opts.quiet) {
1204
- process.stderr.write("[launch-deck] browser disconnected\n");
1205
- }
1206
- const hasClients = [...wss.clients].some((c) => c.readyState === import_ws.WebSocket.OPEN);
1207
- if (!hasClients && pendingFeedback.size > 0) {
1208
- for (const [id, waiter] of pendingFeedback) {
1209
- waiter.reject(new Error("Browser disconnected \u2014 no clients available"));
1210
- pendingFeedback.delete(id);
1211
- }
1212
- }
1213
- });
1214
- });
1215
- const config = loadDeckConfig(cwd);
1216
- const startPort = opts.port ?? config.port ?? DEFAULT_PORT;
1217
- const port = await bindWithFallback(server, startPort);
1218
- const url = `http://localhost:${port}`;
1219
- writeLock({
1220
- pid: process.pid,
1221
- port,
1222
- cwd,
1223
- url,
1224
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1225
- }, cwd);
1226
- const cleanup = () => {
1227
- for (const [id, waiter] of pendingFeedback) {
1228
- waiter.reject(new Error("LaunchDeck server shutting down"));
1229
- pendingFeedback.delete(id);
1230
- }
1231
- try {
1232
- const deckFilesBase = import_node_path3.default.join(cwd, ".launchsecure", "deck-files");
1233
- if (import_node_fs3.default.existsSync(deckFilesBase)) {
1234
- import_node_fs3.default.rmSync(deckFilesBase, { recursive: true, force: true });
1235
- }
1236
- } catch {
1237
- }
1238
- clearLock(cwd);
1239
- server.close();
1240
- };
1241
- process.once("SIGINT", () => {
1242
- cleanup();
1243
- process.exit(0);
1244
- });
1245
- process.once("SIGTERM", () => {
1246
- cleanup();
1247
- process.exit(0);
1248
- });
1249
- process.once("exit", cleanup);
1250
- if (!opts.quiet) {
1251
- process.stderr.write(`[launch-deck] serving ${url}
1252
- `);
1253
- }
1254
- return { port, url };
1255
- }
1256
- function runServeCli(argv) {
1257
- let port;
1258
- for (let i = 0; i < argv.length; i++) {
1259
- if (argv[i] === "--port" && argv[i + 1]) {
1260
- port = parseInt(argv[++i], 10);
1261
- } else if (argv[i].startsWith("--port=")) {
1262
- port = parseInt(argv[i].slice("--port=".length), 10);
1263
- }
1264
- }
1265
- startDeckServer({ port }).catch((err) => {
1266
- process.stderr.write(`[launch-deck] failed to start: ${err}
1267
- `);
1268
- process.exit(1);
1269
- });
1270
- }
1271
- var import_node_http, import_node_fs3, import_node_path3, import_ws, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES, pendingFeedback, lastRenderError, wss;
1272
- var init_deck_serve = __esm({
1273
- "src/deck-server/deck-serve.ts"() {
1274
- "use strict";
1275
- import_node_http = __toESM(require("node:http"));
1276
- import_node_fs3 = __toESM(require("node:fs"));
1277
- import_node_path3 = __toESM(require("node:path"));
1278
- import_ws = require("ws");
1279
- init_lockfile();
1280
- init_config();
1281
- init_blast_radius_render();
1282
- init_contract_generator();
1283
- DEFAULT_PORT = 52829;
1284
- MAX_PORT_SCAN = 3;
1285
- MIME_TYPES = {
1286
- ".html": "text/html; charset=utf-8",
1287
- ".js": "application/javascript; charset=utf-8",
1288
- ".css": "text/css; charset=utf-8",
1289
- ".json": "application/json; charset=utf-8",
1290
- ".png": "image/png",
1291
- ".svg": "image/svg+xml",
1292
- ".ico": "image/x-icon",
1293
- ".woff": "font/woff",
1294
- ".woff2": "font/woff2"
1295
- };
1296
- pendingFeedback = /* @__PURE__ */ new Map();
1297
- lastRenderError = null;
1298
- wss = null;
1299
- }
1300
- });
1301
-
1302
- // src/deck-server/deck-mcp.ts
1303
- var deck_mcp_exports = {};
1304
- __export(deck_mcp_exports, {
1305
- startDeckMcpServer: () => startDeckMcpServer
1306
- });
1307
- function httpGet(url) {
1308
- return new Promise((resolve, reject) => {
1309
- const parsed = new URL(url);
1310
- const req = import_node_http2.default.request(
1311
- { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "GET" },
1312
- (res) => {
1313
- let buf = "";
1314
- res.on("data", (chunk) => {
1315
- buf += chunk;
1316
- });
1317
- res.on("end", () => resolve(buf));
1318
- }
1319
- );
1320
- req.on("error", reject);
1321
- req.end();
1322
- });
1323
- }
1324
- function httpPost(url, body) {
1325
- return new Promise((resolve, reject) => {
1326
- const data = JSON.stringify(body);
1327
- const parsed = new URL(url);
1328
- const req = import_node_http2.default.request(
1329
- {
1330
- hostname: parsed.hostname,
1331
- port: parsed.port,
1332
- path: parsed.pathname,
1333
- method: "POST",
1334
- headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) }
1335
- },
1336
- (res) => {
1337
- let buf = "";
1338
- res.on("data", (chunk) => {
1339
- buf += chunk;
1340
- });
1341
- res.on("end", () => resolve(buf));
1342
- }
1343
- );
1344
- req.on("error", reject);
1345
- req.write(data);
1346
- req.end();
1347
- });
1348
- }
1349
- async function handleTool(name, args) {
1350
- const projectRoot = process.cwd();
1351
- switch (name) {
1352
- case "deck": {
1353
- const session = args.session;
1354
- const mode = args.mode;
1355
- const blocks = args.blocks;
1356
- const prompt = args.prompt ?? void 0;
1357
- const lock = getLiveLock(projectRoot);
1358
- if (!lock) {
1359
- return text("LaunchDeck server is not running. Call start_server first.");
1360
- }
1361
- for (const block of blocks) {
1362
- if (block.type === "blast-radius" && block.manifest) {
1363
- try {
1364
- const sessionEncoded = encodeURIComponent(session);
1365
- const baseUrl = `/deck-files/${sessionEncoded}`;
1366
- const html = generateBlastRadiusHtml(block.manifest, baseUrl);
1367
- const report = generateBlastRadiusReport(block.manifest);
1368
- const dir = (0, import_node_path4.join)(projectRoot, ".launchsecure", "deck-files", session);
1369
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
1370
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "blast-radius.html"), html);
1371
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "blast-radius-report.md"), report);
1372
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
1373
- const contractData = generateContract(block.manifest);
1374
- const contractMd = contractToMarkdown(contractData);
1375
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "contract.json"), JSON.stringify(contractData, null, 2));
1376
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "contract.md"), contractMd);
1377
- block.type = "iframe";
1378
- block.src = `${baseUrl}/blast-radius.html`;
1379
- delete block.manifest;
1380
- } catch (err) {
1381
- return text(JSON.stringify({ error: `Failed to generate blast radius HTML: ${err}` }));
1382
- }
1383
- }
1384
- }
1385
- try {
1386
- await httpPost(`${lock.url}/api/deck`, { session, mode, blocks, prompt });
1387
- } catch (err) {
1388
- return text(JSON.stringify({ error: `Failed to push to browser: ${err}` }));
1389
- }
1390
- const types = blocks.map((b) => b.type).join(", ");
1391
- const hasMermaid = blocks.some((b) => b.type === "mermaid");
1392
- if (hasMermaid) {
1393
- await new Promise((r) => setTimeout(r, 1500));
1394
- try {
1395
- const errResponse = await httpGet(`${lock.url}/api/render-errors`);
1396
- const renderErr = JSON.parse(errResponse);
1397
- if (renderErr) {
1398
- return text(JSON.stringify({
1399
- session,
1400
- ok: false,
1401
- mermaid_error: renderErr.error,
1402
- failed_source: renderErr.source,
1403
- hint: "Fix the mermaid syntax and call deck again."
1404
- }));
1405
- }
1406
- } catch {
1407
- }
1408
- }
1409
- const result = {
1410
- session,
1411
- ok: true,
1412
- mode,
1413
- delivered: blocks.map((b) => b.label).join(", "),
1414
- blocks: blocks.length,
1415
- types,
1416
- url: lock.url
1417
- };
1418
- if (mode === "feedback") {
1419
- result.action = "Call await_feedback with this session ID to get user response.";
1420
- }
1421
- return text(JSON.stringify(result));
1422
- }
1423
- case "await_feedback": {
1424
- const session = args.session;
1425
- const lock = getLiveLock(projectRoot);
1426
- if (!lock) {
1427
- return text("LaunchDeck server is not running.");
1428
- }
1429
- try {
1430
- const response = await httpPost(`${lock.url}/api/await-feedback`, { session });
1431
- try {
1432
- const parsed = JSON.parse(response);
1433
- parsed.session = session;
1434
- return text(JSON.stringify(parsed));
1435
- } catch {
1436
- return text(response);
1437
- }
1438
- } catch (err) {
1439
- return text(JSON.stringify({
1440
- session,
1441
- error: "LaunchDeck server disconnected while waiting for feedback.",
1442
- hint: "The server may have been restarted. Push content again with deck tool."
1443
- }));
1444
- }
1445
- }
1446
- case "clear_session": {
1447
- const session = args.session;
1448
- const lock = getLiveLock(projectRoot);
1449
- if (!lock) {
1450
- return text(JSON.stringify({ session, ok: true }));
1451
- }
1452
- await httpPost(`${lock.url}/api/clear-session`, { session });
1453
- return text(JSON.stringify({ session, cleared: true }));
1454
- }
1455
- case "start_server": {
1456
- const existing = getLiveLock(projectRoot);
1457
- if (existing) {
1458
- return text(JSON.stringify({
1459
- running: true,
1460
- url: existing.url,
1461
- port: existing.port,
1462
- pid: existing.pid,
1463
- message: "LaunchDeck is already running."
1464
- }));
1465
- }
1466
- try {
1467
- const logDir = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".launchsecure");
1468
- (0, import_node_fs5.mkdirSync)(logDir, { recursive: true });
1469
- const logPath = (0, import_node_path4.join)(logDir, "launch-deck.log");
1470
- const out = (0, import_node_fs5.openSync)(logPath, "a");
1471
- const err = (0, import_node_fs5.openSync)(logPath, "a");
1472
- const entryPath = process.argv[1];
1473
- const config = loadDeckConfig(projectRoot);
1474
- const resolvedPort = args.port ?? config.port;
1475
- const portArg = resolvedPort ? ["--port", String(resolvedPort)] : [];
1476
- const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArg], {
1477
- detached: true,
1478
- stdio: ["ignore", out, err],
1479
- env: { ...process.env }
1480
- });
1481
- child.unref();
1482
- await new Promise((r) => setTimeout(r, 1e3));
1483
- const lock = getLiveLock(projectRoot);
1484
- if (lock) {
1485
- return text(JSON.stringify({
1486
- running: true,
1487
- url: lock.url,
1488
- port: lock.port,
1489
- pid: lock.pid
1490
- }));
1491
- }
1492
- return text(JSON.stringify({
1493
- running: false,
1494
- message: `Server spawned (pid ${child.pid}) but lock not yet written. Check ~/.launchsecure/launch-deck.log`
1495
- }));
1496
- } catch (err) {
1497
- return text(JSON.stringify({ running: false, error: String(err) }));
1498
- }
1499
- }
1500
- case "stop_server": {
1501
- const lock = getLiveLock(projectRoot);
1502
- if (!lock) {
1503
- return text(JSON.stringify({ ok: true, message: "No server running." }));
1504
- }
1505
- try {
1506
- process.kill(lock.pid, "SIGTERM");
1507
- } catch {
1508
- }
1509
- clearLock(projectRoot);
1510
- return text(JSON.stringify({ ok: true, message: `Stopped server (pid ${lock.pid}).` }));
1511
- }
1512
- case "server_status": {
1513
- const lock = getLiveLock(projectRoot);
1514
- if (lock) {
1515
- return text(JSON.stringify({
1516
- running: true,
1517
- url: lock.url,
1518
- port: lock.port,
1519
- pid: lock.pid,
1520
- startedAt: lock.startedAt
1521
- }));
1522
- }
1523
- return text(JSON.stringify({ running: false }));
1524
- }
1525
- case "generate_contract": {
1526
- const session = args.session;
1527
- const dir = (0, import_node_path4.join)(projectRoot, ".launchsecure", "deck-files", session);
1528
- const manifestPath = (0, import_node_path4.join)(dir, "blast-radius-manifest.json");
1529
- if (!import_node_fs4.default.existsSync(manifestPath)) {
1530
- return text(JSON.stringify({
1531
- error: `No blast radius manifest found for session "${session}". Push a blast-radius block first.`,
1532
- hint: "Use the deck tool with a blast-radius block type to create a session."
1533
- }));
1534
- }
1535
- try {
1536
- const raw = import_node_fs4.default.readFileSync(manifestPath, "utf-8");
1537
- const manifest = JSON.parse(raw);
1538
- const contract = generateContract(manifest);
1539
- const markdown = contractToMarkdown(contract);
1540
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "contract.json"), JSON.stringify(contract, null, 2));
1541
- import_node_fs4.default.writeFileSync((0, import_node_path4.join)(dir, "contract.md"), markdown);
1542
- return text(JSON.stringify(contract));
1543
- } catch (err) {
1544
- return text(JSON.stringify({ error: `Failed to generate contract: ${err}` }));
1545
- }
1546
- }
1547
- default:
1548
- return text(`Unknown tool: ${name}`);
1549
- }
1550
- }
1551
- function text(t) {
1552
- return { content: [{ type: "text", text: t }] };
1553
- }
1554
- function send(msg) {
1555
- process.stdout.write(JSON.stringify(msg) + "\n");
1556
- }
1557
- function sendResponse(id, result) {
1558
- process.stderr.write(`[DEBUG-RES] id=${id} result=${JSON.stringify(result).slice(0, 300)}
1559
- `);
1560
- send({ jsonrpc: "2.0", id, result });
1561
- }
1562
- function sendError(id, code, message) {
1563
- send({ jsonrpc: "2.0", id, error: { code, message } });
1564
- }
1565
- async function handleMessage(parsed) {
1566
- const id = parsed.id;
1567
- const method = parsed.method;
1568
- const params = parsed.params ?? {};
1569
- process.stderr.write(`
1570
- [DEBUG-REQ] id=${id} method=${method} params=${JSON.stringify(params).slice(0, 300)}
1571
- `);
1572
- switch (method) {
1573
- case "initialize":
1574
- sendResponse(id, {
1575
- protocolVersion: "2024-11-05",
1576
- capabilities: { tools: {} },
1577
- serverInfo: SERVER_INFO
1578
- });
1579
- break;
1580
- case "notifications/initialized":
1581
- break;
1582
- case "tools/list":
1583
- sendResponse(id, { tools: TOOLS });
1584
- break;
1585
- case "tools/call": {
1586
- const toolName = params.name;
1587
- const toolArgs = params.arguments ?? {};
1588
- try {
1589
- const result = await handleTool(toolName, toolArgs);
1590
- sendResponse(id, result);
1591
- } catch (err) {
1592
- sendError(id, -32603, `Tool error: ${err}`);
1593
- }
1594
- break;
1595
- }
1596
- case "ping":
1597
- sendResponse(id, {});
1598
- break;
1599
- default:
1600
- if (id !== void 0) {
1601
- sendError(id, -32601, `Method not found: ${method}`);
1602
- }
1603
- }
1604
- }
1605
- function startDeckMcpServer() {
1606
- process.stderr.write("[launch-deck] MCP server starting on stdio\n");
1607
- process.stdin.setEncoding("utf-8");
1608
- let buffer = "";
1609
- process.stdin.on("data", (chunk) => {
1610
- buffer += chunk;
1611
- const lines = buffer.split("\n");
1612
- buffer = lines.pop() || "";
1613
- for (const line of lines) {
1614
- const trimmed = line.trim();
1615
- if (!trimmed) continue;
1616
- try {
1617
- handleMessage(JSON.parse(trimmed)).catch((err) => {
1618
- process.stderr.write(`[launch-deck] message error: ${err}
1619
- `);
1620
- });
1621
- } catch (err) {
1622
- process.stderr.write(`[launch-deck] parse error: ${err}
1623
- `);
1624
- }
1625
- }
1626
- });
1627
- process.stdin.on("end", () => {
1628
- process.stderr.write("[launch-deck] stdin closed, exiting\n");
1629
- process.exit(0);
1630
- });
1631
- }
1632
- var import_node_http2, import_node_fs4, import_node_path4, import_node_child_process2, import_node_fs5, import_node_os2, SERVER_INFO, TOOLS;
1633
- var init_deck_mcp = __esm({
1634
- "src/deck-server/deck-mcp.ts"() {
1635
- "use strict";
1636
- import_node_http2 = __toESM(require("node:http"));
1637
- import_node_fs4 = __toESM(require("node:fs"));
1638
- import_node_path4 = require("node:path");
1639
- import_node_child_process2 = require("node:child_process");
1640
- import_node_fs5 = require("node:fs");
1641
- import_node_os2 = require("node:os");
1642
- init_lockfile();
1643
- init_config();
1644
- init_blast_radius_render();
1645
- init_contract_generator();
1646
- SERVER_INFO = {
1647
- name: "launch-deck",
1648
- version: "0.0.3"
1649
- };
1650
- TOOLS = [
1651
- {
1652
- name: "deck",
1653
- description: 'Push visual blocks (HTML, Mermaid diagrams, options, markdown) to the LaunchDeck browser.\n\nEach call creates a named session tab in the browser. Tabs accumulate \u2014 previous sessions are preserved.\n\nModes:\n- "show": Display content and return immediately. Terminal confirms delivery. No user feedback.\n- "feedback": Display content with an input bar for the user to respond. Returns immediately with session info. Call `await_feedback` with the same session ID to block until the user responds.\n\nBlock types:\n- html: Raw HTML/CSS (rendered in a sandboxed iframe)\n- mermaid: Mermaid diagram DSL (flowchart, sequence, ERD, etc.)\n- markdown: Rendered markdown text\n- options: Selection cards \u2014 user picks one or more\n- blast-radius: Interactive D3 radial graph for blast radius analysis. Requires `manifest` field.\n\nAfter calling this tool, the user should check the LaunchDeck browser tab to view content.',
1654
- inputSchema: {
1655
- type: "object",
1656
- properties: {
1657
- session: {
1658
- type: "string",
1659
- description: "Session ID \u2014 becomes the tab label in the browser. Must be unique across active sessions."
1660
- },
1661
- mode: {
1662
- type: "string",
1663
- enum: ["show", "feedback"],
1664
- description: '"show" = display only, returns immediately. "feedback" = display + input bar for user response.'
1665
- },
1666
- blocks: {
1667
- type: "array",
1668
- description: "Array of content blocks to display",
1669
- items: {
1670
- type: "object",
1671
- properties: {
1672
- type: { type: "string", enum: ["html", "mermaid", "options", "markdown", "blast-radius"] },
1673
- label: { type: "string", description: "Label for this block (shown as sub-tab if multiple blocks)" },
1674
- content: { type: "string", description: "Content string (HTML, Mermaid DSL, Markdown)" },
1675
- manifest: {
1676
- type: "object",
1677
- description: 'For blast-radius type: BlastRadiusManifest object. Generates an interactive D3 radial graph.\n\nRequired fields:\n- mode: "structural" (file-level, rings = hops) or "feature" (rings = modify/create/ripple)\n- title: string \u2014 shown in header (e.g. "lib/permissions/types.ts" or "Channels for Comms")\n- layers: array of { id, name, icon, color } \u2014 defines the project layers (e.g. db/api/ui). icon = Lucide icon name, color = hex fill color\n- rings: array of { id, name, color } \u2014 defines concentric rings. For structural: [{id:"hop1",name:"Direct",color:"#f97316"},{id:"hop2",name:"Indirect",color:"#eab308"}]. For feature: [{id:"modify",name:"Modify",color:"#f97316"},{id:"create",name:"Create",color:"#22c55e"},{id:"ripple",name:"Ripple",color:"#eab308"}]\n- center: { name, description } \u2014 the center node label and tooltip\n- nodes: array of { id, name, layer, ring, path?, reason?, type? } \u2014 layer refs layers[].id, ring refs rings[].id\n- edges: array of { source, target } \u2014 source/target are node ids, use "center" for edges from the center node\n\nOptional: subtitle (shown above title)\n\nExample layers: [{id:"db",name:"Database",icon:"database",color:"#172554"},{id:"api",name:"API",icon:"server",color:"#1e3a5f"},{id:"ui",name:"UI",icon:"layout-dashboard",color:"#0c4a6e"}]'
1678
- },
1679
- options: {
1680
- type: "array",
1681
- description: "For options type: array of selectable items",
1682
- items: {
1683
- type: "object",
1684
- properties: {
1685
- id: { type: "string" },
1686
- label: { type: "string" },
1687
- description: { type: "string" },
1688
- preview: { type: "string" }
1689
- },
1690
- required: ["id", "label"]
1691
- }
1692
- }
1693
- },
1694
- required: ["type", "label"]
1695
- }
1696
- },
1697
- prompt: {
1698
- type: "string",
1699
- description: 'Prompt shown above the input bar in feedback mode (e.g. "Which layout do you prefer?")'
1700
- }
1701
- },
1702
- required: ["session", "mode", "blocks"]
1703
- }
1704
- },
1705
- {
1706
- name: "await_feedback",
1707
- description: "Block until the user submits feedback for a specific session in the LaunchDeck browser.\nThe user must open the LaunchDeck browser tab, review the content, and click Done.\nReturns the user's comment and any option selections.\n\nIf the user closes the session tab, this returns with closed: true.",
1708
- inputSchema: {
1709
- type: "object",
1710
- properties: {
1711
- session: {
1712
- type: "string",
1713
- description: "Session ID to wait for feedback on (must match a previous deck call with mode: feedback)"
1714
- }
1715
- },
1716
- required: ["session"]
1717
- }
1718
- },
1719
- {
1720
- name: "clear_session",
1721
- description: 'Remove a session tab from the LaunchDeck browser. Pass session: "all" to clear everything.',
1722
- inputSchema: {
1723
- type: "object",
1724
- properties: {
1725
- session: {
1726
- type: "string",
1727
- description: 'Session ID to remove, or "all" to clear all sessions.'
1728
- }
1729
- },
1730
- required: ["session"]
1731
- }
1732
- },
1733
- {
1734
- name: "start_server",
1735
- description: "Start the LaunchDeck browser UI server. Opens a local HTTP + WebSocket server that serves the visual playground. Returns the URL to open in a browser.",
1736
- inputSchema: {
1737
- type: "object",
1738
- properties: {
1739
- port: { type: "number", description: "Port to bind (default 52829)" }
1740
- }
1741
- }
1742
- },
1743
- {
1744
- name: "stop_server",
1745
- description: "Stop the running LaunchDeck server.",
1746
- inputSchema: { type: "object", properties: {} }
1747
- },
1748
- {
1749
- name: "server_status",
1750
- description: "Check whether the LaunchDeck UI server is running.",
1751
- inputSchema: { type: "object", properties: {} }
1752
- },
1753
- {
1754
- name: "generate_contract",
1755
- description: "Generate a contract from a previously pushed blast-radius session.\n\nReads the blast radius manifest from the session's saved files and produces a structured contract with actionable items, acceptance criteria, dependencies, and a verify-unchanged checklist.\n\nReturns JSON contract. Also saves contract.json and contract.md to the session directory.",
1756
- inputSchema: {
1757
- type: "object",
1758
- properties: {
1759
- session: {
1760
- type: "string",
1761
- description: "Session ID of the blast-radius session to generate a contract from."
1762
- }
1763
- },
1764
- required: ["session"]
1765
- }
1766
- }
1767
- ];
1768
- }
1769
- });
1770
-
1771
- // src/deck-server/deck-mcp-entry.ts
1772
- init_lockfile();
1773
- async function main() {
1774
- setProjectRoot(process.cwd());
1775
- const argv = process.argv.slice(2);
1776
- const subcommand = argv[0];
1777
- if (subcommand === "serve") {
1778
- const { runServeCli: runServeCli2 } = await Promise.resolve().then(() => (init_deck_serve(), deck_serve_exports));
1779
- runServeCli2(argv.slice(1));
1780
- return;
1781
- }
1782
- const { startDeckMcpServer: startDeckMcpServer2 } = await Promise.resolve().then(() => (init_deck_mcp(), deck_mcp_exports));
1783
- startDeckMcpServer2();
1784
- }
1785
- main().catch((err) => {
1786
- process.stderr.write(`[launch-deck] fatal: ${err}
1787
- `);
1788
- process.exit(1);
1789
- });