archbyte 0.4.2 → 0.5.1
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 +9 -25
- package/bin/archbyte.js +6 -41
- package/dist/agents/static/component-detector.js +71 -107
- package/dist/agents/static/connection-mapper.js +24 -25
- package/dist/agents/static/deep-drill.d.ts +72 -0
- package/dist/agents/static/deep-drill.js +388 -0
- package/dist/agents/static/doc-parser.js +73 -48
- package/dist/agents/static/env-detector.js +3 -6
- package/dist/agents/static/event-detector.js +20 -26
- package/dist/agents/static/infra-analyzer.js +15 -1
- package/dist/agents/static/structure-scanner.js +56 -57
- package/dist/agents/static/taxonomy.d.ts +19 -0
- package/dist/agents/static/taxonomy.js +147 -0
- package/dist/agents/tools/local-fs.js +5 -2
- package/dist/cli/analyze.js +49 -27
- package/dist/cli/license-gate.js +47 -19
- package/dist/cli/run.js +117 -1
- package/dist/cli/setup.d.ts +6 -1
- package/dist/cli/setup.js +35 -16
- package/dist/cli/shared.d.ts +0 -11
- package/dist/cli/shared.js +0 -61
- package/dist/cli/workflow.js +8 -15
- package/dist/server/src/index.js +276 -168
- package/package.json +2 -2
- package/templates/archbyte.yaml +28 -7
- package/ui/dist/assets/index-BQouokNH.css +1 -0
- package/ui/dist/assets/index-QllGSFhe.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/cli/arch-diff.d.ts +0 -38
- package/dist/cli/arch-diff.js +0 -61
- package/dist/cli/diff.d.ts +0 -10
- package/dist/cli/diff.js +0 -144
- package/dist/cli/patrol.d.ts +0 -18
- package/dist/cli/patrol.js +0 -596
- package/dist/cli/validate.d.ts +0 -53
- package/dist/cli/validate.js +0 -299
- package/ui/dist/assets/index-DDCNauh7.css +0 -1
- package/ui/dist/assets/index-DO4t5Xu1.js +0 -72
package/dist/server/src/index.js
CHANGED
|
@@ -15,13 +15,35 @@ let config;
|
|
|
15
15
|
let sseClients = new Set();
|
|
16
16
|
let diagramWatcher = null;
|
|
17
17
|
let isAnalyzing = false;
|
|
18
|
+
// Source file watcher — detects changes to notify UI for manual re-analysis
|
|
19
|
+
let sourceWatcher = null;
|
|
20
|
+
const pendingSourceChanges = new Map();
|
|
21
|
+
let changeDebounceTimer = null;
|
|
22
|
+
const CHANGE_DEBOUNCE_MS = 2500;
|
|
23
|
+
function pendingChangesPath() {
|
|
24
|
+
return path.join(config.workspaceRoot, ".archbyte", "pending-changes.json");
|
|
25
|
+
}
|
|
26
|
+
function savePendingChanges() {
|
|
27
|
+
const entries = Array.from(pendingSourceChanges.values());
|
|
28
|
+
writeFile(pendingChangesPath(), JSON.stringify(entries), "utf-8").catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
function loadPendingChanges() {
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(pendingChangesPath(), "utf-8");
|
|
33
|
+
const entries = JSON.parse(raw);
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
pendingSourceChanges.set(entry.path, entry);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// No persisted changes — fresh start
|
|
40
|
+
}
|
|
41
|
+
}
|
|
18
42
|
const sessionChanges = [];
|
|
19
43
|
const MAX_SESSION_CHANGES = 50;
|
|
20
44
|
let currentArchitecture = null;
|
|
21
45
|
// Process tracking for run-from-UI
|
|
22
46
|
const runningWorkflows = new Map();
|
|
23
|
-
let patrolProcess = null;
|
|
24
|
-
let patrolRunning = false;
|
|
25
47
|
let chatProcess = null;
|
|
26
48
|
// Resolve archbyte CLI binary path
|
|
27
49
|
function getArchbyteBin() {
|
|
@@ -30,7 +52,7 @@ function getArchbyteBin() {
|
|
|
30
52
|
}
|
|
31
53
|
catch {
|
|
32
54
|
// Fallback: resolve relative to this package
|
|
33
|
-
return path.resolve(__dirname, "../../../
|
|
55
|
+
return path.resolve(__dirname, "../../../bin/archbyte.js");
|
|
34
56
|
}
|
|
35
57
|
}
|
|
36
58
|
// Broadcast ops event to SSE clients
|
|
@@ -257,81 +279,76 @@ function createHttpServer() {
|
|
|
257
279
|
}));
|
|
258
280
|
return;
|
|
259
281
|
}
|
|
260
|
-
// API:
|
|
261
|
-
if (url.startsWith("/api/
|
|
282
|
+
// API: Deep Drill — focused static analysis of a single component
|
|
283
|
+
if (url.startsWith("/api/component/") && url.includes("/deep") && req.method === "GET") {
|
|
262
284
|
const arch = currentArchitecture || (await loadArchitecture());
|
|
263
285
|
if (!arch) {
|
|
264
286
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
265
287
|
res.end(JSON.stringify({ error: "No architecture loaded" }));
|
|
266
288
|
return;
|
|
267
289
|
}
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const reverse = new Map();
|
|
276
|
-
for (const nd of arch.nodes) {
|
|
277
|
-
forward.set(nd.id, []);
|
|
278
|
-
reverse.set(nd.id, []);
|
|
290
|
+
// Extract component ID: /api/component/{id}/deep
|
|
291
|
+
const urlPath = url.split("?")[0];
|
|
292
|
+
const match = urlPath.match(/^\/api\/component\/(.+)\/deep$/);
|
|
293
|
+
if (!match) {
|
|
294
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
295
|
+
res.end(JSON.stringify({ error: "Invalid URL format" }));
|
|
296
|
+
return;
|
|
279
297
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
298
|
+
const componentId = decodeURIComponent(match[1]);
|
|
299
|
+
const node = arch.nodes.find((n) => n.id === componentId);
|
|
300
|
+
if (!node) {
|
|
301
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
302
|
+
res.end(JSON.stringify({ error: `Component not found: ${componentId}` }));
|
|
303
|
+
return;
|
|
283
304
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
const result = [];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
305
|
+
const nodePath = node.path;
|
|
306
|
+
if (!nodePath) {
|
|
307
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
308
|
+
res.end(JSON.stringify({ error: `Component "${node.label}" has no path` }));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const { runDeepDrill } = await import("../../agents/static/deep-drill.js");
|
|
313
|
+
const result = await runDeepDrill(config.workspaceRoot, componentId, node.label.split("\n")[0], nodePath);
|
|
314
|
+
// Enrich with connection data from architecture edges
|
|
315
|
+
const outgoing = [];
|
|
316
|
+
const incoming = [];
|
|
317
|
+
const nodeMap = new Map();
|
|
318
|
+
for (const n of arch.nodes)
|
|
319
|
+
nodeMap.set(n.id, n);
|
|
320
|
+
for (const edge of arch.edges) {
|
|
321
|
+
if (edge.source === componentId) {
|
|
322
|
+
const target = nodeMap.get(edge.target);
|
|
323
|
+
outgoing.push({
|
|
324
|
+
targetId: edge.target,
|
|
325
|
+
targetName: target?.label.split("\n")[0] || edge.target,
|
|
326
|
+
type: edge.label || "depends-on",
|
|
327
|
+
description: edge.label || "",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (edge.target === componentId) {
|
|
331
|
+
const source = nodeMap.get(edge.source);
|
|
332
|
+
incoming.push({
|
|
333
|
+
sourceId: edge.source,
|
|
334
|
+
sourceName: source?.label.split("\n")[0] || edge.source,
|
|
335
|
+
type: edge.label || "depends-on",
|
|
336
|
+
description: edge.label || "",
|
|
337
|
+
});
|
|
304
338
|
}
|
|
305
339
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
affectedSet.add(n.id);
|
|
314
|
-
for (const n of downstream)
|
|
315
|
-
affectedSet.add(n.id);
|
|
316
|
-
// Affected edges: edges between any two nodes in the affected set
|
|
317
|
-
const affectedEdges = [];
|
|
318
|
-
for (const edge of arch.edges) {
|
|
319
|
-
if (affectedSet.has(edge.source) && affectedSet.has(edge.target)) {
|
|
320
|
-
affectedEdges.push(edge.id);
|
|
321
|
-
}
|
|
340
|
+
result.connections = { outgoing, incoming };
|
|
341
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
342
|
+
res.end(JSON.stringify(result));
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
346
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
322
347
|
}
|
|
323
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
324
|
-
res.end(JSON.stringify({
|
|
325
|
-
nodeId,
|
|
326
|
-
upstream,
|
|
327
|
-
downstream,
|
|
328
|
-
affectedEdges,
|
|
329
|
-
totalAffected: affectedSet.size - 1,
|
|
330
|
-
}));
|
|
331
348
|
return;
|
|
332
349
|
}
|
|
333
350
|
// API: Audit — unified validation + health endpoint
|
|
334
|
-
if (
|
|
351
|
+
if (url === "/api/audit" && req.method === "GET") {
|
|
335
352
|
const arch = currentArchitecture || (await loadArchitecture());
|
|
336
353
|
if (!arch) {
|
|
337
354
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -937,51 +954,13 @@ function createHttpServer() {
|
|
|
937
954
|
res.end(content);
|
|
938
955
|
return;
|
|
939
956
|
}
|
|
940
|
-
// API: Patrol history
|
|
941
|
-
if (url === "/api/patrol/latest" && req.method === "GET") {
|
|
942
|
-
const latestPath = path.join(config.workspaceRoot, ".archbyte/patrols/latest.json");
|
|
943
|
-
if (!existsSync(latestPath)) {
|
|
944
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
945
|
-
res.end(JSON.stringify(null));
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
try {
|
|
949
|
-
const content = readFileSync(latestPath, "utf-8");
|
|
950
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
951
|
-
res.end(content);
|
|
952
|
-
}
|
|
953
|
-
catch {
|
|
954
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
955
|
-
res.end(JSON.stringify(null));
|
|
956
|
-
}
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
if (url === "/api/patrol/history" && req.method === "GET") {
|
|
960
|
-
const historyPath = path.join(config.workspaceRoot, ".archbyte/patrols/history.jsonl");
|
|
961
|
-
if (!existsSync(historyPath)) {
|
|
962
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
963
|
-
res.end(JSON.stringify([]));
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
try {
|
|
967
|
-
const content = readFileSync(historyPath, "utf-8");
|
|
968
|
-
const records = content.trim().split("\n").filter(Boolean).slice(-50).map((line) => JSON.parse(line));
|
|
969
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
970
|
-
res.end(JSON.stringify(records));
|
|
971
|
-
}
|
|
972
|
-
catch {
|
|
973
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
974
|
-
res.end(JSON.stringify([]));
|
|
975
|
-
}
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
957
|
// API: Workflow list and status
|
|
979
958
|
if (url === "/api/workflow/list" && req.method === "GET") {
|
|
980
959
|
const workflows = [];
|
|
981
960
|
// Built-in workflows
|
|
982
961
|
const builtins = [
|
|
983
|
-
{ id: "full-analysis", name: "Full Analysis Pipeline", description: "Complete architecture pipeline", steps: ["generate", "
|
|
984
|
-
{ id: "ci-check", name: "CI Architecture Check", description: "Lightweight CI pipeline", steps: ["
|
|
962
|
+
{ id: "full-analysis", name: "Full Analysis Pipeline", description: "Complete architecture pipeline", steps: ["generate", "stats", "export"] },
|
|
963
|
+
{ id: "ci-check", name: "CI Architecture Check", description: "Lightweight CI pipeline", steps: ["stats"] },
|
|
985
964
|
{ id: "drift-check", name: "Architecture Drift Check", description: "Check for architecture drift", steps: ["snapshot", "generate", "diff"] },
|
|
986
965
|
];
|
|
987
966
|
for (const b of builtins) {
|
|
@@ -1168,6 +1147,18 @@ function createHttpServer() {
|
|
|
1168
1147
|
res.end(JSON.stringify({ running: runningWorkflows.has("__generate__") }));
|
|
1169
1148
|
return;
|
|
1170
1149
|
}
|
|
1150
|
+
// API: Reload — re-read architecture, reconcile pending changes
|
|
1151
|
+
if (url === "/api/reload" && req.method === "POST") {
|
|
1152
|
+
currentArchitecture = await loadArchitecture();
|
|
1153
|
+
reconcilePendingWithGit();
|
|
1154
|
+
broadcastUpdate();
|
|
1155
|
+
if (pendingSourceChanges.size > 0) {
|
|
1156
|
+
broadcastPendingChanges();
|
|
1157
|
+
}
|
|
1158
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1159
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1171
1162
|
// API: Run analyze (static or LLM) + generate
|
|
1172
1163
|
if (url === "/api/analyze" && req.method === "POST") {
|
|
1173
1164
|
if (isAnalyzing) {
|
|
@@ -1197,6 +1188,21 @@ function createHttpServer() {
|
|
|
1197
1188
|
res.end(JSON.stringify(sessionChanges));
|
|
1198
1189
|
return;
|
|
1199
1190
|
}
|
|
1191
|
+
// API: Pending source file changes — hydrates UI on reconnect
|
|
1192
|
+
if (url === "/api/changes/pending" && req.method === "GET") {
|
|
1193
|
+
const changes = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
|
|
1194
|
+
const count = changes.length;
|
|
1195
|
+
const { componentChanges, unmapped } = mapFilesToComponents(changes, currentArchitecture);
|
|
1196
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1197
|
+
res.end(JSON.stringify({
|
|
1198
|
+
count,
|
|
1199
|
+
files: changes.slice(0, 20).map((c) => ({ event: c.event, path: c.path })),
|
|
1200
|
+
componentChanges,
|
|
1201
|
+
unmapped,
|
|
1202
|
+
truncated: count > 20,
|
|
1203
|
+
}));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1200
1206
|
// API: Run workflow
|
|
1201
1207
|
if (url.startsWith("/api/workflow/run/") && req.method === "POST") {
|
|
1202
1208
|
const id = url.split("/").pop();
|
|
@@ -1269,56 +1275,6 @@ function createHttpServer() {
|
|
|
1269
1275
|
});
|
|
1270
1276
|
return;
|
|
1271
1277
|
}
|
|
1272
|
-
// API: Start patrol
|
|
1273
|
-
if (url === "/api/patrol/start" && req.method === "POST") {
|
|
1274
|
-
if (patrolRunning) {
|
|
1275
|
-
res.writeHead(409, { "Content-Type": "application/json" });
|
|
1276
|
-
res.end(JSON.stringify({ error: "Patrol already running" }));
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
let body = "";
|
|
1280
|
-
req.on("data", (chunk) => { body += chunk.toString(); });
|
|
1281
|
-
req.on("end", () => {
|
|
1282
|
-
const { interval } = JSON.parse(body || "{}");
|
|
1283
|
-
const bin = getArchbyteBin();
|
|
1284
|
-
const args = [bin, "patrol"];
|
|
1285
|
-
if (interval)
|
|
1286
|
-
args.push("--interval", String(interval));
|
|
1287
|
-
patrolProcess = spawn(process.execPath, args, {
|
|
1288
|
-
cwd: config.workspaceRoot,
|
|
1289
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
1290
|
-
env: { ...process.env, FORCE_COLOR: "1" },
|
|
1291
|
-
});
|
|
1292
|
-
patrolRunning = true;
|
|
1293
|
-
broadcastOpsEvent({ type: "patrol:started" });
|
|
1294
|
-
patrolProcess.on("close", () => {
|
|
1295
|
-
patrolProcess = null;
|
|
1296
|
-
patrolRunning = false;
|
|
1297
|
-
broadcastOpsEvent({ type: "patrol:stopped" });
|
|
1298
|
-
});
|
|
1299
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1300
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1301
|
-
});
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
// API: Stop patrol
|
|
1305
|
-
if (url === "/api/patrol/stop" && req.method === "POST") {
|
|
1306
|
-
if (patrolProcess) {
|
|
1307
|
-
patrolProcess.kill("SIGTERM");
|
|
1308
|
-
patrolProcess = null;
|
|
1309
|
-
patrolRunning = false;
|
|
1310
|
-
broadcastOpsEvent({ type: "patrol:stopped" });
|
|
1311
|
-
}
|
|
1312
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1313
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
// API: Patrol running status
|
|
1317
|
-
if (url === "/api/patrol/running" && req.method === "GET") {
|
|
1318
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1319
|
-
res.end(JSON.stringify({ running: patrolRunning }));
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
1278
|
// API: Config — read project config (including provider settings)
|
|
1323
1279
|
if (url === "/api/config" && req.method === "GET") {
|
|
1324
1280
|
const configPath = path.join(config.workspaceRoot, ".archbyte/config.json");
|
|
@@ -1732,12 +1688,23 @@ function diffArchitectures(prev, curr) {
|
|
|
1732
1688
|
.map((e) => ({ source: e.source, target: e.target, label: e.label }));
|
|
1733
1689
|
return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
|
|
1734
1690
|
}
|
|
1735
|
-
// Run analyze → generate pipeline (used by /api/analyze
|
|
1691
|
+
// Run analyze → generate pipeline (used by /api/analyze)
|
|
1736
1692
|
function runAnalyzePipeline(mode = "static", fileChanges) {
|
|
1737
1693
|
if (isAnalyzing)
|
|
1738
1694
|
return;
|
|
1739
1695
|
isAnalyzing = true;
|
|
1740
1696
|
const pipelineStart = Date.now();
|
|
1697
|
+
// Capture and clear pending source changes
|
|
1698
|
+
if (!fileChanges) {
|
|
1699
|
+
fileChanges = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
|
|
1700
|
+
}
|
|
1701
|
+
pendingSourceChanges.clear();
|
|
1702
|
+
savePendingChanges();
|
|
1703
|
+
if (changeDebounceTimer) {
|
|
1704
|
+
clearTimeout(changeDebounceTimer);
|
|
1705
|
+
changeDebounceTimer = null;
|
|
1706
|
+
}
|
|
1707
|
+
broadcastOpsEvent({ type: "changes:cleared" });
|
|
1741
1708
|
// Snapshot current architecture for diffing after pipeline completes
|
|
1742
1709
|
const prevArchitecture = currentArchitecture
|
|
1743
1710
|
? { ...currentArchitecture, nodes: [...currentArchitecture.nodes], edges: [...currentArchitecture.edges] }
|
|
@@ -1752,10 +1719,14 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
|
|
|
1752
1719
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1753
1720
|
env: { ...process.env, FORCE_COLOR: "0" },
|
|
1754
1721
|
});
|
|
1722
|
+
let analyzeStderr = "";
|
|
1723
|
+
analyzeChild.stdout?.on("data", (d) => process.stderr.write(`[analyze] ${d}`));
|
|
1724
|
+
analyzeChild.stderr?.on("data", (d) => { analyzeStderr += d; process.stderr.write(`[analyze] ${d}`); });
|
|
1755
1725
|
analyzeChild.on("close", (analyzeCode) => {
|
|
1756
1726
|
if (analyzeCode !== 0) {
|
|
1757
1727
|
isAnalyzing = false;
|
|
1758
|
-
|
|
1728
|
+
const errMsg = analyzeStderr.trim().split("\n").pop() || `Analyze failed (exit ${analyzeCode})`;
|
|
1729
|
+
broadcastOpsEvent({ type: "analyzing:finished", code: analyzeCode, success: false, error: errMsg });
|
|
1759
1730
|
return;
|
|
1760
1731
|
}
|
|
1761
1732
|
// Chain: generate after successful analyze
|
|
@@ -1764,6 +1735,8 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
|
|
|
1764
1735
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1765
1736
|
env: { ...process.env, FORCE_COLOR: "0" },
|
|
1766
1737
|
});
|
|
1738
|
+
genChild.stdout?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
|
|
1739
|
+
genChild.stderr?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
|
|
1767
1740
|
genChild.on("close", async (genCode) => {
|
|
1768
1741
|
isAnalyzing = false;
|
|
1769
1742
|
const durationMs = Date.now() - pipelineStart;
|
|
@@ -1828,6 +1801,144 @@ function setupWatcher() {
|
|
|
1828
1801
|
broadcastUpdate();
|
|
1829
1802
|
});
|
|
1830
1803
|
}
|
|
1804
|
+
// Map changed file paths to architecture components using longest-prefix matching
|
|
1805
|
+
function mapFilesToComponents(changes, arch) {
|
|
1806
|
+
if (!arch || arch.nodes.length === 0) {
|
|
1807
|
+
return { componentChanges: [], unmapped: changes.map((c) => ({ path: c.path, event: c.event })) };
|
|
1808
|
+
}
|
|
1809
|
+
// Build sorted list of component paths (longest first for greedy matching)
|
|
1810
|
+
const componentPaths = arch.nodes
|
|
1811
|
+
.filter((n) => n.path)
|
|
1812
|
+
.map((n) => ({ id: n.id, name: n.label.split("\n")[0], path: n.path }))
|
|
1813
|
+
.sort((a, b) => b.path.length - a.path.length);
|
|
1814
|
+
const groups = new Map();
|
|
1815
|
+
const unmapped = [];
|
|
1816
|
+
for (const change of changes) {
|
|
1817
|
+
let matched = false;
|
|
1818
|
+
for (const comp of componentPaths) {
|
|
1819
|
+
const prefix = comp.path.endsWith("/") ? comp.path : `${comp.path}/`;
|
|
1820
|
+
if (change.path.startsWith(prefix) || change.path === comp.path) {
|
|
1821
|
+
if (!groups.has(comp.id)) {
|
|
1822
|
+
groups.set(comp.id, { componentId: comp.id, componentName: comp.name, files: [] });
|
|
1823
|
+
}
|
|
1824
|
+
groups.get(comp.id).files.push({ path: change.path, event: change.event });
|
|
1825
|
+
matched = true;
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (!matched) {
|
|
1830
|
+
unmapped.push({ path: change.path, event: change.event });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return {
|
|
1834
|
+
componentChanges: Array.from(groups.values()),
|
|
1835
|
+
unmapped,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
// Broadcast pending source changes to SSE clients
|
|
1839
|
+
function broadcastPendingChanges() {
|
|
1840
|
+
const changes = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
|
|
1841
|
+
const count = changes.length;
|
|
1842
|
+
const truncated = count > 20;
|
|
1843
|
+
const { componentChanges, unmapped } = mapFilesToComponents(changes, currentArchitecture);
|
|
1844
|
+
broadcastOpsEvent({
|
|
1845
|
+
type: "changes:detected",
|
|
1846
|
+
count,
|
|
1847
|
+
files: changes.slice(0, 20).map((c) => c.path),
|
|
1848
|
+
componentChanges,
|
|
1849
|
+
unmapped,
|
|
1850
|
+
truncated,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
// Setup source file watcher — watches workspace for code changes
|
|
1854
|
+
function setupSourceWatcher() {
|
|
1855
|
+
sourceWatcher = watch(config.workspaceRoot, {
|
|
1856
|
+
ignoreInitial: true,
|
|
1857
|
+
ignored: [
|
|
1858
|
+
/(^|[/\\])\../, // dotfiles/dirs
|
|
1859
|
+
"**/node_modules/**",
|
|
1860
|
+
"**/dist/**",
|
|
1861
|
+
"**/build/**",
|
|
1862
|
+
"**/out/**",
|
|
1863
|
+
"**/.archbyte/**",
|
|
1864
|
+
"**/coverage/**",
|
|
1865
|
+
"**/*.lock",
|
|
1866
|
+
"**/package-lock.json",
|
|
1867
|
+
"**/__pycache__/**",
|
|
1868
|
+
"**/target/**",
|
|
1869
|
+
"**/vendor/**",
|
|
1870
|
+
],
|
|
1871
|
+
depth: 10,
|
|
1872
|
+
});
|
|
1873
|
+
sourceWatcher.on("all", (event, filePath) => {
|
|
1874
|
+
if (event !== "change" && event !== "add" && event !== "unlink")
|
|
1875
|
+
return;
|
|
1876
|
+
const relativePath = path.relative(config.workspaceRoot, filePath);
|
|
1877
|
+
pendingSourceChanges.set(relativePath, {
|
|
1878
|
+
event,
|
|
1879
|
+
path: relativePath,
|
|
1880
|
+
timestamp: Date.now(),
|
|
1881
|
+
});
|
|
1882
|
+
// Debounce: reset timer on each change, broadcast after settling
|
|
1883
|
+
if (changeDebounceTimer)
|
|
1884
|
+
clearTimeout(changeDebounceTimer);
|
|
1885
|
+
changeDebounceTimer = setTimeout(() => {
|
|
1886
|
+
changeDebounceTimer = null;
|
|
1887
|
+
broadcastPendingChanges();
|
|
1888
|
+
savePendingChanges();
|
|
1889
|
+
}, CHANGE_DEBOUNCE_MS);
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
// Watch for git commits — reconcile pending changes against actual dirty files
|
|
1893
|
+
let gitWatcher = null;
|
|
1894
|
+
let gitDebounceTimer = null;
|
|
1895
|
+
function setupGitWatcher() {
|
|
1896
|
+
const gitIndex = path.join(config.workspaceRoot, ".git", "index");
|
|
1897
|
+
if (!existsSync(gitIndex))
|
|
1898
|
+
return;
|
|
1899
|
+
gitWatcher = watch(gitIndex, { ignoreInitial: true, depth: 0 });
|
|
1900
|
+
gitWatcher.on("change", () => {
|
|
1901
|
+
// Debounce — git writes the index multiple times during a commit
|
|
1902
|
+
if (gitDebounceTimer)
|
|
1903
|
+
clearTimeout(gitDebounceTimer);
|
|
1904
|
+
gitDebounceTimer = setTimeout(() => {
|
|
1905
|
+
gitDebounceTimer = null;
|
|
1906
|
+
reconcilePendingWithGit();
|
|
1907
|
+
}, 1000);
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
function reconcilePendingWithGit() {
|
|
1911
|
+
if (pendingSourceChanges.size === 0)
|
|
1912
|
+
return;
|
|
1913
|
+
try {
|
|
1914
|
+
// Get files that are still dirty (modified, untracked, staged)
|
|
1915
|
+
const output = execSync("git status --porcelain", {
|
|
1916
|
+
cwd: config.workspaceRoot,
|
|
1917
|
+
encoding: "utf-8",
|
|
1918
|
+
timeout: 5000,
|
|
1919
|
+
});
|
|
1920
|
+
const dirtyFiles = new Set(output.trim().split("\n").filter(Boolean).map((line) => line.slice(3).trim()));
|
|
1921
|
+
let changed = false;
|
|
1922
|
+
for (const [filePath] of pendingSourceChanges) {
|
|
1923
|
+
if (!dirtyFiles.has(filePath)) {
|
|
1924
|
+
pendingSourceChanges.delete(filePath);
|
|
1925
|
+
changed = true;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (changed) {
|
|
1929
|
+
savePendingChanges();
|
|
1930
|
+
if (pendingSourceChanges.size === 0) {
|
|
1931
|
+
broadcastOpsEvent({ type: "changes:cleared" });
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
broadcastPendingChanges();
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
catch {
|
|
1939
|
+
// Not a git repo or git not available — ignore
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1831
1942
|
// Graceful shutdown
|
|
1832
1943
|
function setupShutdown() {
|
|
1833
1944
|
const shutdown = async () => {
|
|
@@ -1840,14 +1951,6 @@ function setupShutdown() {
|
|
|
1840
1951
|
catch { }
|
|
1841
1952
|
}
|
|
1842
1953
|
runningWorkflows.clear();
|
|
1843
|
-
if (patrolProcess) {
|
|
1844
|
-
try {
|
|
1845
|
-
patrolProcess.kill("SIGTERM");
|
|
1846
|
-
}
|
|
1847
|
-
catch { }
|
|
1848
|
-
patrolProcess = null;
|
|
1849
|
-
patrolRunning = false;
|
|
1850
|
-
}
|
|
1851
1954
|
if (chatProcess) {
|
|
1852
1955
|
try {
|
|
1853
1956
|
chatProcess.kill("SIGTERM");
|
|
@@ -1862,7 +1965,9 @@ function setupShutdown() {
|
|
|
1862
1965
|
catch { }
|
|
1863
1966
|
}
|
|
1864
1967
|
sseClients.clear();
|
|
1968
|
+
await sourceWatcher?.close();
|
|
1865
1969
|
await diagramWatcher?.close();
|
|
1970
|
+
await gitWatcher?.close();
|
|
1866
1971
|
httpServer?.close();
|
|
1867
1972
|
process.exit(0);
|
|
1868
1973
|
};
|
|
@@ -1878,8 +1983,7 @@ function loadLicenseInfo() {
|
|
|
1878
1983
|
tier: "free",
|
|
1879
1984
|
features: {
|
|
1880
1985
|
analyze: true,
|
|
1881
|
-
|
|
1882
|
-
patrol: false,
|
|
1986
|
+
audit: false,
|
|
1883
1987
|
workflows: false,
|
|
1884
1988
|
chat: false,
|
|
1885
1989
|
premiumAgents: false,
|
|
@@ -1931,8 +2035,7 @@ function loadLicenseInfo() {
|
|
|
1931
2035
|
tier: isPremium ? "premium" : "free",
|
|
1932
2036
|
features: {
|
|
1933
2037
|
analyze: true,
|
|
1934
|
-
|
|
1935
|
-
patrol: isPremium,
|
|
2038
|
+
audit: isPremium,
|
|
1936
2039
|
workflows: isPremium,
|
|
1937
2040
|
chat: isPremium,
|
|
1938
2041
|
premiumAgents: isPremium,
|
|
@@ -1955,7 +2058,12 @@ export async function startServer(cfg) {
|
|
|
1955
2058
|
console.error("[archbyte] Failed to start HTTP server:", err);
|
|
1956
2059
|
process.exit(1);
|
|
1957
2060
|
}
|
|
2061
|
+
currentArchitecture = await loadArchitecture();
|
|
1958
2062
|
setupWatcher();
|
|
2063
|
+
loadPendingChanges();
|
|
2064
|
+
reconcilePendingWithGit();
|
|
2065
|
+
setupSourceWatcher();
|
|
2066
|
+
setupGitWatcher();
|
|
1959
2067
|
console.error(`[archbyte] Serving ${config.name}`);
|
|
1960
2068
|
console.error(`[archbyte] Diagram: ${config.diagramPath}`);
|
|
1961
2069
|
// Listen for 'q' keypress to quit gracefully
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archbyte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "ArchByte - See what agents build. As they build it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@anthropic-ai/sdk": "^0.74.0",
|
|
39
|
-
"@google/genai": "^1.
|
|
39
|
+
"@google/genai": "^1.42.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
41
41
|
"chalk": "^5.3.0",
|
|
42
42
|
"chokidar": "^3.5.3",
|
package/templates/archbyte.yaml
CHANGED
|
@@ -102,10 +102,31 @@ rules:
|
|
|
102
102
|
# to: { type: external }
|
|
103
103
|
# level: warn
|
|
104
104
|
|
|
105
|
-
# ──
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
105
|
+
# ── Workflows ──
|
|
106
|
+
# Composable multi-step architecture pipelines.
|
|
107
|
+
# Run built-in workflows:
|
|
108
|
+
# archbyte workflow --list # see available workflows
|
|
109
|
+
# archbyte workflow --run full-analysis
|
|
110
|
+
# archbyte workflow --run ci-check
|
|
111
|
+
# archbyte workflow --run drift-check
|
|
112
|
+
#
|
|
113
|
+
# Create custom workflows in .archbyte/workflows/:
|
|
114
|
+
# archbyte workflow --create "My Pipeline"
|
|
115
|
+
#
|
|
116
|
+
# Custom workflow format (.archbyte/workflows/my-pipeline.yaml):
|
|
117
|
+
# id: my-pipeline
|
|
118
|
+
# name: "My Pipeline"
|
|
119
|
+
# description: "Custom architecture pipeline"
|
|
120
|
+
# steps:
|
|
121
|
+
# - id: generate
|
|
122
|
+
# name: "Generate Diagram"
|
|
123
|
+
# command: "archbyte generate"
|
|
124
|
+
# needs: []
|
|
125
|
+
# - id: validate
|
|
126
|
+
# name: "Validate"
|
|
127
|
+
# command: "archbyte validate"
|
|
128
|
+
# needs: [generate]
|
|
129
|
+
# - id: export
|
|
130
|
+
# name: "Export"
|
|
131
|
+
# command: "archbyte export --format mermaid"
|
|
132
|
+
# needs: [validate]
|