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.
- package/README.md +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- 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
|
+
}
|