archbyte 0.4.0 → 0.4.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/bin/archbyte.js +2 -20
- package/dist/agents/pipeline/merger.d.ts +2 -2
- package/dist/agents/pipeline/merger.js +152 -27
- package/dist/agents/pipeline/types.d.ts +29 -1
- package/dist/agents/pipeline/types.js +0 -1
- package/dist/agents/providers/claude-sdk.js +32 -8
- package/dist/agents/runtime/types.d.ts +4 -0
- package/dist/agents/runtime/types.js +2 -2
- package/dist/agents/static/component-detector.js +35 -3
- package/dist/agents/static/connection-mapper.d.ts +1 -1
- package/dist/agents/static/connection-mapper.js +74 -1
- package/dist/agents/static/index.js +5 -2
- package/dist/agents/static/types.d.ts +26 -0
- package/dist/cli/analyze.js +62 -19
- package/dist/cli/arch-diff.d.ts +38 -0
- package/dist/cli/arch-diff.js +61 -0
- package/dist/cli/patrol.d.ts +5 -3
- package/dist/cli/patrol.js +417 -65
- package/dist/cli/setup.js +2 -7
- package/dist/cli/shared.d.ts +11 -0
- package/dist/cli/shared.js +61 -0
- package/dist/cli/validate.d.ts +0 -1
- package/dist/cli/validate.js +0 -16
- package/dist/server/src/index.js +537 -17
- package/package.json +1 -1
- package/templates/archbyte.yaml +8 -0
- package/ui/dist/assets/index-DDCNauh7.css +1 -0
- package/ui/dist/assets/index-DO4t5Xu1.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/cli/mcp-server.d.ts +0 -1
- package/dist/cli/mcp-server.js +0 -443
- package/dist/cli/mcp.d.ts +0 -1
- package/dist/cli/mcp.js +0 -98
- package/ui/dist/assets/index-0_XpUUZQ.css +0 -1
- package/ui/dist/assets/index-DmO1qYan.js +0 -70
package/dist/cli/patrol.js
CHANGED
|
@@ -3,8 +3,57 @@ import * as fs from "fs";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { runValidation } from "./validate.js";
|
|
6
|
-
import {
|
|
6
|
+
import { loadPatrolIgnore } from "./shared.js";
|
|
7
|
+
import { loadArchitectureJSON, diffArchitectures, hasStructuralChanges } from "./arch-diff.js";
|
|
7
8
|
const PATROL_DIR = ".archbyte/patrols";
|
|
9
|
+
/**
|
|
10
|
+
* Convert a gitignore-style pattern to chokidar glob(s).
|
|
11
|
+
*/
|
|
12
|
+
function toChokidarGlobs(line) {
|
|
13
|
+
// Directory pattern: "dist/" → "**/dist/**"
|
|
14
|
+
if (line.endsWith("/")) {
|
|
15
|
+
return [`**/${line.slice(0, -1)}/**`];
|
|
16
|
+
}
|
|
17
|
+
// Glob with extension: "*.pyc" → "**/*.pyc"
|
|
18
|
+
if (line.startsWith("*.")) {
|
|
19
|
+
return [`**/${line}`];
|
|
20
|
+
}
|
|
21
|
+
// Already a glob — pass through
|
|
22
|
+
if (line.includes("*") || line.includes("?")) {
|
|
23
|
+
return [line];
|
|
24
|
+
}
|
|
25
|
+
// Plain name: treat as both file and directory
|
|
26
|
+
return [`**/${line}`, `**/${line}/**`];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build chokidar ignored list from three layers:
|
|
30
|
+
* 1. Baseline: hidden dirs + node_modules (always)
|
|
31
|
+
* 2. .gitignore patterns (if present)
|
|
32
|
+
* 3. archbyte.yaml patrol.ignore patterns (if configured)
|
|
33
|
+
*/
|
|
34
|
+
function buildWatchIgnored(configPath) {
|
|
35
|
+
const ignored = [
|
|
36
|
+
/(^|[/\\])\./, // hidden files/dirs (.git, .archbyte, .venv, .next, etc.)
|
|
37
|
+
"**/node_modules/**",
|
|
38
|
+
];
|
|
39
|
+
// Layer 2: .gitignore
|
|
40
|
+
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
41
|
+
if (fs.existsSync(gitignorePath)) {
|
|
42
|
+
const lines = fs.readFileSync(gitignorePath, "utf-8").split("\n");
|
|
43
|
+
for (const raw of lines) {
|
|
44
|
+
const line = raw.trim();
|
|
45
|
+
if (!line || line.startsWith("#") || line.startsWith("!"))
|
|
46
|
+
continue;
|
|
47
|
+
ignored.push(...toChokidarGlobs(line));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Layer 3: archbyte.yaml patrol.ignore
|
|
51
|
+
const userPatterns = loadPatrolIgnore(configPath);
|
|
52
|
+
for (const pattern of userPatterns) {
|
|
53
|
+
ignored.push(...toChokidarGlobs(pattern));
|
|
54
|
+
}
|
|
55
|
+
return ignored;
|
|
56
|
+
}
|
|
8
57
|
const HISTORY_FILE = "history.jsonl";
|
|
9
58
|
const LATEST_FILE = "latest.json";
|
|
10
59
|
function parseInterval(str) {
|
|
@@ -41,63 +90,240 @@ function saveRecord(patrolDir, record) {
|
|
|
41
90
|
// Append to history
|
|
42
91
|
fs.appendFileSync(path.join(patrolDir, HISTORY_FILE), JSON.stringify(record) + "\n", "utf-8");
|
|
43
92
|
}
|
|
44
|
-
function
|
|
45
|
-
const key = (
|
|
93
|
+
function diffIssues(previous, current) {
|
|
94
|
+
const key = (i) => `${i.type}:${i.rule || i.metric}:${i.message}`;
|
|
46
95
|
const prevKeys = new Set(previous.map(key));
|
|
47
96
|
const currKeys = new Set(current.map(key));
|
|
48
97
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
newIssues: current.filter((i) => !prevKeys.has(key(i))),
|
|
99
|
+
resolvedIssues: previous.filter((i) => !currKeys.has(key(i))),
|
|
51
100
|
};
|
|
52
101
|
}
|
|
53
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Load token usage from analysis.json metadata
|
|
104
|
+
*/
|
|
105
|
+
function loadTokenUsage() {
|
|
54
106
|
try {
|
|
55
|
-
|
|
107
|
+
const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
|
|
108
|
+
if (!fs.existsSync(analysisPath))
|
|
109
|
+
return null;
|
|
110
|
+
const analysis = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
|
|
111
|
+
const tokenUsage = analysis.metadata?.tokenUsage;
|
|
112
|
+
if (tokenUsage?.input && tokenUsage?.output) {
|
|
113
|
+
return { input: tokenUsage.input, output: tokenUsage.output };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
56
116
|
}
|
|
57
117
|
catch {
|
|
58
118
|
return null;
|
|
59
119
|
}
|
|
60
120
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
121
|
+
// ─── Git change tracking ───
|
|
122
|
+
function gitExec(cmd, rootDir) {
|
|
123
|
+
try {
|
|
124
|
+
return execSync(cmd, {
|
|
125
|
+
cwd: rootDir,
|
|
126
|
+
encoding: "utf-8",
|
|
127
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
128
|
+
}).trim();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function captureGitState(rootDir, patrolDir) {
|
|
135
|
+
const commitHash = gitExec("git rev-parse HEAD", rootDir);
|
|
136
|
+
if (!commitHash)
|
|
137
|
+
return undefined;
|
|
138
|
+
const previous = loadLatestRecord(patrolDir);
|
|
139
|
+
const previousCommitHash = previous?.git?.commitHash;
|
|
140
|
+
let changedFiles = [];
|
|
141
|
+
let commitCount = 0;
|
|
142
|
+
if (previousCommitHash && previousCommitHash !== commitHash) {
|
|
143
|
+
const countStr = gitExec(`git rev-list --count ${previousCommitHash}..HEAD`, rootDir);
|
|
144
|
+
if (countStr)
|
|
145
|
+
commitCount = parseInt(countStr, 10) || 0;
|
|
146
|
+
const diff = gitExec(`git diff --name-only ${previousCommitHash}..HEAD`, rootDir);
|
|
147
|
+
if (diff)
|
|
148
|
+
changedFiles = diff.split("\n").filter(Boolean);
|
|
69
149
|
}
|
|
70
|
-
//
|
|
71
|
-
|
|
150
|
+
// Include uncommitted changes (staged + unstaged + untracked)
|
|
151
|
+
const unstaged = gitExec("git diff --name-only", rootDir);
|
|
152
|
+
if (unstaged) {
|
|
153
|
+
for (const f of unstaged.split("\n").filter(Boolean)) {
|
|
154
|
+
if (!changedFiles.includes(f))
|
|
155
|
+
changedFiles.push(f);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const staged = gitExec("git diff --name-only --cached", rootDir);
|
|
159
|
+
if (staged) {
|
|
160
|
+
for (const f of staged.split("\n").filter(Boolean)) {
|
|
161
|
+
if (!changedFiles.includes(f))
|
|
162
|
+
changedFiles.push(f);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const untracked = gitExec("git ls-files --others --exclude-standard", rootDir);
|
|
166
|
+
if (untracked) {
|
|
167
|
+
for (const f of untracked.split("\n").filter(Boolean)) {
|
|
168
|
+
if (!changedFiles.includes(f))
|
|
169
|
+
changedFiles.push(f);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
commitHash,
|
|
174
|
+
previousCommitHash,
|
|
175
|
+
commitCount,
|
|
176
|
+
changedFiles: changedFiles.slice(0, 100),
|
|
177
|
+
changedFileCount: changedFiles.length,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ─── Structural diff → issues ───
|
|
181
|
+
function structuralDiffToIssues(diff) {
|
|
182
|
+
const issues = [];
|
|
183
|
+
for (const node of diff.addedNodes) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: 'structural',
|
|
186
|
+
metric: 'component-added',
|
|
187
|
+
level: 'warn',
|
|
188
|
+
message: `New component: "${node.label}" (${node.type}, ${node.layer})`,
|
|
189
|
+
nodeIds: [node.id],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const node of diff.removedNodes) {
|
|
193
|
+
issues.push({
|
|
194
|
+
type: 'structural',
|
|
195
|
+
metric: 'component-removed',
|
|
196
|
+
level: 'warn',
|
|
197
|
+
message: `Removed component: "${node.label}" (${node.type}, ${node.layer})`,
|
|
198
|
+
nodeIds: [node.id],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const mod of diff.modifiedNodes) {
|
|
202
|
+
issues.push({
|
|
203
|
+
type: 'structural',
|
|
204
|
+
metric: 'component-modified',
|
|
205
|
+
level: 'warn',
|
|
206
|
+
message: `"${mod.id}" ${mod.field}: "${mod.from}" -> "${mod.to}"`,
|
|
207
|
+
nodeIds: [mod.id],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (diff.addedEdges.length > 0 || diff.removedEdges.length > 0) {
|
|
211
|
+
issues.push({
|
|
212
|
+
type: 'structural',
|
|
213
|
+
metric: 'connections-changed',
|
|
214
|
+
level: 'warn',
|
|
215
|
+
message: `Connections: +${diff.addedEdges.length} added, -${diff.removedEdges.length} removed`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return issues;
|
|
219
|
+
}
|
|
220
|
+
// ─── Metrics from validation ───
|
|
221
|
+
function computeMetrics(result) {
|
|
222
|
+
return {
|
|
223
|
+
circularDependencies: result.violations.filter((v) => v.rule === "no-circular-deps").length,
|
|
224
|
+
highDegreeHubCount: result.violations.filter((v) => v.rule === "max-connections").length,
|
|
225
|
+
disconnectedComponentCount: result.violations.filter((v) => v.rule === "no-orphans").length,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Run a full patrol cycle: snapshot → analyze → generate → validate → diff.
|
|
230
|
+
*/
|
|
231
|
+
async function runPatrolCycle(options, patrolDir) {
|
|
232
|
+
const cycleStart = Date.now();
|
|
233
|
+
const rootDir = process.cwd();
|
|
234
|
+
// 0. Snapshot architecture before cycle for structural diff
|
|
235
|
+
const prevArch = loadArchitectureJSON(rootDir);
|
|
236
|
+
// 0b. Capture git state
|
|
237
|
+
const git = captureGitState(rootDir, patrolDir);
|
|
238
|
+
// 1. Run full analyze (static context → LLM pipeline, incremental)
|
|
72
239
|
try {
|
|
73
240
|
const { handleAnalyze } = await import("./analyze.js");
|
|
74
|
-
await handleAnalyze({ verbose: false });
|
|
75
|
-
return true;
|
|
241
|
+
await handleAnalyze({ verbose: false, skipServeHint: true });
|
|
76
242
|
}
|
|
77
243
|
catch (err) {
|
|
78
|
-
console.error(chalk.yellow(`
|
|
79
|
-
return false;
|
|
244
|
+
console.error(chalk.yellow(` Analyze failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
80
245
|
}
|
|
81
|
-
|
|
82
|
-
|
|
246
|
+
// 2. Generate diagram from updated analysis
|
|
247
|
+
try {
|
|
248
|
+
const { handleGenerate } = await import("./generate.js");
|
|
249
|
+
await handleGenerate({ verbose: false });
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
console.error(chalk.yellow(` Generate failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
253
|
+
}
|
|
254
|
+
// 3. Compute structural diff
|
|
255
|
+
const currArch = loadArchitectureJSON(rootDir);
|
|
256
|
+
let structuralDiff;
|
|
257
|
+
if (prevArch && currArch) {
|
|
258
|
+
const diff = diffArchitectures(prevArch, currArch);
|
|
259
|
+
if (hasStructuralChanges(diff)) {
|
|
260
|
+
structuralDiff = diff;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// 4. Validate (get rule violations)
|
|
83
264
|
const previous = loadLatestRecord(patrolDir);
|
|
84
265
|
const result = runValidation({
|
|
85
266
|
diagram: options.diagram,
|
|
86
267
|
config: options.config,
|
|
87
268
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:
|
|
269
|
+
// Convert rule violations to audit issues
|
|
270
|
+
const ruleIssues = result.violations.map((v) => ({
|
|
271
|
+
type: 'rule-violation',
|
|
272
|
+
rule: v.rule,
|
|
273
|
+
level: v.level,
|
|
274
|
+
message: v.message,
|
|
275
|
+
ruleType: 'builtin',
|
|
276
|
+
}));
|
|
277
|
+
// Convert structural diff to audit issues
|
|
278
|
+
const structuralIssues = structuralDiff ? structuralDiffToIssues(structuralDiff) : [];
|
|
279
|
+
// Merge all issues
|
|
280
|
+
const allIssues = [...ruleIssues, ...structuralIssues];
|
|
281
|
+
// Diff against previous cycle
|
|
282
|
+
const prevIssues = previous?.issues ?? [];
|
|
283
|
+
const { newIssues, resolvedIssues } = diffIssues(prevIssues, allIssues);
|
|
284
|
+
// 5. Extract token usage
|
|
285
|
+
const tokenData = loadTokenUsage();
|
|
286
|
+
// 6. Build record with all fields (structured + flattened for UI)
|
|
91
287
|
const record = {
|
|
92
288
|
timestamp: new Date().toISOString(),
|
|
93
|
-
passed: result.errors === 0,
|
|
289
|
+
passed: result.errors === 0 && structuralIssues.length === 0,
|
|
290
|
+
issues: allIssues,
|
|
291
|
+
summary: {
|
|
292
|
+
ruleViolations: ruleIssues.length,
|
|
293
|
+
structuralIssues: structuralIssues.length,
|
|
294
|
+
totalIssues: allIssues.length,
|
|
295
|
+
},
|
|
296
|
+
newIssues,
|
|
297
|
+
resolvedIssues,
|
|
298
|
+
metrics: computeMetrics(result),
|
|
299
|
+
durationMs: Date.now() - cycleStart,
|
|
300
|
+
tokenUsage: tokenData
|
|
301
|
+
? {
|
|
302
|
+
inputTokens: tokenData.input,
|
|
303
|
+
outputTokens: tokenData.output,
|
|
304
|
+
totalTokens: tokenData.input + tokenData.output,
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
307
|
+
// Structural diff
|
|
308
|
+
structuralDiff,
|
|
309
|
+
// Git context
|
|
310
|
+
git,
|
|
311
|
+
// Architecture counts
|
|
312
|
+
totalNodes: currArch?.nodes.length ?? 0,
|
|
313
|
+
totalEdges: currArch?.edges.length ?? 0,
|
|
314
|
+
// Flattened for UI (backward compat)
|
|
94
315
|
errors: result.errors,
|
|
95
316
|
warnings: result.warnings,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
317
|
+
newViolations: newIssues.map((i) => ({
|
|
318
|
+
rule: i.rule || i.metric || "",
|
|
319
|
+
level: i.level,
|
|
320
|
+
message: i.message,
|
|
321
|
+
})),
|
|
322
|
+
resolvedViolations: resolvedIssues.map((i) => ({
|
|
323
|
+
rule: i.rule || i.metric || "",
|
|
324
|
+
level: i.level,
|
|
325
|
+
message: i.message,
|
|
326
|
+
})),
|
|
101
327
|
};
|
|
102
328
|
saveRecord(patrolDir, record);
|
|
103
329
|
return record;
|
|
@@ -105,23 +331,66 @@ function runPatrolCycle(options, patrolDir) {
|
|
|
105
331
|
function printPatrolResult(record, cycleNum) {
|
|
106
332
|
const time = new Date(record.timestamp).toLocaleTimeString();
|
|
107
333
|
const status = record.passed ? chalk.green("HEALTHY") : chalk.red("VIOLATION");
|
|
334
|
+
const duration = record.durationMs ? `${(record.durationMs / 1000).toFixed(1)}s` : "?";
|
|
108
335
|
console.log();
|
|
109
|
-
console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status}`));
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
336
|
+
console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status} | ${duration}`));
|
|
337
|
+
console.log(chalk.gray(` Architecture: ${record.totalNodes} nodes, ${record.totalEdges} edges`));
|
|
338
|
+
// Token usage
|
|
339
|
+
if (record.tokenUsage) {
|
|
340
|
+
const tokens = record.tokenUsage;
|
|
341
|
+
console.log(chalk.gray(` Tokens: ${tokens.inputTokens.toLocaleString()} in + ${tokens.outputTokens.toLocaleString()} out = ${tokens.totalTokens.toLocaleString()} total`));
|
|
342
|
+
}
|
|
343
|
+
// Git changes
|
|
344
|
+
if (record.git && (record.git.commitCount > 0 || record.git.changedFileCount > 0)) {
|
|
345
|
+
const parts = [];
|
|
346
|
+
if (record.git.commitCount > 0)
|
|
347
|
+
parts.push(`${record.git.commitCount} commit(s)`);
|
|
348
|
+
parts.push(`${record.git.changedFileCount} file(s) changed`);
|
|
349
|
+
console.log(chalk.gray(` Git: ${parts.join(", ")}`));
|
|
350
|
+
}
|
|
351
|
+
// Structural changes
|
|
352
|
+
if (record.structuralDiff && hasStructuralChanges(record.structuralDiff)) {
|
|
353
|
+
const d = record.structuralDiff;
|
|
354
|
+
console.log(chalk.bold.yellow(` Architecture drift detected:`));
|
|
355
|
+
for (const n of d.addedNodes) {
|
|
356
|
+
console.log(chalk.green(` + ${n.label} (${n.type})`));
|
|
357
|
+
}
|
|
358
|
+
for (const n of d.removedNodes) {
|
|
359
|
+
console.log(chalk.red(` - ${n.label} (${n.type})`));
|
|
360
|
+
}
|
|
361
|
+
for (const m of d.modifiedNodes) {
|
|
362
|
+
console.log(chalk.yellow(` ~ ${m.id}: ${m.field} "${m.from}" -> "${m.to}"`));
|
|
363
|
+
}
|
|
364
|
+
if (d.addedEdges.length > 0) {
|
|
365
|
+
console.log(chalk.green(` + ${d.addedEdges.length} new connection(s)`));
|
|
366
|
+
}
|
|
367
|
+
if (d.removedEdges.length > 0) {
|
|
368
|
+
console.log(chalk.red(` - ${d.removedEdges.length} removed connection(s)`));
|
|
115
369
|
}
|
|
116
370
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
371
|
+
// New rule violations
|
|
372
|
+
const newRuleIssues = record.newIssues.filter((i) => i.type === "rule-violation");
|
|
373
|
+
if (newRuleIssues.length > 0) {
|
|
374
|
+
console.log(chalk.red(` ${newRuleIssues.length} new rule violation(s):`));
|
|
375
|
+
for (const issue of newRuleIssues) {
|
|
376
|
+
const icon = issue.level === "error" ? chalk.red("!!") : chalk.yellow("!!");
|
|
377
|
+
const label = issue.rule || issue.metric || "unknown";
|
|
378
|
+
console.log(chalk.gray(` ${icon} [${label}] ${issue.message}`));
|
|
121
379
|
}
|
|
122
380
|
}
|
|
123
|
-
|
|
124
|
-
|
|
381
|
+
// Resolved issues
|
|
382
|
+
if (record.resolvedIssues.length > 0) {
|
|
383
|
+
console.log(chalk.green(` ${record.resolvedIssues.length} resolved:`));
|
|
384
|
+
for (const issue of record.resolvedIssues) {
|
|
385
|
+
const label = issue.rule || issue.metric || "unknown";
|
|
386
|
+
console.log(chalk.green(` -- [${label}] ${issue.message}`));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// No changes at all
|
|
390
|
+
if (record.newIssues.length === 0 &&
|
|
391
|
+
record.resolvedIssues.length === 0 &&
|
|
392
|
+
(!record.structuralDiff || !hasStructuralChanges(record.structuralDiff))) {
|
|
393
|
+
console.log(chalk.gray(` No changes. ${record.summary.ruleViolations} rules, ${record.summary.structuralIssues} structural`));
|
|
125
394
|
}
|
|
126
395
|
}
|
|
127
396
|
function printHistory(patrolDir) {
|
|
@@ -155,34 +424,43 @@ function printHistory(patrolDir) {
|
|
|
155
424
|
console.log(` Health: ${sparkline} (last ${records.length} patrols)`);
|
|
156
425
|
console.log();
|
|
157
426
|
// Table
|
|
158
|
-
console.log(chalk.gray(" Time Status
|
|
159
|
-
console.log(chalk.gray(" " + "-".repeat(
|
|
427
|
+
console.log(chalk.gray(" Time Status Nodes Edges Rules Struct New Resolved Git"));
|
|
428
|
+
console.log(chalk.gray(" " + "-".repeat(100)));
|
|
160
429
|
for (const r of records) {
|
|
161
430
|
const time = new Date(r.timestamp).toLocaleString().padEnd(20);
|
|
162
431
|
const status = r.passed ? chalk.green("PASS ") : chalk.red("FAIL ");
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
432
|
+
const nodes = String(r.totalNodes ?? "?").padEnd(6);
|
|
433
|
+
const edges = String(r.totalEdges ?? "?").padEnd(6);
|
|
434
|
+
const rules = String(r.summary?.ruleViolations ?? 0).padEnd(6);
|
|
435
|
+
const structural = String(r.summary?.structuralIssues ?? 0).padEnd(8);
|
|
436
|
+
const newV = String(r.newIssues?.length ?? 0).padEnd(5);
|
|
437
|
+
const resolved = String(r.resolvedIssues?.length ?? 0).padEnd(10);
|
|
438
|
+
const gitInfo = r.git
|
|
439
|
+
? `${r.git.commitCount}c/${r.git.changedFileCount}f`
|
|
440
|
+
: "-";
|
|
441
|
+
console.log(` ${time} ${status} ${nodes}${edges}${rules}${structural}${newV}${resolved}${gitInfo}`);
|
|
168
442
|
}
|
|
169
443
|
console.log();
|
|
170
444
|
// Summary
|
|
171
445
|
const totalPatrols = lines.length;
|
|
172
|
-
const failedPatrols =
|
|
173
|
-
const healthPct =
|
|
446
|
+
const failedPatrols = records.filter((r) => !r.passed).length;
|
|
447
|
+
const healthPct = totalPatrols > 0
|
|
448
|
+
? Math.round(((totalPatrols - failedPatrols) / totalPatrols) * 100)
|
|
449
|
+
: 100;
|
|
174
450
|
console.log(` Total patrols: ${totalPatrols} | Health rate: ${healthPct}% | Failed: ${failedPatrols}`);
|
|
175
451
|
console.log();
|
|
176
452
|
}
|
|
177
453
|
function handleViolationAction(record, action) {
|
|
178
|
-
if (record.
|
|
454
|
+
if (record.newIssues.length === 0 && (!record.structuralDiff || !hasStructuralChanges(record.structuralDiff)))
|
|
179
455
|
return;
|
|
180
456
|
switch (action) {
|
|
181
457
|
case "json":
|
|
182
458
|
console.log(JSON.stringify({
|
|
183
|
-
event: "patrol-
|
|
459
|
+
event: "patrol-issue",
|
|
184
460
|
timestamp: record.timestamp,
|
|
185
|
-
|
|
461
|
+
newIssues: record.newIssues,
|
|
462
|
+
structuralDiff: record.structuralDiff,
|
|
463
|
+
git: record.git,
|
|
186
464
|
}));
|
|
187
465
|
break;
|
|
188
466
|
case "log":
|
|
@@ -193,8 +471,9 @@ function handleViolationAction(record, action) {
|
|
|
193
471
|
}
|
|
194
472
|
/**
|
|
195
473
|
* Run the architecture patrol daemon.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
474
|
+
* Each cycle: snapshot → analyze (incremental) → generate → validate → diff
|
|
475
|
+
*
|
|
476
|
+
* --once: run a single cycle then exit (used by UI "Run Now")
|
|
198
477
|
*/
|
|
199
478
|
export async function handlePatrol(options) {
|
|
200
479
|
const projectName = process.cwd().split("/").pop() || "project";
|
|
@@ -204,25 +483,98 @@ export async function handlePatrol(options) {
|
|
|
204
483
|
printHistory(patrolDir);
|
|
205
484
|
return;
|
|
206
485
|
}
|
|
486
|
+
const action = options.onViolation || "log";
|
|
487
|
+
// Run initial cycle immediately
|
|
488
|
+
let cycleNum = 1;
|
|
489
|
+
// Watch mode: trigger patrol cycle on source file changes
|
|
490
|
+
if (options.watch) {
|
|
491
|
+
const chokidar = await import("chokidar");
|
|
492
|
+
const userPatterns = loadPatrolIgnore(options.config);
|
|
493
|
+
console.log();
|
|
494
|
+
console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
|
|
495
|
+
console.log(chalk.gray(` Mode: watch | On violation: ${action}`));
|
|
496
|
+
console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
|
|
497
|
+
console.log(chalk.gray(` History: archbyte patrol --history`));
|
|
498
|
+
if (userPatterns.length > 0) {
|
|
499
|
+
console.log(chalk.gray(` Ignoring: ${userPatterns.join(", ")} (from archbyte.yaml)`));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(chalk.gray(` Tip: Add patrol.ignore in archbyte.yaml to exclude paths from watching`));
|
|
503
|
+
}
|
|
504
|
+
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
|
505
|
+
// Set up watcher FIRST — its file handles keep the event loop alive
|
|
506
|
+
// so the process can't exit during the initial cycle.
|
|
507
|
+
let debounceTimer = null;
|
|
508
|
+
let running = false;
|
|
509
|
+
const triggerCycle = () => {
|
|
510
|
+
if (debounceTimer)
|
|
511
|
+
clearTimeout(debounceTimer);
|
|
512
|
+
debounceTimer = setTimeout(async () => {
|
|
513
|
+
if (running)
|
|
514
|
+
return;
|
|
515
|
+
running = true;
|
|
516
|
+
cycleNum++;
|
|
517
|
+
try {
|
|
518
|
+
const record = await runPatrolCycle(options, patrolDir);
|
|
519
|
+
printPatrolResult(record, cycleNum);
|
|
520
|
+
handleViolationAction(record, action);
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
console.error(chalk.red(` Patrol cycle #${cycleNum} failed: ${err}`));
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
running = false;
|
|
527
|
+
}
|
|
528
|
+
}, 500);
|
|
529
|
+
};
|
|
530
|
+
const watcher = chokidar.watch(".", {
|
|
531
|
+
cwd: process.cwd(),
|
|
532
|
+
ignoreInitial: true,
|
|
533
|
+
ignored: buildWatchIgnored(options.config),
|
|
534
|
+
});
|
|
535
|
+
watcher.on("change", triggerCycle);
|
|
536
|
+
watcher.on("add", triggerCycle);
|
|
537
|
+
watcher.on("unlink", triggerCycle);
|
|
538
|
+
const shutdown = () => {
|
|
539
|
+
watcher.close();
|
|
540
|
+
if (debounceTimer)
|
|
541
|
+
clearTimeout(debounceTimer);
|
|
542
|
+
console.log();
|
|
543
|
+
console.log(chalk.gray(` Patrol stopped after ${cycleNum} cycles.`));
|
|
544
|
+
process.exit(0);
|
|
545
|
+
};
|
|
546
|
+
process.on("SIGINT", shutdown);
|
|
547
|
+
process.on("SIGTERM", shutdown);
|
|
548
|
+
// Now run initial cycle — watcher is already anchoring the event loop
|
|
549
|
+
try {
|
|
550
|
+
const record = await runPatrolCycle(options, patrolDir);
|
|
551
|
+
printPatrolResult(record, cycleNum);
|
|
552
|
+
handleViolationAction(record, action);
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
console.error(chalk.red(` Initial patrol cycle failed: ${err}`));
|
|
556
|
+
}
|
|
557
|
+
console.log(chalk.gray(" Watching for changes..."));
|
|
558
|
+
await new Promise(() => { });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Timer mode (default): poll on interval
|
|
207
562
|
const intervalMs = parseInterval(options.interval || "5m");
|
|
208
563
|
const intervalStr = options.interval || "5m";
|
|
209
|
-
const action = options.onViolation || "log";
|
|
210
564
|
console.log();
|
|
211
565
|
console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
|
|
212
566
|
console.log(chalk.gray(` Interval: ${intervalStr} | On violation: ${action}`));
|
|
213
|
-
console.log(chalk.gray(`
|
|
567
|
+
console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
|
|
568
|
+
console.log(chalk.gray(` History: archbyte patrol --history`));
|
|
214
569
|
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
|
215
|
-
|
|
216
|
-
let cycleNum = 1;
|
|
217
|
-
const record = runPatrolCycle(options, patrolDir);
|
|
570
|
+
const record = await runPatrolCycle(options, patrolDir);
|
|
218
571
|
printPatrolResult(record, cycleNum);
|
|
219
572
|
handleViolationAction(record, action);
|
|
220
573
|
// Patrol loop
|
|
221
574
|
const timer = setInterval(async () => {
|
|
222
575
|
cycleNum++;
|
|
223
576
|
try {
|
|
224
|
-
await
|
|
225
|
-
const record = runPatrolCycle(options, patrolDir);
|
|
577
|
+
const record = await runPatrolCycle(options, patrolDir);
|
|
226
578
|
printPatrolResult(record, cycleNum);
|
|
227
579
|
handleViolationAction(record, action);
|
|
228
580
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -211,7 +211,7 @@ export async function handleSetup() {
|
|
|
211
211
|
console.log();
|
|
212
212
|
console.log(chalk.bold.cyan("ArchByte Setup"));
|
|
213
213
|
console.log(chalk.gray("Configure your model provider and API key.\n"));
|
|
214
|
-
// Detect AI coding tools
|
|
214
|
+
// Detect AI coding tools
|
|
215
215
|
const hasClaude = isInPath("claude");
|
|
216
216
|
const codexDir = path.join(CONFIG_DIR, "../.codex");
|
|
217
217
|
const hasCodex = fs.existsSync(codexDir);
|
|
@@ -303,9 +303,6 @@ export async function handleSetup() {
|
|
|
303
303
|
console.log(" " + chalk.bold("Next steps"));
|
|
304
304
|
console.log();
|
|
305
305
|
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
306
|
-
if (hasCodex) {
|
|
307
|
-
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from Codex CLI");
|
|
308
|
-
}
|
|
309
306
|
console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
|
|
310
307
|
console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
|
|
311
308
|
console.log();
|
|
@@ -313,8 +310,7 @@ export async function handleSetup() {
|
|
|
313
310
|
}
|
|
314
311
|
if (choice === "codex") {
|
|
315
312
|
// TODO: Add Codex SDK provider when available
|
|
316
|
-
console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now
|
|
317
|
-
console.log(chalk.gray(" In the meantime, use archbyte mcp install to run ArchByte from Codex.\n"));
|
|
313
|
+
console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now.\n"));
|
|
318
314
|
}
|
|
319
315
|
// User chose BYOK — continue to normal provider selection below
|
|
320
316
|
if (choice === "byok")
|
|
@@ -596,7 +592,6 @@ export async function handleSetup() {
|
|
|
596
592
|
console.log();
|
|
597
593
|
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
598
594
|
if (hasClaude || hasCodex) {
|
|
599
|
-
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
|
|
600
595
|
}
|
|
601
596
|
console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
|
|
602
597
|
console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -64,5 +64,16 @@ export declare function parseRulesFromYaml(content: string): RuleConfig;
|
|
|
64
64
|
* level: error
|
|
65
65
|
*/
|
|
66
66
|
export declare function parseCustomRulesFromYaml(content: string): CustomRule[];
|
|
67
|
+
/**
|
|
68
|
+
* Parse the patrol.ignore list from archbyte.yaml.
|
|
69
|
+
* Returns user-defined glob patterns for watch mode to ignore.
|
|
70
|
+
*
|
|
71
|
+
* patrol:
|
|
72
|
+
* ignore:
|
|
73
|
+
* - "docs/"
|
|
74
|
+
* - "*.md"
|
|
75
|
+
* - "build/"
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadPatrolIgnore(configPath?: string): string[];
|
|
67
78
|
export declare function getRuleLevel(config: RuleConfig, rule: keyof RuleConfig, defaultLevel: RuleLevel): RuleLevel;
|
|
68
79
|
export declare function getThreshold(config: RuleConfig, rule: "max-connections", defaultVal: number): number;
|