archbyte 0.1.0

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 (142) hide show
  1. package/README.md +282 -0
  2. package/bin/archbyte.js +213 -0
  3. package/dist/agents/core/component-detector.d.ts +2 -0
  4. package/dist/agents/core/component-detector.js +57 -0
  5. package/dist/agents/core/connection-mapper.d.ts +2 -0
  6. package/dist/agents/core/connection-mapper.js +77 -0
  7. package/dist/agents/core/doc-parser.d.ts +2 -0
  8. package/dist/agents/core/doc-parser.js +64 -0
  9. package/dist/agents/core/env-detector.d.ts +2 -0
  10. package/dist/agents/core/env-detector.js +51 -0
  11. package/dist/agents/core/event-detector.d.ts +2 -0
  12. package/dist/agents/core/event-detector.js +59 -0
  13. package/dist/agents/core/infra-analyzer.d.ts +2 -0
  14. package/dist/agents/core/infra-analyzer.js +72 -0
  15. package/dist/agents/core/structure-scanner.d.ts +2 -0
  16. package/dist/agents/core/structure-scanner.js +55 -0
  17. package/dist/agents/core/validator.d.ts +2 -0
  18. package/dist/agents/core/validator.js +74 -0
  19. package/dist/agents/index.d.ts +24 -0
  20. package/dist/agents/index.js +73 -0
  21. package/dist/agents/llm/index.d.ts +8 -0
  22. package/dist/agents/llm/index.js +185 -0
  23. package/dist/agents/llm/prompt-builder.d.ts +3 -0
  24. package/dist/agents/llm/prompt-builder.js +251 -0
  25. package/dist/agents/llm/response-parser.d.ts +6 -0
  26. package/dist/agents/llm/response-parser.js +174 -0
  27. package/dist/agents/llm/types.d.ts +31 -0
  28. package/dist/agents/llm/types.js +2 -0
  29. package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
  30. package/dist/agents/pipeline/agents/component-identifier.js +102 -0
  31. package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
  32. package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
  33. package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
  34. package/dist/agents/pipeline/agents/flow-detector.js +101 -0
  35. package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
  36. package/dist/agents/pipeline/agents/service-describer.js +100 -0
  37. package/dist/agents/pipeline/agents/validator.d.ts +3 -0
  38. package/dist/agents/pipeline/agents/validator.js +102 -0
  39. package/dist/agents/pipeline/index.d.ts +13 -0
  40. package/dist/agents/pipeline/index.js +128 -0
  41. package/dist/agents/pipeline/merger.d.ts +7 -0
  42. package/dist/agents/pipeline/merger.js +212 -0
  43. package/dist/agents/pipeline/response-parser.d.ts +5 -0
  44. package/dist/agents/pipeline/response-parser.js +43 -0
  45. package/dist/agents/pipeline/types.d.ts +92 -0
  46. package/dist/agents/pipeline/types.js +3 -0
  47. package/dist/agents/prompt-data.d.ts +1 -0
  48. package/dist/agents/prompt-data.js +15 -0
  49. package/dist/agents/prompts-encode.d.ts +9 -0
  50. package/dist/agents/prompts-encode.js +26 -0
  51. package/dist/agents/prompts.d.ts +12 -0
  52. package/dist/agents/prompts.js +30 -0
  53. package/dist/agents/providers/anthropic.d.ts +10 -0
  54. package/dist/agents/providers/anthropic.js +117 -0
  55. package/dist/agents/providers/google.d.ts +10 -0
  56. package/dist/agents/providers/google.js +136 -0
  57. package/dist/agents/providers/ollama.d.ts +9 -0
  58. package/dist/agents/providers/ollama.js +162 -0
  59. package/dist/agents/providers/openai.d.ts +9 -0
  60. package/dist/agents/providers/openai.js +142 -0
  61. package/dist/agents/providers/router.d.ts +7 -0
  62. package/dist/agents/providers/router.js +55 -0
  63. package/dist/agents/runtime/orchestrator.d.ts +34 -0
  64. package/dist/agents/runtime/orchestrator.js +193 -0
  65. package/dist/agents/runtime/registry.d.ts +23 -0
  66. package/dist/agents/runtime/registry.js +56 -0
  67. package/dist/agents/runtime/types.d.ts +117 -0
  68. package/dist/agents/runtime/types.js +29 -0
  69. package/dist/agents/static/code-sampler.d.ts +3 -0
  70. package/dist/agents/static/code-sampler.js +153 -0
  71. package/dist/agents/static/component-detector.d.ts +3 -0
  72. package/dist/agents/static/component-detector.js +404 -0
  73. package/dist/agents/static/connection-mapper.d.ts +3 -0
  74. package/dist/agents/static/connection-mapper.js +280 -0
  75. package/dist/agents/static/doc-parser.d.ts +3 -0
  76. package/dist/agents/static/doc-parser.js +358 -0
  77. package/dist/agents/static/env-detector.d.ts +3 -0
  78. package/dist/agents/static/env-detector.js +73 -0
  79. package/dist/agents/static/event-detector.d.ts +3 -0
  80. package/dist/agents/static/event-detector.js +70 -0
  81. package/dist/agents/static/file-tree-collector.d.ts +3 -0
  82. package/dist/agents/static/file-tree-collector.js +51 -0
  83. package/dist/agents/static/index.d.ts +19 -0
  84. package/dist/agents/static/index.js +307 -0
  85. package/dist/agents/static/infra-analyzer.d.ts +3 -0
  86. package/dist/agents/static/infra-analyzer.js +208 -0
  87. package/dist/agents/static/structure-scanner.d.ts +3 -0
  88. package/dist/agents/static/structure-scanner.js +195 -0
  89. package/dist/agents/static/types.d.ts +165 -0
  90. package/dist/agents/static/types.js +2 -0
  91. package/dist/agents/static/utils.d.ts +21 -0
  92. package/dist/agents/static/utils.js +146 -0
  93. package/dist/agents/static/validator.d.ts +2 -0
  94. package/dist/agents/static/validator.js +75 -0
  95. package/dist/agents/tools/claude-code.d.ts +38 -0
  96. package/dist/agents/tools/claude-code.js +129 -0
  97. package/dist/agents/tools/local-fs.d.ts +12 -0
  98. package/dist/agents/tools/local-fs.js +112 -0
  99. package/dist/agents/tools/tool-definitions.d.ts +6 -0
  100. package/dist/agents/tools/tool-definitions.js +66 -0
  101. package/dist/cli/analyze.d.ts +27 -0
  102. package/dist/cli/analyze.js +586 -0
  103. package/dist/cli/auth.d.ts +46 -0
  104. package/dist/cli/auth.js +397 -0
  105. package/dist/cli/config.d.ts +11 -0
  106. package/dist/cli/config.js +177 -0
  107. package/dist/cli/diff.d.ts +10 -0
  108. package/dist/cli/diff.js +144 -0
  109. package/dist/cli/export.d.ts +10 -0
  110. package/dist/cli/export.js +321 -0
  111. package/dist/cli/gate.d.ts +13 -0
  112. package/dist/cli/gate.js +131 -0
  113. package/dist/cli/generate.d.ts +10 -0
  114. package/dist/cli/generate.js +213 -0
  115. package/dist/cli/license-gate.d.ts +27 -0
  116. package/dist/cli/license-gate.js +121 -0
  117. package/dist/cli/patrol.d.ts +15 -0
  118. package/dist/cli/patrol.js +212 -0
  119. package/dist/cli/run.d.ts +11 -0
  120. package/dist/cli/run.js +24 -0
  121. package/dist/cli/serve.d.ts +9 -0
  122. package/dist/cli/serve.js +65 -0
  123. package/dist/cli/setup.d.ts +1 -0
  124. package/dist/cli/setup.js +233 -0
  125. package/dist/cli/shared.d.ts +68 -0
  126. package/dist/cli/shared.js +275 -0
  127. package/dist/cli/stats.d.ts +9 -0
  128. package/dist/cli/stats.js +158 -0
  129. package/dist/cli/ui.d.ts +18 -0
  130. package/dist/cli/ui.js +144 -0
  131. package/dist/cli/validate.d.ts +54 -0
  132. package/dist/cli/validate.js +315 -0
  133. package/dist/cli/workflow.d.ts +10 -0
  134. package/dist/cli/workflow.js +594 -0
  135. package/dist/server/src/generator/index.d.ts +123 -0
  136. package/dist/server/src/generator/index.js +254 -0
  137. package/dist/server/src/index.d.ts +8 -0
  138. package/dist/server/src/index.js +1311 -0
  139. package/package.json +62 -0
  140. package/ui/dist/assets/index-B66Til39.js +70 -0
  141. package/ui/dist/assets/index-BE2OWbzu.css +1 -0
  142. package/ui/dist/index.html +14 -0
@@ -0,0 +1,1311 @@
1
+ #!/usr/bin/env node
2
+ import { watch } from "chokidar";
3
+ import { readFile, writeFile, mkdir } from "fs/promises";
4
+ import { existsSync, readFileSync, statSync } from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { createServer } from "http";
8
+ import { execSync, spawn, spawnSync } from "child_process";
9
+ // Get UI assets path
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const UI_DIST = path.resolve(__dirname, "../../../ui/dist");
13
+ // Global state
14
+ let config;
15
+ let sseClients = new Set();
16
+ let diagramWatcher = null;
17
+ let currentArchitecture = null;
18
+ // Process tracking for run-from-UI
19
+ const runningWorkflows = new Map();
20
+ let patrolProcess = null;
21
+ let patrolRunning = false;
22
+ let chatProcess = null;
23
+ // Resolve archbyte CLI binary path
24
+ function getArchbyteBin() {
25
+ try {
26
+ return execSync("which archbyte", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
27
+ }
28
+ catch {
29
+ // Fallback: resolve relative to this package
30
+ return path.resolve(__dirname, "../../../cli/dist/index.js");
31
+ }
32
+ }
33
+ // Broadcast ops event to SSE clients
34
+ function broadcastOpsEvent(event) {
35
+ const data = JSON.stringify(event);
36
+ for (const client of sseClients) {
37
+ client.write(`data: ${data}\n\n`);
38
+ }
39
+ }
40
+ // Load architecture from JSON file
41
+ async function loadArchitecture() {
42
+ try {
43
+ if (!existsSync(config.diagramPath)) {
44
+ return null;
45
+ }
46
+ const content = await readFile(config.diagramPath, "utf-8");
47
+ const data = JSON.parse(content);
48
+ return data;
49
+ }
50
+ catch (error) {
51
+ console.error("[archbyte] Failed to load architecture:", error);
52
+ return null;
53
+ }
54
+ }
55
+ // Get git info
56
+ function getGitInfo() {
57
+ try {
58
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
59
+ cwd: config.workspaceRoot,
60
+ encoding: "utf-8",
61
+ stdio: ["pipe", "pipe", "pipe"],
62
+ }).trim();
63
+ const commit = execSync("git rev-parse --short HEAD", {
64
+ cwd: config.workspaceRoot,
65
+ encoding: "utf-8",
66
+ stdio: ["pipe", "pipe", "pipe"],
67
+ }).trim();
68
+ let repo = null;
69
+ let remoteUrl = null;
70
+ try {
71
+ remoteUrl = execSync("git remote get-url origin", {
72
+ cwd: config.workspaceRoot,
73
+ encoding: "utf-8",
74
+ stdio: ["pipe", "pipe", "pipe"],
75
+ }).trim();
76
+ // Extract repo name from URL (handles both https and ssh)
77
+ const match = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
78
+ repo = match ? match[1] : null;
79
+ }
80
+ catch {
81
+ // No remote configured
82
+ }
83
+ return { branch, commit, repo, remoteUrl };
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ // Broadcast update to SSE clients
90
+ function broadcastUpdate() {
91
+ if (!currentArchitecture)
92
+ return;
93
+ const data = JSON.stringify({ type: "update", architecture: currentArchitecture });
94
+ for (const client of sseClients) {
95
+ client.write(`data: ${data}\n\n`);
96
+ }
97
+ }
98
+ // HTTP Server
99
+ let httpServer;
100
+ function createHttpServer() {
101
+ httpServer = createServer(async (req, res) => {
102
+ // Security headers — restrict to localhost origins only
103
+ const reqOrigin = req.headers.origin || "";
104
+ const allowedOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(reqOrigin) ? reqOrigin : `http://localhost:${config.port}`;
105
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
106
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
107
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
108
+ res.setHeader("X-Content-Type-Options", "nosniff");
109
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
110
+ if (req.method === "OPTIONS") {
111
+ res.writeHead(200);
112
+ res.end();
113
+ return;
114
+ }
115
+ const url = req.url || "/";
116
+ // SSE endpoint
117
+ if (url === "/events") {
118
+ res.writeHead(200, {
119
+ "Content-Type": "text/event-stream",
120
+ "Cache-Control": "no-cache",
121
+ Connection: "keep-alive",
122
+ });
123
+ currentArchitecture = await loadArchitecture();
124
+ if (currentArchitecture) {
125
+ res.write(`data: ${JSON.stringify({ type: "init", architecture: currentArchitecture })}\n\n`);
126
+ }
127
+ sseClients.add(res);
128
+ req.on("close", () => sseClients.delete(res));
129
+ return;
130
+ }
131
+ // API: Get architecture
132
+ if (url === "/api/architecture" && req.method === "GET") {
133
+ currentArchitecture = await loadArchitecture();
134
+ res.writeHead(200, { "Content-Type": "application/json" });
135
+ res.end(JSON.stringify(currentArchitecture || { nodes: [], edges: [] }));
136
+ return;
137
+ }
138
+ // API: Health
139
+ if (url === "/health") {
140
+ res.writeHead(200, { "Content-Type": "application/json" });
141
+ res.end(JSON.stringify({ status: "ok", project: config.name }));
142
+ return;
143
+ }
144
+ // API: License — returns user's tier and feature access
145
+ if (url === "/api/license" && req.method === "GET") {
146
+ const license = loadLicenseInfo();
147
+ res.writeHead(200, { "Content-Type": "application/json" });
148
+ res.end(JSON.stringify(license));
149
+ return;
150
+ }
151
+ // API: Git info (supports both /api/git and /api/tools-git for compatibility)
152
+ if (url === "/api/git" || url === "/api/tools-git") {
153
+ const info = getGitInfo();
154
+ res.writeHead(200, { "Content-Type": "application/json" });
155
+ res.end(JSON.stringify(info || {}));
156
+ return;
157
+ }
158
+ // API: Stats
159
+ if (url === "/api/stats" && req.method === "GET") {
160
+ const arch = currentArchitecture || (await loadArchitecture());
161
+ if (!arch) {
162
+ res.writeHead(404, { "Content-Type": "application/json" });
163
+ res.end(JSON.stringify({ error: "No architecture loaded" }));
164
+ return;
165
+ }
166
+ const realNodes = arch.nodes;
167
+ const n = realNodes.length;
168
+ const possibleConnections = n > 1 ? (n * (n - 1)) / 2 : 1;
169
+ const layerCounts = {};
170
+ for (const node of realNodes) {
171
+ layerCounts[node.layer] = (layerCounts[node.layer] || 0) + 1;
172
+ }
173
+ const connectedIds = new Set();
174
+ for (const edge of arch.edges) {
175
+ connectedIds.add(edge.source);
176
+ connectedIds.add(edge.target);
177
+ }
178
+ const orphans = realNodes.filter((nd) => !connectedIds.has(nd.id)).length;
179
+ const connectionCounts = new Map();
180
+ for (const edge of arch.edges) {
181
+ connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1);
182
+ connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1);
183
+ }
184
+ const hubs = realNodes.filter((nd) => (connectionCounts.get(nd.id) || 0) > 6);
185
+ // Read scan metadata from analysis.json
186
+ let scanMeta = {};
187
+ try {
188
+ const analysisPath = path.join(config.workspaceRoot, ".archbyte/analysis.json");
189
+ if (existsSync(analysisPath)) {
190
+ const raw = JSON.parse(readFileSync(analysisPath, "utf-8"));
191
+ scanMeta = raw.metadata || {};
192
+ }
193
+ }
194
+ catch { /* ignore */ }
195
+ res.writeHead(200, { "Content-Type": "application/json" });
196
+ res.end(JSON.stringify({
197
+ components: realNodes.filter((x) => x.type === "component" || x.type === "service").length,
198
+ databases: realNodes.filter((x) => x.type === "database").length,
199
+ externalServices: realNodes.filter((x) => x.type === "external").length,
200
+ totalConnections: arch.edges.length,
201
+ orphans,
202
+ hubs: hubs.length,
203
+ flows: arch.flows?.length || 0,
204
+ layerCounts,
205
+ lastUpdated: arch.lastUpdated,
206
+ durationMs: scanMeta.durationMs ?? null,
207
+ filesScanned: scanMeta.filesScanned ?? null,
208
+ analyzedAt: scanMeta.analyzedAt ?? null,
209
+ mode: scanMeta.mode ?? null,
210
+ tokenUsage: scanMeta.tokenUsage ?? null,
211
+ }));
212
+ return;
213
+ }
214
+ // API: Validate
215
+ if (url === "/api/validate" && req.method === "GET") {
216
+ const arch = currentArchitecture || (await loadArchitecture());
217
+ if (!arch) {
218
+ res.writeHead(404, { "Content-Type": "application/json" });
219
+ res.end(JSON.stringify({ error: "No architecture loaded" }));
220
+ return;
221
+ }
222
+ const realNodes = arch.nodes;
223
+ const nodeMap = new Map();
224
+ for (const nd of realNodes)
225
+ nodeMap.set(nd.id, nd);
226
+ const violations = [];
227
+ // no-layer-bypass
228
+ const layerOrder = { presentation: 0, application: 1, data: 2 };
229
+ for (const edge of arch.edges) {
230
+ const src = nodeMap.get(edge.source);
231
+ const tgt = nodeMap.get(edge.target);
232
+ if (!src || !tgt)
233
+ continue;
234
+ const srcO = layerOrder[src.layer];
235
+ const tgtO = layerOrder[tgt.layer];
236
+ if (srcO !== undefined && tgtO !== undefined && tgtO - srcO > 1) {
237
+ violations.push({
238
+ rule: "no-layer-bypass",
239
+ level: "error",
240
+ message: `"${src.label}" (${src.layer}) -> "${tgt.label}" (${tgt.layer})`,
241
+ nodeIds: [src.id, tgt.id],
242
+ });
243
+ }
244
+ }
245
+ // max-connections
246
+ const connectionCounts2 = new Map();
247
+ for (const edge of arch.edges) {
248
+ connectionCounts2.set(edge.source, (connectionCounts2.get(edge.source) || 0) + 1);
249
+ connectionCounts2.set(edge.target, (connectionCounts2.get(edge.target) || 0) + 1);
250
+ }
251
+ for (const nd of realNodes) {
252
+ const count = connectionCounts2.get(nd.id) || 0;
253
+ if (count > 6) {
254
+ violations.push({
255
+ rule: "max-connections",
256
+ level: "warn",
257
+ message: `"${nd.label}" has ${count} connections (threshold: 6)`,
258
+ nodeIds: [nd.id],
259
+ });
260
+ }
261
+ }
262
+ // no-orphans
263
+ const connected = new Set();
264
+ for (const edge of arch.edges) {
265
+ connected.add(edge.source);
266
+ connected.add(edge.target);
267
+ }
268
+ for (const nd of realNodes) {
269
+ if (!connected.has(nd.id)) {
270
+ violations.push({
271
+ rule: "no-orphans",
272
+ level: "warn",
273
+ message: `"${nd.label}" has no connections`,
274
+ nodeIds: [nd.id],
275
+ });
276
+ }
277
+ }
278
+ // no-circular-deps (simple cycle detection)
279
+ const adjacency = new Map();
280
+ for (const nd of realNodes)
281
+ adjacency.set(nd.id, []);
282
+ for (const edge of arch.edges) {
283
+ adjacency.get(edge.source)?.push(edge.target);
284
+ }
285
+ const visited = new Set();
286
+ const inStack = new Set();
287
+ const cyclePath = [];
288
+ const cycles = [];
289
+ function dfs(nodeId) {
290
+ visited.add(nodeId);
291
+ inStack.add(nodeId);
292
+ cyclePath.push(nodeId);
293
+ for (const neighbor of adjacency.get(nodeId) || []) {
294
+ if (!visited.has(neighbor)) {
295
+ dfs(neighbor);
296
+ }
297
+ else if (inStack.has(neighbor)) {
298
+ const start = cyclePath.indexOf(neighbor);
299
+ if (start !== -1)
300
+ cycles.push([...cyclePath.slice(start), neighbor]);
301
+ }
302
+ }
303
+ cyclePath.pop();
304
+ inStack.delete(nodeId);
305
+ }
306
+ for (const nd of realNodes) {
307
+ if (!visited.has(nd.id))
308
+ dfs(nd.id);
309
+ }
310
+ for (const cycle of cycles) {
311
+ violations.push({
312
+ rule: "no-circular-deps",
313
+ level: "error",
314
+ message: `Cycle: ${cycle.map((id) => nodeMap.get(id)?.label || id).join(" -> ")}`,
315
+ nodeIds: cycle.filter((id, i, arr) => arr.indexOf(id) === i),
316
+ });
317
+ }
318
+ const errors = violations.filter((v) => v.level === "error").length;
319
+ const warnings = violations.filter((v) => v.level === "warn").length;
320
+ res.writeHead(200, { "Content-Type": "application/json" });
321
+ res.end(JSON.stringify({
322
+ passed: errors === 0,
323
+ errors,
324
+ warnings,
325
+ violations,
326
+ }));
327
+ return;
328
+ }
329
+ // API: Export architecture as text format
330
+ if (url.startsWith("/api/export") && req.method === "GET") {
331
+ const arch = currentArchitecture || (await loadArchitecture());
332
+ if (!arch) {
333
+ res.writeHead(404, { "Content-Type": "application/json" });
334
+ res.end(JSON.stringify({ error: "No architecture loaded" }));
335
+ return;
336
+ }
337
+ const params = new URL(url, `http://localhost:${config.port}`).searchParams;
338
+ const format = params.get("format") || "mermaid";
339
+ const supported = ["mermaid", "markdown", "json", "plantuml", "dot"];
340
+ if (!supported.includes(format)) {
341
+ res.writeHead(400, { "Content-Type": "application/json" });
342
+ res.end(JSON.stringify({ error: `Unknown format: ${format}. Supported: ${supported.join(", ")}` }));
343
+ return;
344
+ }
345
+ const realNodes = arch.nodes;
346
+ const nodeMap = new Map();
347
+ for (const nd of realNodes)
348
+ nodeMap.set(nd.id, nd);
349
+ let content;
350
+ let contentType = "text/plain";
351
+ let ext = "txt";
352
+ switch (format) {
353
+ case "mermaid": {
354
+ const layerGroups = new Map();
355
+ for (const node of realNodes) {
356
+ const layer = node.layer || "other";
357
+ if (!layerGroups.has(layer))
358
+ layerGroups.set(layer, []);
359
+ layerGroups.get(layer).push(node);
360
+ }
361
+ const lines = ["graph TD"];
362
+ for (const layer of ["presentation", "application", "data", "external", "deployment"]) {
363
+ const nodes = layerGroups.get(layer);
364
+ if (!nodes || nodes.length === 0)
365
+ continue;
366
+ lines.push(` subgraph ${layer.charAt(0).toUpperCase() + layer.slice(1)}`);
367
+ for (const node of nodes) {
368
+ const id = node.id.replace(/[^a-zA-Z0-9_-]/g, "_");
369
+ const shape = node.type === "database" ? `[("${node.label}")]` : `["${node.label}"]`;
370
+ lines.push(` ${id}${shape}`);
371
+ }
372
+ lines.push(" end");
373
+ }
374
+ for (const edge of arch.edges) {
375
+ const src = edge.source.replace(/[^a-zA-Z0-9_-]/g, "_");
376
+ const tgt = edge.target.replace(/[^a-zA-Z0-9_-]/g, "_");
377
+ lines.push(edge.label ? ` ${src} -->|${edge.label}| ${tgt}` : ` ${src} --> ${tgt}`);
378
+ }
379
+ content = lines.join("\n");
380
+ ext = "mmd";
381
+ break;
382
+ }
383
+ case "plantuml": {
384
+ const layerGroups = new Map();
385
+ for (const node of realNodes) {
386
+ const layer = node.layer || "other";
387
+ if (!layerGroups.has(layer))
388
+ layerGroups.set(layer, []);
389
+ layerGroups.get(layer).push(node);
390
+ }
391
+ const lines = ["@startuml Architecture", "!theme plain", "skinparam componentStyle rectangle", ""];
392
+ for (const layer of ["presentation", "application", "data", "external", "deployment"]) {
393
+ const nodes = layerGroups.get(layer);
394
+ if (!nodes || nodes.length === 0)
395
+ continue;
396
+ lines.push(`package "${layer.charAt(0).toUpperCase() + layer.slice(1)}" {`);
397
+ for (const node of nodes) {
398
+ const type = node.type === "database" ? "database" : "component";
399
+ const id = node.id.replace(/[^a-zA-Z0-9_]/g, "_");
400
+ lines.push(` ${type} "${node.label.split("\\n")[0]}" as ${id}`);
401
+ }
402
+ lines.push("}", "");
403
+ }
404
+ for (const edge of arch.edges) {
405
+ const src = edge.source.replace(/[^a-zA-Z0-9_]/g, "_");
406
+ const tgt = edge.target.replace(/[^a-zA-Z0-9_]/g, "_");
407
+ lines.push(`${src} --> ${tgt}${edge.label ? ` : ${edge.label}` : ""}`);
408
+ }
409
+ lines.push("", "@enduml");
410
+ content = lines.join("\n");
411
+ ext = "puml";
412
+ break;
413
+ }
414
+ case "dot": {
415
+ const layerFills = {
416
+ presentation: "#d0ebff", application: "#ffe8cc", data: "#b2f2bb",
417
+ external: "#eebefa", deployment: "#ffe3e3",
418
+ };
419
+ const layerGroups = new Map();
420
+ for (const node of realNodes) {
421
+ const l = node.layer || "other";
422
+ if (!layerGroups.has(l))
423
+ layerGroups.set(l, []);
424
+ layerGroups.get(l).push(node);
425
+ }
426
+ const lines = ["digraph Architecture {", " rankdir=TB;", ' node [shape=box, style="rounded,filled", fontname="Helvetica"];', ""];
427
+ for (const layer of ["presentation", "application", "data", "external", "deployment"]) {
428
+ const nodes = layerGroups.get(layer);
429
+ if (!nodes || nodes.length === 0)
430
+ continue;
431
+ lines.push(` subgraph cluster_${layer} {`);
432
+ lines.push(` label="${layer.charAt(0).toUpperCase() + layer.slice(1)}";`);
433
+ lines.push(` style=filled; color="${layerFills[layer] || "#f0f0f0"}";`);
434
+ for (const node of nodes) {
435
+ const shape = node.type === "database" ? "cylinder" : "box";
436
+ lines.push(` "${node.id}" [label="${node.label.split("\\n")[0]}", shape=${shape}, fillcolor="${layerFills[layer] || "#ffffff"}"];`);
437
+ }
438
+ lines.push(" }");
439
+ }
440
+ lines.push("");
441
+ for (const edge of arch.edges) {
442
+ lines.push(` "${edge.source}" -> "${edge.target}"${edge.label ? ` [label="${edge.label}"]` : ""};`);
443
+ }
444
+ lines.push("}");
445
+ content = lines.join("\n");
446
+ ext = "dot";
447
+ break;
448
+ }
449
+ case "json": {
450
+ const components = realNodes.map((node) => {
451
+ const clean = { id: node.id, type: node.type, label: node.label, layer: node.layer };
452
+ if (node.environments?.length)
453
+ clean.environments = node.environments;
454
+ return clean;
455
+ });
456
+ const connections = arch.edges.map((edge) => {
457
+ const clean = { id: edge.id, source: edge.source, target: edge.target };
458
+ if (edge.label)
459
+ clean.label = edge.label;
460
+ if (edge.environments?.length)
461
+ clean.environments = edge.environments;
462
+ return clean;
463
+ });
464
+ const result = { components, connections };
465
+ if (arch.flows?.length)
466
+ result.flows = arch.flows;
467
+ if (arch.environments)
468
+ result.environments = arch.environments;
469
+ result.metadata = { lastUpdated: arch.lastUpdated, version: arch.version };
470
+ content = JSON.stringify(result, null, 2);
471
+ contentType = "application/json";
472
+ ext = "json";
473
+ break;
474
+ }
475
+ default: { // markdown
476
+ const lines = [`# Architecture — ${config.name}`, "", `> Generated by ArchByte on ${new Date().toISOString().slice(0, 10)}`, ""];
477
+ lines.push("## Summary", "");
478
+ lines.push(`- **Components:** ${realNodes.filter((n) => n.type === "component" || n.type === "service").length}`);
479
+ lines.push(`- **Databases:** ${realNodes.filter((n) => n.type === "database").length}`);
480
+ lines.push(`- **External Services:** ${realNodes.filter((n) => n.type === "external").length}`);
481
+ lines.push(`- **Connections:** ${arch.edges.length}`, "");
482
+ lines.push("## Components", "", "| Name | Type | Layer |", "|------|------|-------|");
483
+ for (const node of realNodes)
484
+ lines.push(`| ${node.label} | ${node.type} | ${node.layer} |`);
485
+ lines.push("", "## Connections", "");
486
+ for (const edge of arch.edges) {
487
+ const src = nodeMap.get(edge.source)?.label || edge.source;
488
+ const tgt = nodeMap.get(edge.target)?.label || edge.target;
489
+ lines.push(`- **${src}** → **${tgt}**${edge.label ? ` — ${edge.label}` : ""}`);
490
+ }
491
+ content = lines.join("\n");
492
+ ext = "md";
493
+ break;
494
+ }
495
+ }
496
+ res.writeHead(200, {
497
+ "Content-Type": contentType,
498
+ "Content-Disposition": `attachment; filename="architecture.${ext}"`,
499
+ });
500
+ res.end(content);
501
+ return;
502
+ }
503
+ // API: Patrol history
504
+ if (url === "/api/patrol/latest" && req.method === "GET") {
505
+ const latestPath = path.join(config.workspaceRoot, ".archbyte/patrols/latest.json");
506
+ if (!existsSync(latestPath)) {
507
+ res.writeHead(200, { "Content-Type": "application/json" });
508
+ res.end(JSON.stringify(null));
509
+ return;
510
+ }
511
+ try {
512
+ const content = readFileSync(latestPath, "utf-8");
513
+ res.writeHead(200, { "Content-Type": "application/json" });
514
+ res.end(content);
515
+ }
516
+ catch {
517
+ res.writeHead(200, { "Content-Type": "application/json" });
518
+ res.end(JSON.stringify(null));
519
+ }
520
+ return;
521
+ }
522
+ if (url === "/api/patrol/history" && req.method === "GET") {
523
+ const historyPath = path.join(config.workspaceRoot, ".archbyte/patrols/history.jsonl");
524
+ if (!existsSync(historyPath)) {
525
+ res.writeHead(200, { "Content-Type": "application/json" });
526
+ res.end(JSON.stringify([]));
527
+ return;
528
+ }
529
+ try {
530
+ const content = readFileSync(historyPath, "utf-8");
531
+ const records = content.trim().split("\n").filter(Boolean).slice(-50).map((line) => JSON.parse(line));
532
+ res.writeHead(200, { "Content-Type": "application/json" });
533
+ res.end(JSON.stringify(records));
534
+ }
535
+ catch {
536
+ res.writeHead(200, { "Content-Type": "application/json" });
537
+ res.end(JSON.stringify([]));
538
+ }
539
+ return;
540
+ }
541
+ // API: Workflow list and status
542
+ if (url === "/api/workflow/list" && req.method === "GET") {
543
+ const workflows = [];
544
+ // Built-in workflows
545
+ const builtins = [
546
+ { id: "full-analysis", name: "Full Analysis Pipeline", description: "Complete architecture pipeline", steps: ["generate", "validate", "stats", "export"] },
547
+ { id: "ci-check", name: "CI Architecture Check", description: "Lightweight CI pipeline", steps: ["validate"] },
548
+ { id: "drift-check", name: "Architecture Drift Check", description: "Check for architecture drift", steps: ["snapshot", "generate", "diff"] },
549
+ ];
550
+ for (const b of builtins) {
551
+ let status = null;
552
+ const stPath = path.join(config.workspaceRoot, `.archbyte/workflows/.state/${b.id}.json`);
553
+ if (existsSync(stPath)) {
554
+ try {
555
+ status = JSON.parse(readFileSync(stPath, "utf-8")).status;
556
+ }
557
+ catch { }
558
+ }
559
+ workflows.push({ ...b, builtin: true, status });
560
+ }
561
+ // Custom workflows
562
+ const workflowDir = path.join(config.workspaceRoot, ".archbyte/workflows");
563
+ if (existsSync(workflowDir)) {
564
+ const { readdirSync } = await import("fs");
565
+ const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
566
+ for (const file of files) {
567
+ try {
568
+ const content = readFileSync(path.join(workflowDir, file), "utf-8");
569
+ const idMatch = content.match(/^id:\s*(.+)$/m);
570
+ const nameMatch = content.match(/^name:\s*"?([^"\n]+)"?$/m);
571
+ const descMatch = content.match(/^description:\s*"?([^"\n]+)"?$/m);
572
+ if (idMatch) {
573
+ const id = idMatch[1].trim();
574
+ if (builtins.some((b) => b.id === id))
575
+ continue;
576
+ let status = null;
577
+ const stPath = path.join(config.workspaceRoot, `.archbyte/workflows/.state/${id}.json`);
578
+ if (existsSync(stPath)) {
579
+ try {
580
+ status = JSON.parse(readFileSync(stPath, "utf-8")).status;
581
+ }
582
+ catch { }
583
+ }
584
+ workflows.push({
585
+ id,
586
+ name: nameMatch?.[1]?.trim() || id,
587
+ description: descMatch?.[1]?.trim() || "",
588
+ steps: [],
589
+ builtin: false,
590
+ status,
591
+ });
592
+ }
593
+ }
594
+ catch { }
595
+ }
596
+ }
597
+ res.writeHead(200, { "Content-Type": "application/json" });
598
+ res.end(JSON.stringify(workflows));
599
+ return;
600
+ }
601
+ if (url.startsWith("/api/workflow/status/") && req.method === "GET") {
602
+ const id = url.split("/").pop();
603
+ // Validate ID to prevent path traversal (alphanumeric, hyphens, underscores only)
604
+ if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
605
+ res.writeHead(400, { "Content-Type": "application/json" });
606
+ res.end(JSON.stringify({ error: "Invalid workflow ID" }));
607
+ return;
608
+ }
609
+ const stPath = path.join(config.workspaceRoot, `.archbyte/workflows/.state/${id}.json`);
610
+ if (!existsSync(stPath)) {
611
+ res.writeHead(200, { "Content-Type": "application/json" });
612
+ res.end(JSON.stringify(null));
613
+ return;
614
+ }
615
+ try {
616
+ const content = readFileSync(stPath, "utf-8");
617
+ res.writeHead(200, { "Content-Type": "application/json" });
618
+ res.end(content);
619
+ }
620
+ catch {
621
+ res.writeHead(200, { "Content-Type": "application/json" });
622
+ res.end(JSON.stringify(null));
623
+ }
624
+ return;
625
+ }
626
+ // API: Open file in VS Code
627
+ if (url === "/api/open-file" && req.method === "POST") {
628
+ let body = "";
629
+ req.on("data", (chunk) => { body += chunk.toString(); });
630
+ req.on("end", async () => {
631
+ try {
632
+ const { path: filePath } = JSON.parse(body);
633
+ if (!filePath) {
634
+ res.writeHead(400, { "Content-Type": "application/json" });
635
+ res.end(JSON.stringify({ error: "Path required" }));
636
+ return;
637
+ }
638
+ const fullPath = path.resolve(config.workspaceRoot, filePath);
639
+ // Validate path stays within workspace to prevent traversal
640
+ if (!fullPath.startsWith(path.resolve(config.workspaceRoot))) {
641
+ res.writeHead(403, { "Content-Type": "application/json" });
642
+ res.end(JSON.stringify({ error: "Path outside workspace" }));
643
+ return;
644
+ }
645
+ if (existsSync(fullPath) && statSync(fullPath).isFile()) {
646
+ // Open file in existing VS Code window (no shell — prevents injection)
647
+ spawnSync("code", ["-r", "-g", fullPath], { stdio: "ignore" });
648
+ }
649
+ else if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
650
+ spawnSync("code", ["-r", fullPath], { stdio: "ignore" });
651
+ }
652
+ else {
653
+ spawnSync("code", ["-r", config.workspaceRoot], { stdio: "ignore" });
654
+ }
655
+ res.writeHead(200, { "Content-Type": "application/json" });
656
+ res.end(JSON.stringify({ success: true, path: fullPath }));
657
+ }
658
+ catch (error) {
659
+ res.writeHead(500, { "Content-Type": "application/json" });
660
+ res.end(JSON.stringify({ error: String(error) }));
661
+ }
662
+ });
663
+ return;
664
+ }
665
+ // API: Update positions from UI
666
+ if (url === "/api/update-positions" && req.method === "POST") {
667
+ let body = "";
668
+ req.on("data", (chunk) => { body += chunk.toString(); });
669
+ req.on("end", async () => {
670
+ try {
671
+ const { updates } = JSON.parse(body);
672
+ const content = existsSync(config.diagramPath)
673
+ ? await readFile(config.diagramPath, "utf-8")
674
+ : '{"nodes":[],"edges":[],"flows":[],"lastUpdated":"","version":1}';
675
+ const architecture = JSON.parse(content);
676
+ for (const update of updates || []) {
677
+ const node = architecture.nodes?.find((n) => n.id === update.id);
678
+ if (node) {
679
+ node.x = update.x;
680
+ node.y = update.y;
681
+ if (update.width)
682
+ node.width = update.width;
683
+ if (update.height)
684
+ node.height = update.height;
685
+ }
686
+ }
687
+ // Update timestamp
688
+ architecture.lastUpdated = new Date().toISOString();
689
+ const dir = path.dirname(config.diagramPath);
690
+ if (!existsSync(dir)) {
691
+ await mkdir(dir, { recursive: true });
692
+ }
693
+ await writeFile(config.diagramPath, JSON.stringify(architecture, null, 2));
694
+ res.writeHead(200, { "Content-Type": "application/json" });
695
+ res.end(JSON.stringify({ success: true, updated: updates?.length || 0 }));
696
+ }
697
+ catch (error) {
698
+ res.writeHead(500, { "Content-Type": "application/json" });
699
+ res.end(JSON.stringify({ error: String(error) }));
700
+ }
701
+ });
702
+ return;
703
+ }
704
+ // API: Run generate
705
+ if (url === "/api/generate" && req.method === "POST") {
706
+ if (runningWorkflows.has("__generate__")) {
707
+ res.writeHead(409, { "Content-Type": "application/json" });
708
+ res.end(JSON.stringify({ error: "Generate already running" }));
709
+ return;
710
+ }
711
+ const bin = getArchbyteBin();
712
+ const child = spawn(process.execPath, [bin, "generate"], {
713
+ cwd: config.workspaceRoot,
714
+ stdio: ["ignore", "pipe", "pipe"],
715
+ env: { ...process.env, FORCE_COLOR: "0" },
716
+ });
717
+ runningWorkflows.set("__generate__", child);
718
+ broadcastOpsEvent({ type: "generate:started" });
719
+ child.on("close", (code) => {
720
+ runningWorkflows.delete("__generate__");
721
+ broadcastOpsEvent({ type: "generate:finished", code });
722
+ });
723
+ res.writeHead(200, { "Content-Type": "application/json" });
724
+ res.end(JSON.stringify({ ok: true }));
725
+ return;
726
+ }
727
+ // API: Generate status
728
+ if (url === "/api/generate/status" && req.method === "GET") {
729
+ res.writeHead(200, { "Content-Type": "application/json" });
730
+ res.end(JSON.stringify({ running: runningWorkflows.has("__generate__") }));
731
+ return;
732
+ }
733
+ // API: Run workflow
734
+ if (url.startsWith("/api/workflow/run/") && req.method === "POST") {
735
+ const id = url.split("/").pop();
736
+ // Validate ID to prevent injection into spawn args
737
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
738
+ res.writeHead(400, { "Content-Type": "application/json" });
739
+ res.end(JSON.stringify({ error: "Invalid workflow ID" }));
740
+ return;
741
+ }
742
+ if (runningWorkflows.has(id)) {
743
+ res.writeHead(409, { "Content-Type": "application/json" });
744
+ res.end(JSON.stringify({ error: "Workflow already running", id }));
745
+ return;
746
+ }
747
+ const bin = getArchbyteBin();
748
+ const child = spawn(process.execPath, [bin, "workflow", "--run", id], {
749
+ cwd: config.workspaceRoot,
750
+ stdio: ["ignore", "pipe", "pipe"],
751
+ env: { ...process.env, FORCE_COLOR: "0" },
752
+ });
753
+ runningWorkflows.set(id, child);
754
+ broadcastOpsEvent({ type: "workflow:started", id });
755
+ child.on("close", (code) => {
756
+ runningWorkflows.delete(id);
757
+ broadcastOpsEvent({ type: "workflow:finished", id, code });
758
+ });
759
+ res.writeHead(200, { "Content-Type": "application/json" });
760
+ res.end(JSON.stringify({ ok: true, id }));
761
+ return;
762
+ }
763
+ // API: Reset workflow state
764
+ if (url === "/api/workflow/reset" && req.method === "POST") {
765
+ let body = "";
766
+ req.on("data", (chunk) => { body += chunk.toString(); });
767
+ req.on("end", async () => {
768
+ try {
769
+ const { id } = JSON.parse(body || "{}");
770
+ const stateDir = path.join(config.workspaceRoot, ".archbyte/workflows/.state");
771
+ if (id) {
772
+ // Validate ID to prevent path traversal
773
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
774
+ res.writeHead(400, { "Content-Type": "application/json" });
775
+ res.end(JSON.stringify({ error: "Invalid workflow ID" }));
776
+ return;
777
+ }
778
+ const stPath = path.join(stateDir, `${id}.json`);
779
+ if (existsSync(stPath)) {
780
+ const { unlinkSync } = await import("fs");
781
+ unlinkSync(stPath);
782
+ }
783
+ }
784
+ else {
785
+ // Reset all
786
+ if (existsSync(stateDir)) {
787
+ const { readdirSync, unlinkSync } = await import("fs");
788
+ for (const f of readdirSync(stateDir)) {
789
+ if (f.endsWith(".json"))
790
+ unlinkSync(path.join(stateDir, f));
791
+ }
792
+ }
793
+ }
794
+ broadcastOpsEvent({ type: "workflow:reset", id: id || "all" });
795
+ res.writeHead(200, { "Content-Type": "application/json" });
796
+ res.end(JSON.stringify({ ok: true }));
797
+ }
798
+ catch (error) {
799
+ res.writeHead(500, { "Content-Type": "application/json" });
800
+ res.end(JSON.stringify({ error: String(error) }));
801
+ }
802
+ });
803
+ return;
804
+ }
805
+ // API: Start patrol
806
+ if (url === "/api/patrol/start" && req.method === "POST") {
807
+ if (patrolRunning) {
808
+ res.writeHead(409, { "Content-Type": "application/json" });
809
+ res.end(JSON.stringify({ error: "Patrol already running" }));
810
+ return;
811
+ }
812
+ let body = "";
813
+ req.on("data", (chunk) => { body += chunk.toString(); });
814
+ req.on("end", () => {
815
+ const { interval } = JSON.parse(body || "{}");
816
+ const bin = getArchbyteBin();
817
+ const args = [bin, "patrol", "--watch"];
818
+ if (interval)
819
+ args.push("--interval", String(interval));
820
+ patrolProcess = spawn(process.execPath, args, {
821
+ cwd: config.workspaceRoot,
822
+ stdio: ["ignore", "pipe", "pipe"],
823
+ env: { ...process.env, FORCE_COLOR: "0" },
824
+ });
825
+ patrolRunning = true;
826
+ broadcastOpsEvent({ type: "patrol:started" });
827
+ patrolProcess.on("close", () => {
828
+ patrolProcess = null;
829
+ patrolRunning = false;
830
+ broadcastOpsEvent({ type: "patrol:stopped" });
831
+ });
832
+ res.writeHead(200, { "Content-Type": "application/json" });
833
+ res.end(JSON.stringify({ ok: true }));
834
+ });
835
+ return;
836
+ }
837
+ // API: Stop patrol
838
+ if (url === "/api/patrol/stop" && req.method === "POST") {
839
+ if (patrolProcess) {
840
+ patrolProcess.kill("SIGTERM");
841
+ patrolProcess = null;
842
+ patrolRunning = false;
843
+ broadcastOpsEvent({ type: "patrol:stopped" });
844
+ }
845
+ res.writeHead(200, { "Content-Type": "application/json" });
846
+ res.end(JSON.stringify({ ok: true }));
847
+ return;
848
+ }
849
+ // API: Patrol running status
850
+ if (url === "/api/patrol/running" && req.method === "GET") {
851
+ res.writeHead(200, { "Content-Type": "application/json" });
852
+ res.end(JSON.stringify({ running: patrolRunning }));
853
+ return;
854
+ }
855
+ // API: Config — read project config (including provider settings)
856
+ if (url === "/api/config" && req.method === "GET") {
857
+ const configPath = path.join(config.workspaceRoot, ".archbyte/config.json");
858
+ if (!existsSync(configPath)) {
859
+ res.writeHead(200, { "Content-Type": "application/json" });
860
+ res.end(JSON.stringify({}));
861
+ return;
862
+ }
863
+ try {
864
+ const content = readFileSync(configPath, "utf-8");
865
+ res.writeHead(200, { "Content-Type": "application/json" });
866
+ res.end(content);
867
+ }
868
+ catch {
869
+ res.writeHead(200, { "Content-Type": "application/json" });
870
+ res.end(JSON.stringify({}));
871
+ }
872
+ return;
873
+ }
874
+ // API: Config — save project config (provider, API key, etc.)
875
+ if (url === "/api/config" && req.method === "POST") {
876
+ let body = "";
877
+ req.on("data", (chunk) => { body += chunk.toString(); });
878
+ req.on("end", async () => {
879
+ try {
880
+ const data = JSON.parse(body);
881
+ const configDir = path.join(config.workspaceRoot, ".archbyte");
882
+ if (!existsSync(configDir))
883
+ await mkdir(configDir, { recursive: true });
884
+ const configPath = path.join(configDir, "config.json");
885
+ // Merge with existing config if present
886
+ let existing = {};
887
+ if (existsSync(configPath)) {
888
+ try {
889
+ existing = JSON.parse(readFileSync(configPath, "utf-8"));
890
+ }
891
+ catch { }
892
+ }
893
+ const merged = { ...existing, ...data };
894
+ await writeFile(configPath, JSON.stringify(merged, null, 2));
895
+ res.writeHead(200, { "Content-Type": "application/json" });
896
+ res.end(JSON.stringify({ ok: true, config: merged }));
897
+ }
898
+ catch (error) {
899
+ res.writeHead(500, { "Content-Type": "application/json" });
900
+ res.end(JSON.stringify({ error: String(error) }));
901
+ }
902
+ });
903
+ return;
904
+ }
905
+ // API: Premium agent results
906
+ if (url === "/api/premium-agents/results" && req.method === "GET") {
907
+ const premiumPath = path.join(config.workspaceRoot, ".archbyte/premium-results.json");
908
+ if (!existsSync(premiumPath)) {
909
+ res.writeHead(200, { "Content-Type": "application/json" });
910
+ res.end(JSON.stringify([]));
911
+ return;
912
+ }
913
+ try {
914
+ const content = readFileSync(premiumPath, "utf-8");
915
+ res.writeHead(200, { "Content-Type": "application/json" });
916
+ res.end(content);
917
+ }
918
+ catch {
919
+ res.writeHead(200, { "Content-Type": "application/json" });
920
+ res.end(JSON.stringify([]));
921
+ }
922
+ return;
923
+ }
924
+ // API: Project info from analysis.json
925
+ if (url === "/api/project" && req.method === "GET") {
926
+ const analysisPath = path.join(config.workspaceRoot, ".archbyte/analysis.json");
927
+ if (!existsSync(analysisPath)) {
928
+ res.writeHead(200, { "Content-Type": "application/json" });
929
+ res.end(JSON.stringify(null));
930
+ return;
931
+ }
932
+ try {
933
+ const content = readFileSync(analysisPath, "utf-8");
934
+ const analysis = JSON.parse(content);
935
+ const project = analysis.project || {};
936
+ const gitInfo = getGitInfo();
937
+ const remoteUrl = gitInfo?.remoteUrl || null;
938
+ const repoUrl = remoteUrl
939
+ ? remoteUrl.replace(/^git@([^:]+):/, "https://$1/").replace(/\.git$/, "")
940
+ : null;
941
+ res.writeHead(200, { "Content-Type": "application/json" });
942
+ res.end(JSON.stringify({
943
+ name: project.name || config.name,
944
+ description: project.description || null,
945
+ primaryLanguage: project.primaryLanguage || null,
946
+ isMonorepo: project.isMonorepo || false,
947
+ components: (analysis.components || []).length,
948
+ databases: (analysis.databases || []).length,
949
+ externalServices: (analysis.externalServices || []).length,
950
+ connections: (analysis.connections || []).length,
951
+ flows: (analysis.flows || []).length,
952
+ repoUrl,
953
+ workspacePath: config.workspaceRoot,
954
+ }));
955
+ }
956
+ catch {
957
+ res.writeHead(200, { "Content-Type": "application/json" });
958
+ res.end(JSON.stringify({ name: config.name }));
959
+ }
960
+ return;
961
+ }
962
+ // API: Analysis status — last analyze result (success/error)
963
+ if (url === "/api/analysis-status" && req.method === "GET") {
964
+ const statusPath = path.join(config.workspaceRoot, ".archbyte/analysis-status.json");
965
+ if (!existsSync(statusPath)) {
966
+ res.writeHead(200, { "Content-Type": "application/json" });
967
+ res.end(JSON.stringify(null));
968
+ return;
969
+ }
970
+ try {
971
+ const content = readFileSync(statusPath, "utf-8");
972
+ res.writeHead(200, { "Content-Type": "application/json" });
973
+ res.end(content);
974
+ }
975
+ catch {
976
+ res.writeHead(200, { "Content-Type": "application/json" });
977
+ res.end(JSON.stringify(null));
978
+ }
979
+ return;
980
+ }
981
+ // API: Telemetry — read agent timing data from analysis runs
982
+ if (url === "/api/telemetry" && req.method === "GET") {
983
+ const telPath = path.join(config.workspaceRoot, ".archbyte/telemetry.json");
984
+ if (!existsSync(telPath)) {
985
+ res.writeHead(200, { "Content-Type": "application/json" });
986
+ res.end(JSON.stringify(null));
987
+ return;
988
+ }
989
+ try {
990
+ const content = readFileSync(telPath, "utf-8");
991
+ res.writeHead(200, { "Content-Type": "application/json" });
992
+ res.end(content);
993
+ }
994
+ catch {
995
+ res.writeHead(200, { "Content-Type": "application/json" });
996
+ res.end(JSON.stringify(null));
997
+ }
998
+ return;
999
+ }
1000
+ // API: Telemetry — record agent timing data
1001
+ if (url === "/api/telemetry" && req.method === "POST") {
1002
+ let body = "";
1003
+ req.on("data", (chunk) => { body += chunk.toString(); });
1004
+ req.on("end", async () => {
1005
+ try {
1006
+ const data = JSON.parse(body);
1007
+ const telDir = path.join(config.workspaceRoot, ".archbyte");
1008
+ if (!existsSync(telDir))
1009
+ await mkdir(telDir, { recursive: true });
1010
+ await writeFile(path.join(telDir, "telemetry.json"), JSON.stringify(data, null, 2));
1011
+ res.writeHead(200, { "Content-Type": "application/json" });
1012
+ res.end(JSON.stringify({ ok: true }));
1013
+ }
1014
+ catch (error) {
1015
+ res.writeHead(500, { "Content-Type": "application/json" });
1016
+ res.end(JSON.stringify({ error: String(error) }));
1017
+ }
1018
+ });
1019
+ return;
1020
+ }
1021
+ // API: Chat with architecture assistant
1022
+ if (url === "/api/chat" && req.method === "POST") {
1023
+ let body = "";
1024
+ req.on("data", (chunk) => { body += chunk.toString(); });
1025
+ req.on("end", async () => {
1026
+ try {
1027
+ const { message, history } = JSON.parse(body || "{}");
1028
+ if (!message) {
1029
+ res.writeHead(400, { "Content-Type": "application/json" });
1030
+ res.end(JSON.stringify({ error: "Message required" }));
1031
+ return;
1032
+ }
1033
+ const arch = currentArchitecture || (await loadArchitecture());
1034
+ if (!arch) {
1035
+ res.writeHead(400, { "Content-Type": "application/json" });
1036
+ res.end(JSON.stringify({ error: "No architecture loaded. Run 'archbyte generate' first." }));
1037
+ return;
1038
+ }
1039
+ // Build compact architecture summary
1040
+ const nodeMap = new Map();
1041
+ for (const nd of arch.nodes)
1042
+ nodeMap.set(nd.id, nd);
1043
+ const layerGroups = new Map();
1044
+ for (const nd of arch.nodes) {
1045
+ if (!layerGroups.has(nd.layer))
1046
+ layerGroups.set(nd.layer, []);
1047
+ layerGroups.get(nd.layer).push(nd.label.split("\n")[0]);
1048
+ }
1049
+ let summary = "";
1050
+ for (const [layer, names] of layerGroups) {
1051
+ summary += `${layer}: ${names.join(", ")}\n`;
1052
+ }
1053
+ summary += "\nConnections:\n";
1054
+ for (const edge of arch.edges) {
1055
+ const src = nodeMap.get(edge.source)?.label.split("\n")[0] || edge.source;
1056
+ const tgt = nodeMap.get(edge.target)?.label.split("\n")[0] || edge.target;
1057
+ summary += ` ${src} -> ${tgt}${edge.label ? ` (${edge.label})` : ""}\n`;
1058
+ }
1059
+ if (arch.flows && arch.flows.length > 0) {
1060
+ summary += "\nFlows:\n";
1061
+ for (const flow of arch.flows) {
1062
+ summary += ` ${flow.name}: ${flow.description}\n`;
1063
+ }
1064
+ }
1065
+ if (arch.environments) {
1066
+ summary += "\nEnvironments: " + Object.keys(arch.environments).join(", ") + "\n";
1067
+ }
1068
+ // Build the prompt with conversation history
1069
+ const systemPrompt = `You are an architecture assistant for "${config.name}". Answer ONLY about this architecture. If asked unrelated questions, politely decline.\n\nARCHITECTURE:\n${summary}`;
1070
+ let fullPrompt = systemPrompt + "\n\n";
1071
+ if (history && Array.isArray(history)) {
1072
+ for (const msg of history) {
1073
+ if (msg.role === "user")
1074
+ fullPrompt += `User: ${msg.content}\n`;
1075
+ else if (msg.role === "assistant")
1076
+ fullPrompt += `Assistant: ${msg.content}\n`;
1077
+ }
1078
+ }
1079
+ fullPrompt += `User: ${message}\nAssistant:`;
1080
+ // Kill previous chat process if still running
1081
+ if (chatProcess) {
1082
+ try {
1083
+ chatProcess.kill("SIGTERM");
1084
+ }
1085
+ catch { }
1086
+ chatProcess = null;
1087
+ }
1088
+ // Check if claude CLI is available (no shell — prevents injection)
1089
+ let claudePath;
1090
+ try {
1091
+ const result = spawnSync("which", ["claude"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
1092
+ claudePath = (result.stdout || "").trim();
1093
+ if (!claudePath || result.status !== 0)
1094
+ throw new Error("not found");
1095
+ }
1096
+ catch {
1097
+ res.writeHead(503, { "Content-Type": "application/json" });
1098
+ res.end(JSON.stringify({ error: "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code" }));
1099
+ return;
1100
+ }
1101
+ // Stream response as SSE
1102
+ res.writeHead(200, {
1103
+ "Content-Type": "text/event-stream",
1104
+ "Cache-Control": "no-cache",
1105
+ Connection: "keep-alive",
1106
+ "Access-Control-Allow-Origin": "*",
1107
+ });
1108
+ const child = spawn(claudePath, ["-p", "--model", "sonnet"], {
1109
+ cwd: config.workspaceRoot,
1110
+ stdio: ["pipe", "pipe", "pipe"],
1111
+ env: { ...process.env, FORCE_COLOR: "0" },
1112
+ });
1113
+ chatProcess = child;
1114
+ child.stdin.write(fullPrompt);
1115
+ child.stdin.end();
1116
+ child.stdout.on("data", (data) => {
1117
+ const text = data.toString();
1118
+ res.write(`data: ${JSON.stringify({ type: "text", text })}\n\n`);
1119
+ });
1120
+ child.stderr.on("data", (data) => {
1121
+ console.error("[archbyte-chat] stderr:", data.toString().trim());
1122
+ });
1123
+ child.on("close", (code) => {
1124
+ res.write(`data: ${JSON.stringify({ type: "done", code })}\n\n`);
1125
+ res.end();
1126
+ if (chatProcess === child)
1127
+ chatProcess = null;
1128
+ });
1129
+ child.on("error", (err) => {
1130
+ res.write(`data: ${JSON.stringify({ type: "error", message: String(err) })}\n\n`);
1131
+ res.end();
1132
+ if (chatProcess === child)
1133
+ chatProcess = null;
1134
+ });
1135
+ req.on("close", () => {
1136
+ if (chatProcess === child) {
1137
+ try {
1138
+ child.kill("SIGTERM");
1139
+ }
1140
+ catch { }
1141
+ chatProcess = null;
1142
+ }
1143
+ });
1144
+ }
1145
+ catch (error) {
1146
+ res.writeHead(500, { "Content-Type": "application/json" });
1147
+ res.end(JSON.stringify({ error: String(error) }));
1148
+ }
1149
+ });
1150
+ return;
1151
+ }
1152
+ // Serve static UI files
1153
+ const MIME_TYPES = {
1154
+ ".html": "text/html",
1155
+ ".js": "application/javascript",
1156
+ ".css": "text/css",
1157
+ ".json": "application/json",
1158
+ ".png": "image/png",
1159
+ ".svg": "image/svg+xml",
1160
+ };
1161
+ let filePath = url === "/" ? "/index.html" : url;
1162
+ // Use path.resolve to normalize traversal sequences (e.g. /../)
1163
+ const fullPath = path.resolve(UI_DIST, filePath.replace(/^\//, ""));
1164
+ if (!fullPath.startsWith(UI_DIST)) {
1165
+ res.writeHead(403);
1166
+ res.end("Forbidden");
1167
+ return;
1168
+ }
1169
+ try {
1170
+ const content = readFileSync(fullPath);
1171
+ const ext = path.extname(filePath);
1172
+ res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
1173
+ res.end(content);
1174
+ }
1175
+ catch {
1176
+ try {
1177
+ const indexContent = readFileSync(path.join(UI_DIST, "index.html"));
1178
+ res.writeHead(200, { "Content-Type": "text/html" });
1179
+ res.end(indexContent);
1180
+ }
1181
+ catch {
1182
+ res.writeHead(404);
1183
+ res.end("Not found");
1184
+ }
1185
+ }
1186
+ });
1187
+ }
1188
+ // Start HTTP server
1189
+ async function startHttpServer() {
1190
+ return new Promise((resolve, reject) => {
1191
+ httpServer.once("error", reject);
1192
+ httpServer.listen(config.port, () => {
1193
+ console.error(`[archbyte] Server running at http://localhost:${config.port}`);
1194
+ resolve();
1195
+ });
1196
+ });
1197
+ }
1198
+ // Setup file watcher
1199
+ function setupWatcher() {
1200
+ if (!existsSync(config.diagramPath))
1201
+ return;
1202
+ diagramWatcher = watch(config.diagramPath, { ignoreInitial: true });
1203
+ diagramWatcher.on("change", async () => {
1204
+ console.error("[archbyte] Diagram changed, reloading...");
1205
+ currentArchitecture = await loadArchitecture();
1206
+ broadcastUpdate();
1207
+ });
1208
+ }
1209
+ // Graceful shutdown
1210
+ function setupShutdown() {
1211
+ const shutdown = async () => {
1212
+ console.error("[archbyte] Shutting down...");
1213
+ // Kill tracked child processes
1214
+ for (const [, proc] of runningWorkflows) {
1215
+ try {
1216
+ proc.kill("SIGTERM");
1217
+ }
1218
+ catch { }
1219
+ }
1220
+ runningWorkflows.clear();
1221
+ if (patrolProcess) {
1222
+ try {
1223
+ patrolProcess.kill("SIGTERM");
1224
+ }
1225
+ catch { }
1226
+ patrolProcess = null;
1227
+ patrolRunning = false;
1228
+ }
1229
+ if (chatProcess) {
1230
+ try {
1231
+ chatProcess.kill("SIGTERM");
1232
+ }
1233
+ catch { }
1234
+ chatProcess = null;
1235
+ }
1236
+ for (const client of sseClients) {
1237
+ try {
1238
+ client.end();
1239
+ }
1240
+ catch { }
1241
+ }
1242
+ sseClients.clear();
1243
+ await diagramWatcher?.close();
1244
+ httpServer?.close();
1245
+ process.exit(0);
1246
+ };
1247
+ process.on("SIGTERM", shutdown);
1248
+ process.on("SIGINT", shutdown);
1249
+ }
1250
+ // License info helper — reads ~/.archbyte/credentials.json
1251
+ function loadLicenseInfo() {
1252
+ const defaults = {
1253
+ loggedIn: false,
1254
+ email: null,
1255
+ tier: "free",
1256
+ features: {
1257
+ analyze: true,
1258
+ validate: false,
1259
+ patrol: false,
1260
+ workflows: false,
1261
+ chat: false,
1262
+ premiumAgents: false,
1263
+ },
1264
+ };
1265
+ try {
1266
+ const home = process.env.HOME || process.env.USERPROFILE || "";
1267
+ const credPath = path.join(home, ".archbyte", "credentials.json");
1268
+ if (!existsSync(credPath))
1269
+ return defaults;
1270
+ const creds = JSON.parse(readFileSync(credPath, "utf-8"));
1271
+ if (!creds.token)
1272
+ return defaults;
1273
+ // Check expiry
1274
+ if (creds.expiresAt && new Date(creds.expiresAt) < new Date()) {
1275
+ return { ...defaults, loggedIn: true, email: creds.email ?? null };
1276
+ }
1277
+ const isPremium = creds.tier === "premium";
1278
+ return {
1279
+ loggedIn: true,
1280
+ email: creds.email ?? null,
1281
+ tier: isPremium ? "premium" : "free",
1282
+ features: {
1283
+ analyze: true,
1284
+ validate: isPremium,
1285
+ patrol: isPremium,
1286
+ workflows: isPremium,
1287
+ chat: isPremium,
1288
+ premiumAgents: isPremium,
1289
+ },
1290
+ };
1291
+ }
1292
+ catch {
1293
+ return defaults;
1294
+ }
1295
+ }
1296
+ // Export start function
1297
+ export async function startServer(cfg) {
1298
+ config = cfg;
1299
+ setupShutdown();
1300
+ createHttpServer();
1301
+ try {
1302
+ await startHttpServer();
1303
+ }
1304
+ catch (err) {
1305
+ console.error("[archbyte] Failed to start HTTP server:", err);
1306
+ process.exit(1);
1307
+ }
1308
+ setupWatcher();
1309
+ console.error(`[archbyte] Serving ${config.name}`);
1310
+ console.error(`[archbyte] Diagram: ${config.diagramPath}`);
1311
+ }