archbyte 0.7.1 → 0.7.3
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 +21 -82
- package/bin/archbyte.js +27 -1
- package/dist/cli/analyze.js +13 -4
- package/dist/cli/auth.d.ts +26 -0
- package/dist/cli/auth.js +85 -34
- package/dist/cli/config.js +20 -22
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +34 -27
- package/dist/cli/license-gate.js +10 -21
- package/dist/cli/output.d.ts +57 -0
- package/dist/cli/output.js +111 -0
- package/dist/cli/shared.d.ts +6 -1
- package/dist/cli/shared.js +20 -10
- package/dist/cli/stats.d.ts +36 -0
- package/dist/cli/stats.js +146 -90
- package/dist/cli/ui.js +27 -14
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +253 -0
- package/package.json +4 -2
package/dist/cli/stats.js
CHANGED
|
@@ -2,10 +2,12 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { resolveArchitecturePath, loadArchitectureFile, loadRulesConfig, getThreshold } from "./shared.js";
|
|
5
|
+
import { isJsonMode, outputSuccess } from "./output.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Compute architecture stats as a structured object.
|
|
8
|
+
* Pure data function — no console output.
|
|
7
9
|
*/
|
|
8
|
-
export
|
|
10
|
+
export function computeStats(options) {
|
|
9
11
|
const diagramPath = resolveArchitecturePath(options);
|
|
10
12
|
const arch = loadArchitectureFile(diagramPath);
|
|
11
13
|
const config = loadRulesConfig(options.config);
|
|
@@ -14,12 +16,7 @@ export async function handleStats(options) {
|
|
|
14
16
|
const databases = realNodes.filter((n) => n.type === "database");
|
|
15
17
|
const externals = realNodes.filter((n) => n.type === "external");
|
|
16
18
|
const totalConnections = arch.edges.length;
|
|
17
|
-
//
|
|
18
|
-
const projectName = process.cwd().split("/").pop() || "project";
|
|
19
|
-
console.log();
|
|
20
|
-
console.log(chalk.bold.cyan(`⚡ ArchByte Stats: ${projectName}`));
|
|
21
|
-
console.log();
|
|
22
|
-
// ── Scan Info (from metadata.json, fallback to analysis.json) ──
|
|
19
|
+
// Scan metadata
|
|
23
20
|
const metadataJsonPath = path.join(process.cwd(), ".archbyte", "metadata.json");
|
|
24
21
|
const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
|
|
25
22
|
let scanMeta = {};
|
|
@@ -33,113 +30,80 @@ export async function handleStats(options) {
|
|
|
33
30
|
}
|
|
34
31
|
}
|
|
35
32
|
catch { /* ignore */ }
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (scanMeta.durationMs != null) {
|
|
39
|
-
parts.push(`${(scanMeta.durationMs / 1000).toFixed(1)}s`);
|
|
40
|
-
}
|
|
41
|
-
if (scanMeta.filesScanned != null) {
|
|
42
|
-
parts.push(`${scanMeta.filesScanned} files scanned`);
|
|
43
|
-
}
|
|
44
|
-
if (scanMeta.tokenUsage) {
|
|
45
|
-
const total = scanMeta.tokenUsage.input + scanMeta.tokenUsage.output;
|
|
46
|
-
parts.push(`${total.toLocaleString()} tokens`);
|
|
47
|
-
}
|
|
48
|
-
if (scanMeta.mode) {
|
|
49
|
-
parts.push(scanMeta.mode === "pipeline" ? "static + model" : "static only");
|
|
50
|
-
}
|
|
51
|
-
if (scanMeta.analyzedAt) {
|
|
52
|
-
const d = new Date(scanMeta.analyzedAt);
|
|
53
|
-
parts.push(d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }));
|
|
54
|
-
}
|
|
55
|
-
console.log(chalk.gray(` Last scan: ${parts.join(" | ")}`));
|
|
56
|
-
console.log();
|
|
57
|
-
}
|
|
58
|
-
// ── Summary ──
|
|
59
|
-
console.log(` Components: ${chalk.bold(String(components.length))} Connections: ${chalk.bold(String(totalConnections))}`);
|
|
60
|
-
console.log(` Databases: ${chalk.bold(String(databases.length))} Ext Services: ${chalk.bold(String(externals.length))}`);
|
|
61
|
-
console.log();
|
|
62
|
-
// ── By Layer ──
|
|
63
|
-
const layers = ["presentation", "application", "data", "external", "deployment"];
|
|
33
|
+
// Layer counts
|
|
34
|
+
const layerNames = ["presentation", "application", "data", "external", "deployment"];
|
|
64
35
|
const layerCounts = new Map();
|
|
65
36
|
for (const node of realNodes) {
|
|
66
37
|
layerCounts.set(node.layer, (layerCounts.get(node.layer) || 0) + 1);
|
|
67
38
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
for (const layer of layers) {
|
|
39
|
+
const layers = {};
|
|
40
|
+
for (const layer of layerNames) {
|
|
71
41
|
const count = layerCounts.get(layer) || 0;
|
|
72
|
-
if (count
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: "";
|
|
79
|
-
const label = (layer + " ").slice(0, 14);
|
|
80
|
-
console.log(chalk.gray(` ${label}${bar} ${count} ${pct}`));
|
|
42
|
+
if (count > 0) {
|
|
43
|
+
layers[layer] = {
|
|
44
|
+
count,
|
|
45
|
+
percent: realNodes.length > 0 ? Math.round((count / realNodes.length) * 100) : 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
81
48
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.log(chalk.bold(" Health Indicators:"));
|
|
85
|
-
// Connection density: edges / (n * (n-1) / 2) for undirected sense
|
|
49
|
+
// Health indicators
|
|
50
|
+
const health = [];
|
|
86
51
|
const n = realNodes.length;
|
|
87
52
|
const possibleConnections = n > 1 ? (n * (n - 1)) / 2 : 1;
|
|
88
53
|
const density = totalConnections / possibleConnections;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
54
|
+
health.push({
|
|
55
|
+
rule: "connection-density",
|
|
56
|
+
status: density < 0.5 ? "pass" : "warn",
|
|
57
|
+
message: density < 0.5
|
|
58
|
+
? `Connection density: ${density.toFixed(2)} (healthy < 0.5)`
|
|
59
|
+
: `Connection density: ${density.toFixed(2)} (threshold: 0.5)`,
|
|
60
|
+
details: { density: parseFloat(density.toFixed(4)), threshold: 0.5 },
|
|
61
|
+
});
|
|
62
|
+
// Hub detection
|
|
97
63
|
const connectionCounts = new Map();
|
|
98
64
|
for (const edge of arch.edges) {
|
|
99
65
|
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1);
|
|
100
66
|
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1);
|
|
101
67
|
}
|
|
102
68
|
const hubThreshold = getThreshold(config, "max-connections", 6);
|
|
103
|
-
|
|
69
|
+
const hubs = [];
|
|
104
70
|
for (const node of realNodes) {
|
|
105
71
|
const count = connectionCounts.get(node.id) || 0;
|
|
106
72
|
if (count >= hubThreshold) {
|
|
107
|
-
|
|
108
|
-
hubFound = true;
|
|
73
|
+
hubs.push({ label: node.label, connections: count });
|
|
109
74
|
}
|
|
110
75
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
76
|
+
health.push({
|
|
77
|
+
rule: "max-connections",
|
|
78
|
+
status: hubs.length === 0 ? "pass" : "warn",
|
|
79
|
+
message: hubs.length === 0
|
|
80
|
+
? `No hubs detected (threshold: ${hubThreshold})`
|
|
81
|
+
: `${hubs.length} hub(s) detected (threshold: ${hubThreshold})`,
|
|
82
|
+
details: { threshold: hubThreshold, hubs },
|
|
83
|
+
});
|
|
114
84
|
// Orphan detection
|
|
115
85
|
const connectedIds = new Set();
|
|
116
86
|
for (const edge of arch.edges) {
|
|
117
87
|
connectedIds.add(edge.source);
|
|
118
88
|
connectedIds.add(edge.target);
|
|
119
89
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Layer violations (presentation → data, skipping application)
|
|
90
|
+
const orphans = realNodes.filter((node) => !connectedIds.has(node.id)).map((n) => n.label);
|
|
91
|
+
health.push({
|
|
92
|
+
rule: "no-orphans",
|
|
93
|
+
status: orphans.length === 0 ? "pass" : "warn",
|
|
94
|
+
message: orphans.length === 0
|
|
95
|
+
? "No orphans detected"
|
|
96
|
+
: `${orphans.length} orphan(s) found`,
|
|
97
|
+
details: { orphans },
|
|
98
|
+
});
|
|
99
|
+
// Layer violations
|
|
131
100
|
const layerOrder = {
|
|
132
|
-
presentation: 0,
|
|
133
|
-
application: 1,
|
|
134
|
-
data: 2,
|
|
135
|
-
external: 3,
|
|
136
|
-
deployment: 4,
|
|
101
|
+
presentation: 0, application: 1, data: 2, external: 3, deployment: 4,
|
|
137
102
|
};
|
|
138
103
|
const nodeMap = new Map();
|
|
139
|
-
for (const node of realNodes)
|
|
104
|
+
for (const node of realNodes)
|
|
140
105
|
nodeMap.set(node.id, node);
|
|
141
|
-
|
|
142
|
-
let violationFound = false;
|
|
106
|
+
const violations = [];
|
|
143
107
|
for (const edge of arch.edges) {
|
|
144
108
|
const src = nodeMap.get(edge.source);
|
|
145
109
|
const tgt = nodeMap.get(edge.target);
|
|
@@ -149,14 +113,106 @@ export async function handleStats(options) {
|
|
|
149
113
|
const tgtOrder = layerOrder[tgt.layer];
|
|
150
114
|
if (srcOrder === undefined || tgtOrder === undefined)
|
|
151
115
|
continue;
|
|
152
|
-
// Only check the core layers (presentation/application/data)
|
|
153
116
|
if (srcOrder <= 1 && tgtOrder === 2 && tgtOrder - srcOrder > 1) {
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
violations.push({ from: src.label, to: tgt.label, fromLayer: src.layer, toLayer: tgt.layer });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
health.push({
|
|
121
|
+
rule: "no-layer-bypass",
|
|
122
|
+
status: violations.length === 0 ? "pass" : "warn",
|
|
123
|
+
message: violations.length === 0
|
|
124
|
+
? "No layer violations detected"
|
|
125
|
+
: `${violations.length} layer violation(s)`,
|
|
126
|
+
details: { violations },
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
summary: {
|
|
130
|
+
components: components.length,
|
|
131
|
+
databases: databases.length,
|
|
132
|
+
externals: externals.length,
|
|
133
|
+
connections: totalConnections,
|
|
134
|
+
totalNodes: realNodes.length,
|
|
135
|
+
},
|
|
136
|
+
layers,
|
|
137
|
+
health,
|
|
138
|
+
...(scanMeta.analyzedAt || scanMeta.durationMs ? { scan: scanMeta } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Print an architecture health dashboard to the terminal.
|
|
143
|
+
*/
|
|
144
|
+
export async function handleStats(options) {
|
|
145
|
+
const stats = computeStats(options);
|
|
146
|
+
if (isJsonMode()) {
|
|
147
|
+
outputSuccess(stats);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Stats: ${projectName}`));
|
|
153
|
+
console.log();
|
|
154
|
+
// ── Scan Info ──
|
|
155
|
+
if (stats.scan) {
|
|
156
|
+
const parts = [];
|
|
157
|
+
if (stats.scan.durationMs != null)
|
|
158
|
+
parts.push(`${(stats.scan.durationMs / 1000).toFixed(1)}s`);
|
|
159
|
+
if (stats.scan.filesScanned != null)
|
|
160
|
+
parts.push(`${stats.scan.filesScanned} files scanned`);
|
|
161
|
+
if (stats.scan.tokenUsage) {
|
|
162
|
+
const total = stats.scan.tokenUsage.input + stats.scan.tokenUsage.output;
|
|
163
|
+
parts.push(`${total.toLocaleString()} tokens`);
|
|
164
|
+
}
|
|
165
|
+
if (stats.scan.mode)
|
|
166
|
+
parts.push(stats.scan.mode === "pipeline" ? "static + model" : "static only");
|
|
167
|
+
if (stats.scan.analyzedAt) {
|
|
168
|
+
const d = new Date(stats.scan.analyzedAt);
|
|
169
|
+
parts.push(d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }));
|
|
170
|
+
}
|
|
171
|
+
if (parts.length > 0) {
|
|
172
|
+
console.log(chalk.gray(` Last scan: ${parts.join(" | ")}`));
|
|
173
|
+
console.log();
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
// ── Summary ──
|
|
177
|
+
console.log(` Components: ${chalk.bold(String(stats.summary.components))} Connections: ${chalk.bold(String(stats.summary.connections))}`);
|
|
178
|
+
console.log(` Databases: ${chalk.bold(String(stats.summary.databases))} Ext Services: ${chalk.bold(String(stats.summary.externals))}`);
|
|
179
|
+
console.log();
|
|
180
|
+
// ── By Layer ──
|
|
181
|
+
const maxCount = Math.max(...Object.values(stats.layers).map((l) => l.count), 1);
|
|
182
|
+
console.log(chalk.bold(" By Layer:"));
|
|
183
|
+
for (const [layer, data] of Object.entries(stats.layers)) {
|
|
184
|
+
const barLen = Math.round((data.count / maxCount) * 10);
|
|
185
|
+
const bar = "█".repeat(barLen) + "░".repeat(10 - barLen);
|
|
186
|
+
const label = (layer + " ").slice(0, 14);
|
|
187
|
+
console.log(chalk.gray(` ${label}${bar} ${data.count} (${data.percent}%)`));
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
// ── Health Indicators ──
|
|
191
|
+
console.log(chalk.bold(" Health Indicators:"));
|
|
192
|
+
for (const ind of stats.health) {
|
|
193
|
+
if (ind.status === "pass") {
|
|
194
|
+
console.log(chalk.green(` ✓ ${ind.message}`));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log(chalk.yellow(` ⚠ ${ind.message}`));
|
|
198
|
+
// Print details for hubs, orphans, violations
|
|
199
|
+
const details = ind.details ?? {};
|
|
200
|
+
if (Array.isArray(details.hubs)) {
|
|
201
|
+
for (const h of details.hubs) {
|
|
202
|
+
console.log(chalk.yellow(` "${h.label}" has ${h.connections} connections`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(details.orphans) && details.orphans.length > 0) {
|
|
206
|
+
for (const o of details.orphans) {
|
|
207
|
+
console.log(chalk.yellow(` "${o}" has 0 connections`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (Array.isArray(details.violations)) {
|
|
211
|
+
for (const v of details.violations) {
|
|
212
|
+
console.log(chalk.yellow(` "${v.from}" (${v.fromLayer}) → "${v.to}" (${v.toLayer})`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
160
216
|
}
|
|
161
217
|
console.log();
|
|
162
218
|
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { isJsonMode, isQuietMode } from "./output.js";
|
|
2
3
|
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
4
|
// ─── Cursor Safety ───
|
|
4
5
|
// Ensure the terminal cursor is always restored, even on unhandled crashes.
|
|
@@ -25,11 +26,15 @@ for (const event of ["exit", "SIGINT", "SIGTERM", "uncaughtException", "unhandle
|
|
|
25
26
|
* Animated braille spinner. Falls back to static console.log when not a TTY.
|
|
26
27
|
*/
|
|
27
28
|
export function spinner(label) {
|
|
28
|
-
if (!process.stdout.isTTY) {
|
|
29
|
-
|
|
29
|
+
if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
|
|
30
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
31
|
+
console.log(` ${label}...`);
|
|
32
|
+
}
|
|
30
33
|
return {
|
|
31
34
|
stop(result) {
|
|
32
|
-
|
|
35
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
36
|
+
console.log(` ${label}... ${result}`);
|
|
37
|
+
}
|
|
33
38
|
},
|
|
34
39
|
};
|
|
35
40
|
}
|
|
@@ -55,10 +60,12 @@ export function spinner(label) {
|
|
|
55
60
|
* Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
|
|
56
61
|
*/
|
|
57
62
|
export function select(prompt, options) {
|
|
58
|
-
if (!process.stdout.isTTY || options.length === 0) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if (isJsonMode() || isQuietMode() || !process.stdout.isTTY || options.length === 0) {
|
|
64
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
65
|
+
console.log(` ${prompt}`);
|
|
66
|
+
if (options.length > 0)
|
|
67
|
+
console.log(` → ${options[0]}`);
|
|
68
|
+
}
|
|
62
69
|
return Promise.resolve(0);
|
|
63
70
|
}
|
|
64
71
|
return new Promise((resolve) => {
|
|
@@ -138,16 +145,18 @@ export function progressBar(totalSteps) {
|
|
|
138
145
|
function elapsed() {
|
|
139
146
|
return ((Date.now() - startTime) / 1000).toFixed(1);
|
|
140
147
|
}
|
|
141
|
-
if (!process.stdout.isTTY) {
|
|
148
|
+
if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
|
|
142
149
|
return {
|
|
143
150
|
update(_step, label) {
|
|
144
|
-
if (label !== lastLabel) {
|
|
151
|
+
if (!isJsonMode() && !isQuietMode() && label !== lastLabel) {
|
|
145
152
|
console.log(` [${elapsed()}s] ${label}`);
|
|
146
153
|
lastLabel = label;
|
|
147
154
|
}
|
|
148
155
|
},
|
|
149
156
|
done(label) {
|
|
150
|
-
|
|
157
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
158
|
+
console.log(` [${elapsed()}s] ${label ?? "Done"}`);
|
|
159
|
+
}
|
|
151
160
|
},
|
|
152
161
|
};
|
|
153
162
|
}
|
|
@@ -179,8 +188,10 @@ export function progressBar(totalSteps) {
|
|
|
179
188
|
* (arrow keys, etc.) to prevent accidental confirmation.
|
|
180
189
|
*/
|
|
181
190
|
export function confirm(prompt) {
|
|
182
|
-
if (!process.stdout.isTTY) {
|
|
183
|
-
|
|
191
|
+
if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
|
|
192
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
193
|
+
console.log(` ${prompt} (Y/n): y`);
|
|
194
|
+
}
|
|
184
195
|
return Promise.resolve(true);
|
|
185
196
|
}
|
|
186
197
|
return new Promise((resolve) => {
|
|
@@ -222,8 +233,10 @@ export function confirm(prompt) {
|
|
|
222
233
|
* @param mask - If true, replaces each character with * (for passwords).
|
|
223
234
|
*/
|
|
224
235
|
export function textInput(prompt, opts) {
|
|
225
|
-
if (!process.stdout.isTTY) {
|
|
226
|
-
|
|
236
|
+
if (isJsonMode() || isQuietMode() || !process.stdout.isTTY) {
|
|
237
|
+
if (!isJsonMode() && !isQuietMode()) {
|
|
238
|
+
console.log(` ${prompt}: `);
|
|
239
|
+
}
|
|
227
240
|
return Promise.resolve("");
|
|
228
241
|
}
|
|
229
242
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchByte MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes architecture analysis tools and resources via the
|
|
5
|
+
* Model Context Protocol (stdio transport).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* archbyte mcp
|
|
9
|
+
*
|
|
10
|
+
* Agent config:
|
|
11
|
+
* { "mcpServers": { "archbyte": { "command": "npx", "args": ["archbyte", "mcp"] } } }
|
|
12
|
+
*/
|
|
13
|
+
export declare function startMcpServer(version: string): Promise<void>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchByte MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes architecture analysis tools and resources via the
|
|
5
|
+
* Model Context Protocol (stdio transport).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* archbyte mcp
|
|
9
|
+
*
|
|
10
|
+
* Agent config:
|
|
11
|
+
* { "mcpServers": { "archbyte": { "command": "npx", "args": ["archbyte", "mcp"] } } }
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
// ─── Server Setup ───
|
|
18
|
+
export async function startMcpServer(version) {
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: "archbyte",
|
|
21
|
+
version,
|
|
22
|
+
});
|
|
23
|
+
// ─── Tools ───
|
|
24
|
+
server.tool("archbyte_analyze", "Run architecture analysis on a codebase. Returns component and connection counts, duration, and output path.", {
|
|
25
|
+
dir: z.string().optional().describe("Project root directory (default: cwd)"),
|
|
26
|
+
static: z.boolean().optional().describe("Static-only analysis (no model)"),
|
|
27
|
+
force: z.boolean().optional().describe("Force full re-scan"),
|
|
28
|
+
}, async (params) => {
|
|
29
|
+
try {
|
|
30
|
+
const { handleAnalyze } = await import("../cli/analyze.js");
|
|
31
|
+
const rootDir = params.dir ? path.resolve(params.dir) : process.cwd();
|
|
32
|
+
// Capture output by running analyze in quiet mode
|
|
33
|
+
const { setOutputMode } = await import("../cli/output.js");
|
|
34
|
+
setOutputMode({ json: true, quiet: true, command: "analyze", version });
|
|
35
|
+
// Run pipeline — it writes analysis.json and architecture.json
|
|
36
|
+
await handleAnalyze({
|
|
37
|
+
dir: rootDir,
|
|
38
|
+
static: params.static,
|
|
39
|
+
force: params.force,
|
|
40
|
+
skipServeHint: true,
|
|
41
|
+
});
|
|
42
|
+
// Read the results
|
|
43
|
+
const fs = await import("fs");
|
|
44
|
+
const analysisPath = path.join(rootDir, ".archbyte", "analysis.json");
|
|
45
|
+
if (fs.existsSync(analysisPath)) {
|
|
46
|
+
const analysis = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
|
|
47
|
+
const components = analysis.components?.length ?? 0;
|
|
48
|
+
const connections = analysis.connections?.length ?? 0;
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: JSON.stringify({
|
|
54
|
+
outputPath: ".archbyte/analysis.json",
|
|
55
|
+
components,
|
|
56
|
+
connections,
|
|
57
|
+
mode: analysis.metadata?.mode ?? "unknown",
|
|
58
|
+
}, null, 2),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { content: [{ type: "text", text: "Analysis complete. Output: .archbyte/analysis.json" }] };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: `Analysis failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
server.tool("archbyte_status", "Check ArchByte account status, tier, and usage.", {}, async () => {
|
|
73
|
+
try {
|
|
74
|
+
const { getAccountStatus } = await import("../cli/auth.js");
|
|
75
|
+
const status = await getAccountStatus();
|
|
76
|
+
if (!status) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Not logged in. Run: archbyte login" }) }],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: `Status check failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.tool("archbyte_export", "Export architecture diagram to various formats (mermaid, json, markdown, plantuml, dot).", {
|
|
94
|
+
format: z.enum(["mermaid", "json", "markdown", "plantuml", "dot"]).optional().describe("Export format (default: mermaid)"),
|
|
95
|
+
diagram: z.string().optional().describe("Path to architecture.json"),
|
|
96
|
+
output: z.string().optional().describe("Write to file instead of returning content"),
|
|
97
|
+
}, async (params) => {
|
|
98
|
+
try {
|
|
99
|
+
const { generateExport } = await import("../cli/export.js");
|
|
100
|
+
const result = await generateExport({
|
|
101
|
+
format: params.format,
|
|
102
|
+
diagram: params.diagram,
|
|
103
|
+
output: params.output,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: result.content }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `Export failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
server.tool("archbyte_stats", "Get architecture health metrics: component counts, layer distribution, and health indicators.", {
|
|
117
|
+
diagram: z.string().optional().describe("Path to architecture.json"),
|
|
118
|
+
config: z.string().optional().describe("Path to archbyte.yaml config"),
|
|
119
|
+
}, async (params) => {
|
|
120
|
+
try {
|
|
121
|
+
const { computeStats } = await import("../cli/stats.js");
|
|
122
|
+
const stats = computeStats({
|
|
123
|
+
diagram: params.diagram,
|
|
124
|
+
config: params.config,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: `Stats failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
server.tool("archbyte_validate", "Run architecture fitness rules (connection density, hubs, orphans, layer violations).", {
|
|
138
|
+
diagram: z.string().optional().describe("Path to architecture.json"),
|
|
139
|
+
config: z.string().optional().describe("Path to archbyte.yaml config"),
|
|
140
|
+
}, async (params) => {
|
|
141
|
+
try {
|
|
142
|
+
const { computeStats } = await import("../cli/stats.js");
|
|
143
|
+
const stats = computeStats({
|
|
144
|
+
diagram: params.diagram,
|
|
145
|
+
config: params.config,
|
|
146
|
+
});
|
|
147
|
+
const failures = stats.health.filter((h) => h.status === "warn");
|
|
148
|
+
const passed = failures.length === 0;
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: JSON.stringify({
|
|
154
|
+
valid: passed,
|
|
155
|
+
rules: stats.health.length,
|
|
156
|
+
passed: stats.health.filter((h) => h.status === "pass").length,
|
|
157
|
+
warnings: failures.length,
|
|
158
|
+
results: stats.health,
|
|
159
|
+
}, null, 2),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: `Validation failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// ─── Resources ───
|
|
172
|
+
server.resource("Architecture", "archbyte://architecture", { description: "Full architecture state (nodes, edges, flows, environments)" }, async () => {
|
|
173
|
+
const data = await loadArchitectureData();
|
|
174
|
+
if (!data) {
|
|
175
|
+
return {
|
|
176
|
+
contents: [{
|
|
177
|
+
uri: "archbyte://architecture",
|
|
178
|
+
mimeType: "application/json",
|
|
179
|
+
text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
contents: [{
|
|
185
|
+
uri: "archbyte://architecture",
|
|
186
|
+
mimeType: "application/json",
|
|
187
|
+
text: JSON.stringify(data, null, 2),
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
server.resource("Components", "archbyte://components", { description: "Component list with types, layers, and tech stacks" }, async () => {
|
|
192
|
+
const data = await loadArchitectureData();
|
|
193
|
+
if (!data) {
|
|
194
|
+
return {
|
|
195
|
+
contents: [{
|
|
196
|
+
uri: "archbyte://components",
|
|
197
|
+
mimeType: "application/json",
|
|
198
|
+
text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
|
|
199
|
+
}],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const components = data.nodes.map((n) => ({
|
|
203
|
+
id: n.id,
|
|
204
|
+
label: n.label,
|
|
205
|
+
type: n.type,
|
|
206
|
+
layer: n.layer,
|
|
207
|
+
techStack: n.techStack ?? [],
|
|
208
|
+
description: n.description,
|
|
209
|
+
}));
|
|
210
|
+
return {
|
|
211
|
+
contents: [{
|
|
212
|
+
uri: "archbyte://components",
|
|
213
|
+
mimeType: "application/json",
|
|
214
|
+
text: JSON.stringify(components, null, 2),
|
|
215
|
+
}],
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
server.resource("Connections", "archbyte://connections", { description: "Edge list with labels and environments" }, async () => {
|
|
219
|
+
const data = await loadArchitectureData();
|
|
220
|
+
if (!data) {
|
|
221
|
+
return {
|
|
222
|
+
contents: [{
|
|
223
|
+
uri: "archbyte://connections",
|
|
224
|
+
mimeType: "application/json",
|
|
225
|
+
text: JSON.stringify({ error: "No architecture.json found. Run: archbyte analyze" }),
|
|
226
|
+
}],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const connections = data.edges.map((e) => ({
|
|
230
|
+
id: e.id,
|
|
231
|
+
source: e.source,
|
|
232
|
+
target: e.target,
|
|
233
|
+
label: e.label,
|
|
234
|
+
environments: e.environments ?? [],
|
|
235
|
+
}));
|
|
236
|
+
return {
|
|
237
|
+
contents: [{
|
|
238
|
+
uri: "archbyte://connections",
|
|
239
|
+
mimeType: "application/json",
|
|
240
|
+
text: JSON.stringify(connections, null, 2),
|
|
241
|
+
}],
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
// ─── Start ───
|
|
245
|
+
const transport = new StdioServerTransport();
|
|
246
|
+
await server.connect(transport);
|
|
247
|
+
}
|
|
248
|
+
// ─── Helpers ───
|
|
249
|
+
async function loadArchitectureData() {
|
|
250
|
+
const { loadArchitectureFileSafe, resolveArchitecturePath } = await import("../cli/shared.js");
|
|
251
|
+
const archPath = resolveArchitecturePath({});
|
|
252
|
+
return loadArchitectureFileSafe(archPath);
|
|
253
|
+
}
|