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
package/dist/cli/diff.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { resolveArchitecturePath, loadArchitectureFile, loadRulesConfig, getRuleLevel, getThreshold, } from "./shared.js";
|
|
4
|
+
import { checkNoLayerBypass, checkMaxConnections, checkNoOrphans, checkCircularDeps, } from "./validate.js";
|
|
5
|
+
/**
|
|
6
|
+
* Compare two architecture snapshots and show drift report.
|
|
7
|
+
*/
|
|
8
|
+
export async function handleDiff(options) {
|
|
9
|
+
if (!options.baseline) {
|
|
10
|
+
console.error(chalk.red("--baseline <path> is required"));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const baseArch = loadArchitectureFile(path.resolve(process.cwd(), options.baseline));
|
|
14
|
+
const currentPath = options.current
|
|
15
|
+
? path.resolve(process.cwd(), options.current)
|
|
16
|
+
: resolveArchitecturePath({});
|
|
17
|
+
const currentArch = loadArchitectureFile(currentPath);
|
|
18
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Diff — ${projectName}`));
|
|
21
|
+
console.log(chalk.gray(` Baseline: ${options.baseline}`));
|
|
22
|
+
console.log(chalk.gray(` Current: ${options.current || ".archbyte/architecture.json"}`));
|
|
23
|
+
console.log();
|
|
24
|
+
// ── Components ──
|
|
25
|
+
const baseNodes = new Set(baseArch.nodes.map((n) => n.id));
|
|
26
|
+
const currentNodes = new Set(currentArch.nodes.map((n) => n.id));
|
|
27
|
+
const added = [...currentNodes].filter((id) => !baseNodes.has(id));
|
|
28
|
+
const removed = [...baseNodes].filter((id) => !currentNodes.has(id));
|
|
29
|
+
const currentNodeMap = new Map();
|
|
30
|
+
for (const n of currentArch.nodes)
|
|
31
|
+
currentNodeMap.set(n.id, n);
|
|
32
|
+
const baseNodeMap = new Map();
|
|
33
|
+
for (const n of baseArch.nodes)
|
|
34
|
+
baseNodeMap.set(n.id, n);
|
|
35
|
+
console.log(chalk.bold(" Components:"));
|
|
36
|
+
if (added.length === 0 && removed.length === 0) {
|
|
37
|
+
console.log(chalk.green(" No changes"));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
for (const id of added) {
|
|
41
|
+
const node = currentNodeMap.get(id);
|
|
42
|
+
console.log(chalk.green(` + ${node?.label || id} (${node?.layer || "unknown"})`));
|
|
43
|
+
}
|
|
44
|
+
for (const id of removed) {
|
|
45
|
+
const node = baseNodeMap.get(id);
|
|
46
|
+
console.log(chalk.red(` - ${node?.label || id} (${node?.layer || "unknown"})`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log();
|
|
50
|
+
// ── Connections ──
|
|
51
|
+
const edgeKey = (e) => `${e.source}->${e.target}`;
|
|
52
|
+
const baseEdges = new Set(baseArch.edges.map(edgeKey));
|
|
53
|
+
const currentEdges = new Set(currentArch.edges.map(edgeKey));
|
|
54
|
+
const addedEdges = currentArch.edges.filter((e) => !baseEdges.has(edgeKey(e)));
|
|
55
|
+
const removedEdges = baseArch.edges.filter((e) => !currentEdges.has(edgeKey(e)));
|
|
56
|
+
console.log(chalk.bold(" Connections:"));
|
|
57
|
+
if (addedEdges.length === 0 && removedEdges.length === 0) {
|
|
58
|
+
console.log(chalk.green(" No changes"));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
for (const e of addedEdges) {
|
|
62
|
+
const src = currentNodeMap.get(e.source)?.label || e.source;
|
|
63
|
+
const tgt = currentNodeMap.get(e.target)?.label || e.target;
|
|
64
|
+
console.log(chalk.green(` + ${src} → ${tgt}`));
|
|
65
|
+
}
|
|
66
|
+
for (const e of removedEdges) {
|
|
67
|
+
const src = baseNodeMap.get(e.source)?.label || e.source;
|
|
68
|
+
const tgt = baseNodeMap.get(e.target)?.label || e.target;
|
|
69
|
+
console.log(chalk.red(` - ${src} → ${tgt}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
// ── Density Change ──
|
|
74
|
+
const calcDensity = (arch) => {
|
|
75
|
+
const n = arch.nodes.length;
|
|
76
|
+
const possible = n > 1 ? (n * (n - 1)) / 2 : 1;
|
|
77
|
+
return arch.edges.length / possible;
|
|
78
|
+
};
|
|
79
|
+
const baseDensity = calcDensity(baseArch);
|
|
80
|
+
const currentDensity = calcDensity(currentArch);
|
|
81
|
+
const densityDelta = currentDensity - baseDensity;
|
|
82
|
+
console.log(chalk.bold(" Density:"));
|
|
83
|
+
const arrow = densityDelta > 0 ? "↑" : densityDelta < 0 ? "↓" : "=";
|
|
84
|
+
const densityColor = Math.abs(densityDelta) < 0.01 ? chalk.gray : densityDelta > 0 ? chalk.yellow : chalk.green;
|
|
85
|
+
console.log(densityColor(` ${baseDensity.toFixed(3)} → ${currentDensity.toFixed(3)} (${arrow} ${Math.abs(densityDelta).toFixed(3)})`));
|
|
86
|
+
console.log();
|
|
87
|
+
// ── Violations Diff ──
|
|
88
|
+
const config = loadRulesConfig(options.config);
|
|
89
|
+
const getViolations = (arch) => {
|
|
90
|
+
const nodes = arch.nodes;
|
|
91
|
+
const map = new Map();
|
|
92
|
+
for (const n of nodes)
|
|
93
|
+
map.set(n.id, n);
|
|
94
|
+
const violations = [];
|
|
95
|
+
const bypassLevel = getRuleLevel(config, "no-layer-bypass", "error");
|
|
96
|
+
if (bypassLevel !== "off")
|
|
97
|
+
violations.push(...checkNoLayerBypass(arch, nodes, map, bypassLevel));
|
|
98
|
+
const maxLevel = getRuleLevel(config, "max-connections", "warn");
|
|
99
|
+
if (maxLevel !== "off")
|
|
100
|
+
violations.push(...checkMaxConnections(arch, nodes, maxLevel, getThreshold(config, "max-connections", 6)));
|
|
101
|
+
const orphanLevel = getRuleLevel(config, "no-orphans", "warn");
|
|
102
|
+
if (orphanLevel !== "off")
|
|
103
|
+
violations.push(...checkNoOrphans(arch, nodes, orphanLevel));
|
|
104
|
+
const circularLevel = getRuleLevel(config, "no-circular-deps", "error");
|
|
105
|
+
if (circularLevel !== "off")
|
|
106
|
+
violations.push(...checkCircularDeps(arch, nodes, map, circularLevel));
|
|
107
|
+
return violations;
|
|
108
|
+
};
|
|
109
|
+
const baseViolations = getViolations(baseArch);
|
|
110
|
+
const currentViolations = getViolations(currentArch);
|
|
111
|
+
const violationKey = (v) => `${v.rule}:${v.message}`;
|
|
112
|
+
const baseViolationKeys = new Set(baseViolations.map(violationKey));
|
|
113
|
+
const currentViolationKeys = new Set(currentViolations.map(violationKey));
|
|
114
|
+
const newViolations = currentViolations.filter((v) => !baseViolationKeys.has(violationKey(v)));
|
|
115
|
+
const resolvedViolations = baseViolations.filter((v) => !currentViolationKeys.has(violationKey(v)));
|
|
116
|
+
console.log(chalk.bold(" Violations:"));
|
|
117
|
+
if (newViolations.length === 0 && resolvedViolations.length === 0) {
|
|
118
|
+
console.log(chalk.green(" No changes"));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const v of newViolations) {
|
|
122
|
+
const icon = v.level === "error" ? chalk.red("NEW") : chalk.yellow("NEW");
|
|
123
|
+
console.log(` ${icon} [${v.rule}] ${v.message}`);
|
|
124
|
+
}
|
|
125
|
+
for (const v of resolvedViolations) {
|
|
126
|
+
console.log(chalk.green(` RESOLVED [${v.rule}] ${v.message}`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── Summary ──
|
|
130
|
+
console.log();
|
|
131
|
+
const totalChanges = added.length + removed.length + addedEdges.length + removedEdges.length;
|
|
132
|
+
if (totalChanges === 0 && newViolations.length === 0) {
|
|
133
|
+
console.log(chalk.green(" No architectural drift detected."));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(` Summary: ${chalk.green(`+${added.length}`)} / ${chalk.red(`-${removed.length}`)} components, ` +
|
|
137
|
+
`${chalk.green(`+${addedEdges.length}`)} / ${chalk.red(`-${removedEdges.length}`)} connections, ` +
|
|
138
|
+
`${chalk.red(`${newViolations.length} new`)} / ${chalk.green(`${resolvedViolations.length} resolved`)} violations`);
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
if (newViolations.some((v) => v.level === "error")) {
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ExportOptions {
|
|
2
|
+
diagram?: string;
|
|
3
|
+
format?: string;
|
|
4
|
+
output?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Export architecture diagram to Mermaid, Markdown, or JSON format.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleExport(options: ExportOptions): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { resolveArchitecturePath, loadArchitectureFile } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Export architecture diagram to Mermaid, Markdown, or JSON format.
|
|
5
|
+
*/
|
|
6
|
+
export async function handleExport(options) {
|
|
7
|
+
const diagramPath = resolveArchitecturePath(options);
|
|
8
|
+
const arch = loadArchitectureFile(diagramPath);
|
|
9
|
+
const format = options.format || "mermaid";
|
|
10
|
+
const SUPPORTED_FORMATS = ["mermaid", "markdown", "json", "plantuml", "dot"];
|
|
11
|
+
if (!SUPPORTED_FORMATS.includes(format)) {
|
|
12
|
+
console.error(chalk.red(`Unknown format: "${format}". Supported: ${SUPPORTED_FORMATS.join(", ")}`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
let output;
|
|
16
|
+
switch (format) {
|
|
17
|
+
case "mermaid":
|
|
18
|
+
output = exportMermaid(arch);
|
|
19
|
+
break;
|
|
20
|
+
case "json":
|
|
21
|
+
output = exportJson(arch);
|
|
22
|
+
break;
|
|
23
|
+
case "plantuml":
|
|
24
|
+
output = exportPlantUML(arch);
|
|
25
|
+
break;
|
|
26
|
+
case "dot":
|
|
27
|
+
output = exportDot(arch);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
output = exportMarkdown(arch);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (options.output) {
|
|
34
|
+
const fs = await import("fs");
|
|
35
|
+
const path = await import("path");
|
|
36
|
+
const outPath = path.resolve(process.cwd(), options.output);
|
|
37
|
+
fs.writeFileSync(outPath, output, "utf-8");
|
|
38
|
+
console.error(chalk.green(`✓ Exported to ${outPath}`));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(output);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Generate a Mermaid flowchart from the architecture.
|
|
46
|
+
*/
|
|
47
|
+
function exportMermaid(arch) {
|
|
48
|
+
const realNodes = arch.nodes;
|
|
49
|
+
// Group nodes by layer
|
|
50
|
+
const layerGroups = new Map();
|
|
51
|
+
for (const node of realNodes) {
|
|
52
|
+
const layer = node.layer || "other";
|
|
53
|
+
if (!layerGroups.has(layer)) {
|
|
54
|
+
layerGroups.set(layer, []);
|
|
55
|
+
}
|
|
56
|
+
layerGroups.get(layer).push(node);
|
|
57
|
+
}
|
|
58
|
+
const lines = ["graph TD"];
|
|
59
|
+
// Desired layer ordering
|
|
60
|
+
const layerOrder = [
|
|
61
|
+
"presentation",
|
|
62
|
+
"application",
|
|
63
|
+
"data",
|
|
64
|
+
"external",
|
|
65
|
+
"deployment",
|
|
66
|
+
];
|
|
67
|
+
for (const layer of layerOrder) {
|
|
68
|
+
const nodes = layerGroups.get(layer);
|
|
69
|
+
if (!nodes || nodes.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const label = layer.charAt(0).toUpperCase() + layer.slice(1);
|
|
72
|
+
lines.push(` subgraph ${label}`);
|
|
73
|
+
for (const node of nodes) {
|
|
74
|
+
const id = sanitizeMermaidId(node.id);
|
|
75
|
+
const techStr = node.techStack && node.techStack.length > 0
|
|
76
|
+
? `<br/>${node.techStack.join(", ")}`
|
|
77
|
+
: "";
|
|
78
|
+
if (node.type === "database") {
|
|
79
|
+
lines.push(` ${id}[("${node.label}${techStr}")]`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
lines.push(` ${id}["${node.label}${techStr}"]`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push(" end");
|
|
86
|
+
}
|
|
87
|
+
// Edges
|
|
88
|
+
for (const edge of arch.edges) {
|
|
89
|
+
const src = sanitizeMermaidId(edge.source);
|
|
90
|
+
const tgt = sanitizeMermaidId(edge.target);
|
|
91
|
+
if (edge.label) {
|
|
92
|
+
lines.push(` ${src} -->|${edge.label}| ${tgt}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push(` ${src} --> ${tgt}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Generate a structured Markdown document from the architecture.
|
|
102
|
+
*/
|
|
103
|
+
function exportMarkdown(arch) {
|
|
104
|
+
const realNodes = arch.nodes;
|
|
105
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
106
|
+
const lines = [];
|
|
107
|
+
lines.push(`# Architecture — ${projectName}`);
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(`> Generated by ArchByte on ${new Date().toISOString().slice(0, 10)}`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
// Summary
|
|
112
|
+
const components = realNodes.filter((n) => n.type === "component" || n.type === "service");
|
|
113
|
+
const databases = realNodes.filter((n) => n.type === "database");
|
|
114
|
+
const externals = realNodes.filter((n) => n.type === "external");
|
|
115
|
+
lines.push("## Summary");
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push(`- **Components:** ${components.length}`);
|
|
118
|
+
lines.push(`- **Databases:** ${databases.length}`);
|
|
119
|
+
lines.push(`- **External Services:** ${externals.length}`);
|
|
120
|
+
lines.push(`- **Connections:** ${arch.edges.length}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
// Component table
|
|
123
|
+
lines.push("## Components");
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("| Name | Type | Layer | Tech Stack |");
|
|
126
|
+
lines.push("|------|------|-------|------------|");
|
|
127
|
+
for (const node of realNodes) {
|
|
128
|
+
const tech = node.techStack && node.techStack.length > 0
|
|
129
|
+
? node.techStack.join(", ")
|
|
130
|
+
: "—";
|
|
131
|
+
lines.push(`| ${node.label} | ${node.type} | ${node.layer} | ${tech} |`);
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
// Connections
|
|
135
|
+
const nodeMap = new Map();
|
|
136
|
+
for (const node of realNodes) {
|
|
137
|
+
nodeMap.set(node.id, node);
|
|
138
|
+
}
|
|
139
|
+
lines.push("## Connections");
|
|
140
|
+
lines.push("");
|
|
141
|
+
if (arch.edges.length === 0) {
|
|
142
|
+
lines.push("_No connections defined._");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
for (const edge of arch.edges) {
|
|
146
|
+
const src = nodeMap.get(edge.source)?.label || edge.source;
|
|
147
|
+
const tgt = nodeMap.get(edge.target)?.label || edge.target;
|
|
148
|
+
const label = edge.label ? ` — ${edge.label}` : "";
|
|
149
|
+
lines.push(`- **${src}** → **${tgt}**${label}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Export architecture as clean semantic JSON, stripping layout coordinates.
|
|
157
|
+
*/
|
|
158
|
+
function exportJson(arch) {
|
|
159
|
+
const realNodes = arch.nodes;
|
|
160
|
+
const components = realNodes.map((node) => {
|
|
161
|
+
const clean = {
|
|
162
|
+
id: node.id,
|
|
163
|
+
type: node.type,
|
|
164
|
+
label: node.label,
|
|
165
|
+
layer: node.layer,
|
|
166
|
+
};
|
|
167
|
+
if (node.path)
|
|
168
|
+
clean.path = node.path;
|
|
169
|
+
if (node.techStack && node.techStack.length > 0)
|
|
170
|
+
clean.techStack = node.techStack;
|
|
171
|
+
if (node.description)
|
|
172
|
+
clean.description = node.description;
|
|
173
|
+
if (node.environments && node.environments.length > 0)
|
|
174
|
+
clean.environments = node.environments;
|
|
175
|
+
return clean;
|
|
176
|
+
});
|
|
177
|
+
const connections = arch.edges.map((edge) => {
|
|
178
|
+
const clean = {
|
|
179
|
+
id: edge.id,
|
|
180
|
+
source: edge.source,
|
|
181
|
+
target: edge.target,
|
|
182
|
+
};
|
|
183
|
+
if (edge.label)
|
|
184
|
+
clean.label = edge.label;
|
|
185
|
+
if (edge.environments && edge.environments.length > 0)
|
|
186
|
+
clean.environments = edge.environments;
|
|
187
|
+
return clean;
|
|
188
|
+
});
|
|
189
|
+
const result = {
|
|
190
|
+
components,
|
|
191
|
+
connections,
|
|
192
|
+
};
|
|
193
|
+
if (arch.flows && arch.flows.length > 0) {
|
|
194
|
+
result.flows = arch.flows;
|
|
195
|
+
}
|
|
196
|
+
if (arch.environments) {
|
|
197
|
+
result.environments = arch.environments;
|
|
198
|
+
}
|
|
199
|
+
result.metadata = {
|
|
200
|
+
lastUpdated: arch.lastUpdated,
|
|
201
|
+
version: arch.version,
|
|
202
|
+
};
|
|
203
|
+
return JSON.stringify(result, null, 2);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Generate a PlantUML component diagram from the architecture.
|
|
207
|
+
*/
|
|
208
|
+
function exportPlantUML(arch) {
|
|
209
|
+
const realNodes = arch.nodes;
|
|
210
|
+
const nodeMap = new Map();
|
|
211
|
+
for (const n of realNodes)
|
|
212
|
+
nodeMap.set(n.id, n);
|
|
213
|
+
const lines = ["@startuml Architecture"];
|
|
214
|
+
lines.push("!theme plain");
|
|
215
|
+
lines.push("skinparam componentStyle rectangle");
|
|
216
|
+
lines.push("");
|
|
217
|
+
const layerOrder = [
|
|
218
|
+
"presentation",
|
|
219
|
+
"application",
|
|
220
|
+
"data",
|
|
221
|
+
"external",
|
|
222
|
+
"deployment",
|
|
223
|
+
];
|
|
224
|
+
const layerGroups = new Map();
|
|
225
|
+
for (const node of realNodes) {
|
|
226
|
+
const layer = node.layer || "other";
|
|
227
|
+
if (!layerGroups.has(layer))
|
|
228
|
+
layerGroups.set(layer, []);
|
|
229
|
+
layerGroups.get(layer).push(node);
|
|
230
|
+
}
|
|
231
|
+
for (const layer of layerOrder) {
|
|
232
|
+
const nodes = layerGroups.get(layer);
|
|
233
|
+
if (!nodes || nodes.length === 0)
|
|
234
|
+
continue;
|
|
235
|
+
const label = layer.charAt(0).toUpperCase() + layer.slice(1);
|
|
236
|
+
lines.push(`package "${label}" {`);
|
|
237
|
+
for (const node of nodes) {
|
|
238
|
+
const type = node.type === "database" ? "database" : "component";
|
|
239
|
+
const id = sanitizePlantUMLId(node.id);
|
|
240
|
+
const name = node.label.split("\\n")[0];
|
|
241
|
+
lines.push(` ${type} "${name}" as ${id}`);
|
|
242
|
+
}
|
|
243
|
+
lines.push("}");
|
|
244
|
+
lines.push("");
|
|
245
|
+
}
|
|
246
|
+
for (const edge of arch.edges) {
|
|
247
|
+
const src = sanitizePlantUMLId(edge.source);
|
|
248
|
+
const tgt = sanitizePlantUMLId(edge.target);
|
|
249
|
+
const label = edge.label ? ` : ${edge.label}` : "";
|
|
250
|
+
lines.push(`${src} --> ${tgt}${label}`);
|
|
251
|
+
}
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push("@enduml");
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Generate a DOT/Graphviz directed graph from the architecture.
|
|
258
|
+
*/
|
|
259
|
+
function exportDot(arch) {
|
|
260
|
+
const realNodes = arch.nodes;
|
|
261
|
+
const layerFills = {
|
|
262
|
+
presentation: "#d0ebff",
|
|
263
|
+
application: "#ffe8cc",
|
|
264
|
+
data: "#b2f2bb",
|
|
265
|
+
external: "#eebefa",
|
|
266
|
+
deployment: "#ffe3e3",
|
|
267
|
+
};
|
|
268
|
+
const lines = ["digraph Architecture {"];
|
|
269
|
+
lines.push(" rankdir=TB;");
|
|
270
|
+
lines.push(' node [shape=box, style="rounded,filled", fontname="Helvetica"];');
|
|
271
|
+
lines.push("");
|
|
272
|
+
const layerOrder = [
|
|
273
|
+
"presentation",
|
|
274
|
+
"application",
|
|
275
|
+
"data",
|
|
276
|
+
"external",
|
|
277
|
+
"deployment",
|
|
278
|
+
];
|
|
279
|
+
const layerGroups = new Map();
|
|
280
|
+
for (const node of realNodes) {
|
|
281
|
+
const l = node.layer || "other";
|
|
282
|
+
if (!layerGroups.has(l))
|
|
283
|
+
layerGroups.set(l, []);
|
|
284
|
+
layerGroups.get(l).push(node);
|
|
285
|
+
}
|
|
286
|
+
for (const layer of layerOrder) {
|
|
287
|
+
const nodes = layerGroups.get(layer);
|
|
288
|
+
if (!nodes || nodes.length === 0)
|
|
289
|
+
continue;
|
|
290
|
+
const label = layer.charAt(0).toUpperCase() + layer.slice(1);
|
|
291
|
+
lines.push(` subgraph cluster_${layer} {`);
|
|
292
|
+
lines.push(` label="${label}";`);
|
|
293
|
+
lines.push(` style=filled; color="${layerFills[layer] || "#f0f0f0"}";`);
|
|
294
|
+
for (const node of nodes) {
|
|
295
|
+
const shape = node.type === "database" ? "cylinder" : "box";
|
|
296
|
+
const name = node.label.split("\\n")[0];
|
|
297
|
+
lines.push(` "${node.id}" [label="${name}", shape=${shape}, fillcolor="${layerFills[layer] || "#ffffff"}"];`);
|
|
298
|
+
}
|
|
299
|
+
lines.push(" }");
|
|
300
|
+
}
|
|
301
|
+
lines.push("");
|
|
302
|
+
for (const edge of arch.edges) {
|
|
303
|
+
const label = edge.label ? ` [label="${edge.label}"]` : "";
|
|
304
|
+
lines.push(` "${edge.source}" -> "${edge.target}"${label};`);
|
|
305
|
+
}
|
|
306
|
+
lines.push("}");
|
|
307
|
+
return lines.join("\n");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Sanitize an ID for use in PlantUML diagrams.
|
|
311
|
+
*/
|
|
312
|
+
function sanitizePlantUMLId(id) {
|
|
313
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Sanitize an ID for use in Mermaid diagrams.
|
|
317
|
+
* Mermaid IDs must be alphanumeric (with hyphens/underscores).
|
|
318
|
+
*/
|
|
319
|
+
function sanitizeMermaidId(id) {
|
|
320
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
321
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth gate command for Claude Code integration.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* archbyte gate analyze → check if action is allowed (JSON stdout, exit 0 or 1)
|
|
6
|
+
* archbyte gate --record → record a completed scan cycle
|
|
7
|
+
*
|
|
8
|
+
* Outputs JSON to stdout for the Claude command to parse.
|
|
9
|
+
* Uses exit code 1 for denied/error, exit code 0 for allowed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function handleGate(action: string | undefined, options: {
|
|
12
|
+
record?: boolean;
|
|
13
|
+
}): Promise<void>;
|
package/dist/cli/gate.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { loadCredentials, cacheVerifiedTier, resetOfflineActions, checkOfflineAction } from "./auth.js";
|
|
2
|
+
const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
|
|
3
|
+
/**
|
|
4
|
+
* Auth gate command for Claude Code integration.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* archbyte gate analyze → check if action is allowed (JSON stdout, exit 0 or 1)
|
|
8
|
+
* archbyte gate --record → record a completed scan cycle
|
|
9
|
+
*
|
|
10
|
+
* Outputs JSON to stdout for the Claude command to parse.
|
|
11
|
+
* Uses exit code 1 for denied/error, exit code 0 for allowed.
|
|
12
|
+
*/
|
|
13
|
+
export async function handleGate(action, options) {
|
|
14
|
+
if (options.record) {
|
|
15
|
+
await recordScan();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!action) {
|
|
19
|
+
const result = { allowed: false, reason: "Usage: archbyte gate <action> or archbyte gate --record" };
|
|
20
|
+
console.log(JSON.stringify(result));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
await checkGate(action);
|
|
24
|
+
}
|
|
25
|
+
async function checkGate(action) {
|
|
26
|
+
const creds = loadCredentials();
|
|
27
|
+
if (!creds) {
|
|
28
|
+
const result = {
|
|
29
|
+
allowed: false,
|
|
30
|
+
reason: "Not logged in. Run `archbyte login` to sign in.",
|
|
31
|
+
};
|
|
32
|
+
console.log(JSON.stringify(result));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (new Date(creds.expiresAt) < new Date()) {
|
|
36
|
+
const result = {
|
|
37
|
+
allowed: false,
|
|
38
|
+
reason: "Session expired. Run `archbyte login` to refresh.",
|
|
39
|
+
};
|
|
40
|
+
console.log(JSON.stringify(result));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${API_BASE}/api/v1/check-usage`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${creds.token}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({ action }),
|
|
51
|
+
signal: AbortSignal.timeout(5000),
|
|
52
|
+
});
|
|
53
|
+
if (res.status === 401) {
|
|
54
|
+
const result = {
|
|
55
|
+
allowed: false,
|
|
56
|
+
reason: "Session invalid. Run `archbyte login` to sign in again.",
|
|
57
|
+
};
|
|
58
|
+
console.log(JSON.stringify(result));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
return handleOfflineGate("server error");
|
|
63
|
+
}
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const tier = data.tier === "premium" ? "premium" : "free";
|
|
66
|
+
cacheVerifiedTier(tier, creds.email);
|
|
67
|
+
resetOfflineActions();
|
|
68
|
+
if (!data.allowed) {
|
|
69
|
+
const result = {
|
|
70
|
+
allowed: false,
|
|
71
|
+
tier: data.tier,
|
|
72
|
+
reason: data.message ?? "Scan not allowed. Check your account status.",
|
|
73
|
+
};
|
|
74
|
+
console.log(JSON.stringify(result));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const result = {
|
|
78
|
+
allowed: true,
|
|
79
|
+
tier: data.tier,
|
|
80
|
+
remaining: data.remaining,
|
|
81
|
+
};
|
|
82
|
+
console.log(JSON.stringify(result));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const reason = err instanceof Error &&
|
|
86
|
+
(err.name === "TimeoutError" || err.name === "AbortError")
|
|
87
|
+
? "timeout" : "network error";
|
|
88
|
+
return handleOfflineGate(reason);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function handleOfflineGate(reason) {
|
|
92
|
+
const { allowed, reason: blockReason } = checkOfflineAction();
|
|
93
|
+
if (!allowed) {
|
|
94
|
+
const result = {
|
|
95
|
+
allowed: false,
|
|
96
|
+
reason: `License server unreachable (${reason}). ${blockReason ?? "Offline actions not permitted."}`,
|
|
97
|
+
};
|
|
98
|
+
console.log(JSON.stringify(result));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
// Premium user within offline grace window
|
|
102
|
+
const result = {
|
|
103
|
+
allowed: true,
|
|
104
|
+
tier: "premium",
|
|
105
|
+
remaining: -1,
|
|
106
|
+
};
|
|
107
|
+
console.log(JSON.stringify(result));
|
|
108
|
+
}
|
|
109
|
+
async function recordScan() {
|
|
110
|
+
const creds = loadCredentials();
|
|
111
|
+
if (!creds) {
|
|
112
|
+
console.log(JSON.stringify({ recorded: false }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await fetch(`${API_BASE}/api/v1/scans`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${creds.token}`,
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({}),
|
|
123
|
+
signal: AbortSignal.timeout(5000),
|
|
124
|
+
});
|
|
125
|
+
console.log(JSON.stringify({ recorded: true }));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Best-effort — still report success to not block the pipeline
|
|
129
|
+
console.log(JSON.stringify({ recorded: false }));
|
|
130
|
+
}
|
|
131
|
+
}
|