archbyte 0.3.5 → 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/README.md +42 -0
- package/bin/archbyte.js +26 -25
- package/dist/agents/pipeline/merger.d.ts +2 -2
- package/dist/agents/pipeline/merger.js +165 -35
- package/dist/agents/pipeline/types.d.ts +29 -1
- package/dist/agents/pipeline/types.js +0 -1
- package/dist/agents/providers/claude-sdk.d.ts +7 -0
- package/dist/agents/providers/claude-sdk.js +83 -0
- package/dist/agents/providers/router.d.ts +5 -0
- package/dist/agents/providers/router.js +23 -1
- package/dist/agents/runtime/types.d.ts +6 -2
- package/dist/agents/runtime/types.js +6 -1
- 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 +65 -18
- package/dist/cli/arch-diff.d.ts +38 -0
- package/dist/cli/arch-diff.js +61 -0
- package/dist/cli/auth.d.ts +8 -2
- package/dist/cli/auth.js +241 -31
- package/dist/cli/config.js +31 -5
- package/dist/cli/export.js +64 -2
- package/dist/cli/patrol.d.ts +5 -3
- package/dist/cli/patrol.js +417 -65
- package/dist/cli/setup.js +76 -8
- package/dist/cli/shared.d.ts +11 -0
- package/dist/cli/shared.js +61 -0
- package/dist/cli/ui.d.ts +9 -0
- package/dist/cli/ui.js +59 -5
- package/dist/cli/validate.d.ts +0 -1
- package/dist/cli/validate.js +0 -16
- package/dist/server/src/index.js +593 -19
- package/package.json +4 -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-BTo0zV5E.js +0 -70
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// === Model Routing ===
|
|
3
3
|
export const MODEL_MAP = {
|
|
4
4
|
anthropic: {
|
|
5
|
-
fast: "claude-
|
|
5
|
+
fast: "claude-sonnet-4-5-20250929",
|
|
6
6
|
standard: "claude-sonnet-4-5-20250929",
|
|
7
7
|
advanced: "claude-opus-4-6",
|
|
8
8
|
},
|
|
9
|
+
"claude-sdk": {
|
|
10
|
+
fast: "sonnet",
|
|
11
|
+
standard: "sonnet",
|
|
12
|
+
advanced: "opus",
|
|
13
|
+
},
|
|
9
14
|
openai: {
|
|
10
15
|
fast: "gpt-5.2",
|
|
11
16
|
standard: "gpt-5.2",
|
|
@@ -74,15 +74,21 @@ export async function detectComponents(tk, structure) {
|
|
|
74
74
|
// Strategy 1: Monorepo workspaces
|
|
75
75
|
if (structure.isMonorepo) {
|
|
76
76
|
const components = await detectFromWorkspaces(tk, structure);
|
|
77
|
-
if (components.length > 0)
|
|
77
|
+
if (components.length > 0) {
|
|
78
|
+
await enrichWithFileMetrics(tk, components);
|
|
78
79
|
return { components };
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
82
|
// Strategy 2: Scan ALL top-level directories for build configs + conventional names
|
|
81
83
|
const components = await detectAllComponents(tk, structure);
|
|
82
|
-
if (components.length > 0)
|
|
84
|
+
if (components.length > 0) {
|
|
85
|
+
await enrichWithFileMetrics(tk, components);
|
|
83
86
|
return { components };
|
|
87
|
+
}
|
|
84
88
|
// Strategy 3: Single app fallback
|
|
85
|
-
|
|
89
|
+
const fallback = [buildSingleAppComponent(structure)];
|
|
90
|
+
await enrichWithFileMetrics(tk, fallback);
|
|
91
|
+
return { components: fallback };
|
|
86
92
|
}
|
|
87
93
|
async function detectFromWorkspaces(tk, structure) {
|
|
88
94
|
// Collect workspace patterns from package.json OR pnpm-workspace.yaml
|
|
@@ -383,6 +389,32 @@ function extractTechStack(pkg) {
|
|
|
383
389
|
function capitalize(s) {
|
|
384
390
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
385
391
|
}
|
|
392
|
+
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i;
|
|
393
|
+
async function enrichWithFileMetrics(tk, components) {
|
|
394
|
+
await Promise.all(components.map(async (comp) => {
|
|
395
|
+
if (comp.path === ".")
|
|
396
|
+
return;
|
|
397
|
+
try {
|
|
398
|
+
const allFiles = await tk.globFiles(`**/*`, comp.path);
|
|
399
|
+
const codeFiles = allFiles.filter((f) => CODE_EXTENSIONS.test(f));
|
|
400
|
+
let totalLines = 0;
|
|
401
|
+
const filesToCount = codeFiles.slice(0, 150);
|
|
402
|
+
await Promise.all(filesToCount.map(async (file) => {
|
|
403
|
+
const content = await tk.readFileSafe(file);
|
|
404
|
+
if (content)
|
|
405
|
+
totalLines += content.split("\n").length;
|
|
406
|
+
}));
|
|
407
|
+
comp.fileMetrics = {
|
|
408
|
+
fileCount: allFiles.length,
|
|
409
|
+
totalLines,
|
|
410
|
+
codeFileCount: codeFiles.length,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// skip
|
|
415
|
+
}
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
386
418
|
function extractFirstParagraph(readme) {
|
|
387
419
|
const lines = readme.split("\n");
|
|
388
420
|
let capturing = false;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ConnectionResult, StaticComponent, InfraResult, EventResult } from "./types.js";
|
|
2
2
|
import type { StaticToolkit } from "./utils.js";
|
|
3
|
-
export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult): Promise<ConnectionResult>;
|
|
3
|
+
export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult, importMap?: Record<string, string[]>): Promise<ConnectionResult>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Maps connections between components via imports, Docker, K8s, env vars, known SDKs
|
|
3
3
|
// Only deterministic matches — ambiguous mappings are left as gaps for the LLM
|
|
4
4
|
import { slugify } from "./utils.js";
|
|
5
|
-
export async function mapConnections(tk, components, infra, events) {
|
|
5
|
+
export async function mapConnections(tk, components, infra, events, importMap) {
|
|
6
6
|
const connections = [];
|
|
7
7
|
const componentIds = new Set(components.map((c) => c.id));
|
|
8
8
|
// Run all detection methods in parallel
|
|
@@ -10,6 +10,7 @@ export async function mapConnections(tk, components, infra, events) {
|
|
|
10
10
|
detectDockerDependencies(infra, components, connections),
|
|
11
11
|
detectK8sIngress(infra, components, connections),
|
|
12
12
|
detectImportConnections(tk, components, connections),
|
|
13
|
+
detectImportMapConnections(components, connections, importMap ?? {}),
|
|
13
14
|
detectDatabaseConnections(tk, components, connections),
|
|
14
15
|
detectServerServesUI(tk, components, connections),
|
|
15
16
|
detectKnownSDKConnections(components, connections),
|
|
@@ -225,6 +226,78 @@ async function detectKnownSDKConnections(components, connections) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Detect cross-component connections from the code-sampler import map.
|
|
231
|
+
* Resolves relative imports and workspace package names to component boundaries.
|
|
232
|
+
*/
|
|
233
|
+
async function detectImportMapConnections(components, connections, importMap) {
|
|
234
|
+
if (Object.keys(importMap).length === 0)
|
|
235
|
+
return;
|
|
236
|
+
// Build component path → id lookup, sorted longest-first for greedy matching
|
|
237
|
+
const compPathEntries = components
|
|
238
|
+
.filter((c) => c.path !== ".")
|
|
239
|
+
.map((c) => ({ path: c.path.endsWith("/") ? c.path : c.path + "/", id: c.id }))
|
|
240
|
+
.sort((a, b) => b.path.length - a.path.length);
|
|
241
|
+
// Build package name → component id lookup (for monorepo imports)
|
|
242
|
+
const pkgNameToComp = new Map();
|
|
243
|
+
for (const comp of components) {
|
|
244
|
+
pkgNameToComp.set(comp.id, comp.id);
|
|
245
|
+
pkgNameToComp.set(comp.name, comp.id);
|
|
246
|
+
// Handle scoped package names: @org/name → name
|
|
247
|
+
const unscoped = comp.name.replace(/^@[^/]+\//, "");
|
|
248
|
+
if (unscoped !== comp.name)
|
|
249
|
+
pkgNameToComp.set(unscoped, comp.id);
|
|
250
|
+
}
|
|
251
|
+
const findCompForFile = (filePath) => {
|
|
252
|
+
for (const entry of compPathEntries) {
|
|
253
|
+
if (filePath.startsWith(entry.path) || filePath === entry.path.slice(0, -1)) {
|
|
254
|
+
return entry.id;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
};
|
|
259
|
+
// Track seen connections to avoid duplicates within this strategy
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const [filePath, imports] of Object.entries(importMap)) {
|
|
262
|
+
const sourceComp = findCompForFile(filePath);
|
|
263
|
+
if (!sourceComp)
|
|
264
|
+
continue;
|
|
265
|
+
for (const imp of imports) {
|
|
266
|
+
let targetComp = null;
|
|
267
|
+
if (imp.startsWith(".") || imp.startsWith("/")) {
|
|
268
|
+
// Relative import — resolve path
|
|
269
|
+
const fromDir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
270
|
+
const parts = fromDir.split("/");
|
|
271
|
+
for (const seg of imp.split("/")) {
|
|
272
|
+
if (seg === "..")
|
|
273
|
+
parts.pop();
|
|
274
|
+
else if (seg !== ".")
|
|
275
|
+
parts.push(seg);
|
|
276
|
+
}
|
|
277
|
+
targetComp = findCompForFile(parts.join("/") + "/");
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Package/bare import — check if it maps to a workspace component
|
|
281
|
+
const name = imp.split("/").slice(0, imp.startsWith("@") ? 2 : 1).join("/");
|
|
282
|
+
targetComp = pkgNameToComp.get(name) ?? pkgNameToComp.get(name.replace(/^@[^/]+\//, "")) ?? null;
|
|
283
|
+
}
|
|
284
|
+
if (targetComp && targetComp !== sourceComp) {
|
|
285
|
+
const key = `${sourceComp}::${targetComp}`;
|
|
286
|
+
if (seen.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(key);
|
|
289
|
+
connections.push({
|
|
290
|
+
from: sourceComp,
|
|
291
|
+
to: targetComp,
|
|
292
|
+
type: "import",
|
|
293
|
+
description: `Import: ${sourceComp} → ${targetComp}`,
|
|
294
|
+
confidence: 85,
|
|
295
|
+
async: false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
228
301
|
/**
|
|
229
302
|
* Exact match only: id, slug, path, or name must match exactly.
|
|
230
303
|
* No fuzzy matching, synonyms, or partial matches — those are for the LLM.
|
|
@@ -37,9 +37,12 @@ export async function runStaticAnalysis(projectRoot, onProgress) {
|
|
|
37
37
|
onProgress?.("Detecting components...");
|
|
38
38
|
const components = await detectComponents(tk, structure);
|
|
39
39
|
onProgress?.(`Found ${components.components.length} component(s)`);
|
|
40
|
-
// Phase
|
|
40
|
+
// Phase 2.5: collect import map for enriched connection detection
|
|
41
|
+
onProgress?.("Building import map...");
|
|
42
|
+
const codeSamples = await collectCodeSamples(tk);
|
|
43
|
+
// Phase 3: connection mapping (needs components + infra + events + import map)
|
|
41
44
|
onProgress?.("Mapping connections...");
|
|
42
|
-
const connections = await mapConnections(tk, components.components, infra, events);
|
|
45
|
+
const connections = await mapConnections(tk, components.components, infra, events, codeSamples.importMap);
|
|
43
46
|
onProgress?.(`Found ${connections.connections.length} connection(s)`);
|
|
44
47
|
// Assemble result
|
|
45
48
|
const analysis = {
|
|
@@ -32,6 +32,20 @@ export interface StaticComponent {
|
|
|
32
32
|
path: string;
|
|
33
33
|
description: string;
|
|
34
34
|
technologies: string[];
|
|
35
|
+
/** Coupling: count of public exports (from enricher) */
|
|
36
|
+
interfaceSurface?: number;
|
|
37
|
+
/** Security: has auth/permission boundary (from enricher) */
|
|
38
|
+
hasBoundary?: boolean;
|
|
39
|
+
/** Security: type of boundary e.g. "auth-middleware", "rbac" */
|
|
40
|
+
boundaryType?: string;
|
|
41
|
+
/** Key public export names (from enricher) */
|
|
42
|
+
publicExports?: string[];
|
|
43
|
+
/** File metrics from static analysis */
|
|
44
|
+
fileMetrics?: {
|
|
45
|
+
fileCount: number;
|
|
46
|
+
totalLines: number;
|
|
47
|
+
codeFileCount: number;
|
|
48
|
+
};
|
|
35
49
|
}
|
|
36
50
|
export interface InfraResult {
|
|
37
51
|
docker: {
|
|
@@ -90,6 +104,8 @@ export interface StaticConnection {
|
|
|
90
104
|
description: string;
|
|
91
105
|
confidence: number;
|
|
92
106
|
async: boolean;
|
|
107
|
+
/** Coupling weight 1-10 (from enricher) */
|
|
108
|
+
weight?: number;
|
|
93
109
|
}
|
|
94
110
|
export interface ConnectionResult {
|
|
95
111
|
connections: StaticConnection[];
|
|
@@ -130,6 +146,16 @@ export interface StaticAnalysisResult {
|
|
|
130
146
|
validation: ValidationResult;
|
|
131
147
|
/** Gaps the static analysis couldn't resolve — passed to LLM for resolution */
|
|
132
148
|
gaps: AnalysisGap[];
|
|
149
|
+
/** Free-form architectural observations from enricher */
|
|
150
|
+
enrichmentInsights?: string[];
|
|
151
|
+
/** Flow verification results from enricher */
|
|
152
|
+
flowVerifications?: Array<{
|
|
153
|
+
flowName: string;
|
|
154
|
+
verified: boolean;
|
|
155
|
+
stepsVerified: number;
|
|
156
|
+
stepsTotal: number;
|
|
157
|
+
issues?: string[];
|
|
158
|
+
}>;
|
|
133
159
|
}
|
|
134
160
|
export interface TreeEntry {
|
|
135
161
|
path: string;
|
package/dist/cli/analyze.js
CHANGED
|
@@ -72,31 +72,38 @@ export async function handleAnalyze(options) {
|
|
|
72
72
|
if (options.provider) {
|
|
73
73
|
config = {
|
|
74
74
|
provider: options.provider,
|
|
75
|
-
apiKey: options.apiKey ?? config?.apiKey ?? "",
|
|
75
|
+
...(options.provider !== "claude-sdk" ? { apiKey: options.apiKey ?? config?.apiKey ?? "" } : {}),
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
if (options.apiKey && config) {
|
|
79
79
|
config.apiKey = options.apiKey;
|
|
80
80
|
}
|
|
81
81
|
if (!config) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
const msg = [
|
|
83
|
+
chalk.red("No model provider configured."),
|
|
84
|
+
"",
|
|
85
|
+
chalk.bold("Zero-config (Claude Code users):"),
|
|
86
|
+
chalk.gray(" Install Claude Code → archbyte analyze just works"),
|
|
87
|
+
"",
|
|
88
|
+
chalk.bold("Or set up with:"),
|
|
89
|
+
chalk.gray(" archbyte config set provider anthropic"),
|
|
90
|
+
chalk.gray(" archbyte config set api-key sk-ant-..."),
|
|
91
|
+
"",
|
|
92
|
+
chalk.bold("Or use environment variables:"),
|
|
93
|
+
chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"),
|
|
94
|
+
chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."),
|
|
95
|
+
"",
|
|
96
|
+
chalk.bold("Or run without a model:"),
|
|
97
|
+
chalk.gray(" archbyte analyze --static"),
|
|
98
|
+
"",
|
|
99
|
+
chalk.bold("Supported providers:"),
|
|
100
|
+
chalk.gray(" anthropic, openai, google, claude-sdk"),
|
|
101
|
+
].join("\n");
|
|
102
|
+
console.error(msg);
|
|
103
|
+
throw new Error("No model provider configured");
|
|
98
104
|
}
|
|
99
|
-
|
|
105
|
+
const providerLabel = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
|
|
106
|
+
console.log(chalk.gray(`Provider: ${chalk.white(providerLabel)}`));
|
|
100
107
|
console.log(chalk.gray(`Project: ${chalk.white(path.basename(rootDir))}`));
|
|
101
108
|
console.log();
|
|
102
109
|
// 2. Create provider
|
|
@@ -173,6 +180,19 @@ export async function handleAnalyze(options) {
|
|
|
173
180
|
console.log();
|
|
174
181
|
}
|
|
175
182
|
}
|
|
183
|
+
// 3b. File-tree drift detection (catches zero-commit and major project growth)
|
|
184
|
+
if (priorMeta && !options.force && !incrementalContext) {
|
|
185
|
+
const currentFileCount = countProjectFiles(rootDir);
|
|
186
|
+
const priorCount = priorMeta.filesScanned ?? 0;
|
|
187
|
+
if (currentFileCount > 0 && priorCount > 0 && currentFileCount <= priorCount * 1.5) {
|
|
188
|
+
// File count hasn't grown significantly — skip re-scan
|
|
189
|
+
console.log(chalk.green("No significant changes detected since last scan. Use --force to re-scan."));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (currentFileCount > priorCount) {
|
|
193
|
+
console.log(chalk.yellow(`File tree grew from ${priorCount} to ${currentFileCount} files — running full scan.`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
176
196
|
// 4. Run static context collection → LLM pipeline
|
|
177
197
|
const progress = progressBar(7);
|
|
178
198
|
progress.update(0, "Collecting static context...");
|
|
@@ -324,6 +344,33 @@ function getGitCommit() {
|
|
|
324
344
|
return undefined;
|
|
325
345
|
}
|
|
326
346
|
}
|
|
347
|
+
function countProjectFiles(rootDir) {
|
|
348
|
+
let count = 0;
|
|
349
|
+
const skip = new Set(["node_modules", ".git", ".archbyte", ".next", "dist", "build", ".cache"]);
|
|
350
|
+
function walk(dir, depth) {
|
|
351
|
+
if (depth > 4)
|
|
352
|
+
return;
|
|
353
|
+
try {
|
|
354
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
if (entry.name.startsWith(".") && entry.name !== ".github")
|
|
357
|
+
continue;
|
|
358
|
+
if (skip.has(entry.name))
|
|
359
|
+
continue;
|
|
360
|
+
const full = path.join(dir, entry.name);
|
|
361
|
+
if (entry.isDirectory()) {
|
|
362
|
+
walk(full, depth + 1);
|
|
363
|
+
}
|
|
364
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i.test(entry.name)) {
|
|
365
|
+
count++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch { /* permission errors, etc. */ }
|
|
370
|
+
}
|
|
371
|
+
walk(rootDir, 0);
|
|
372
|
+
return count;
|
|
373
|
+
}
|
|
327
374
|
function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage, incrementalMode, skippedAgents) {
|
|
328
375
|
const meta = {
|
|
329
376
|
analyzedAt: new Date().toISOString(),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture diff — computes structural changes between two architecture snapshots.
|
|
3
|
+
* Used by patrol for change detection and by diff command for comparison.
|
|
4
|
+
*/
|
|
5
|
+
import type { Architecture } from "../server/src/generator/index.js";
|
|
6
|
+
export interface StructuralDiff {
|
|
7
|
+
addedNodes: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
type: string;
|
|
11
|
+
layer: string;
|
|
12
|
+
}>;
|
|
13
|
+
removedNodes: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
type: string;
|
|
17
|
+
layer: string;
|
|
18
|
+
}>;
|
|
19
|
+
modifiedNodes: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
field: string;
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
}>;
|
|
25
|
+
addedEdges: Array<{
|
|
26
|
+
source: string;
|
|
27
|
+
target: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
}>;
|
|
30
|
+
removedEdges: Array<{
|
|
31
|
+
source: string;
|
|
32
|
+
target: string;
|
|
33
|
+
label?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function diffArchitectures(prev: Architecture, curr: Architecture): StructuralDiff;
|
|
37
|
+
export declare function loadArchitectureJSON(rootDir: string): Architecture | null;
|
|
38
|
+
export declare function hasStructuralChanges(diff: StructuralDiff): boolean;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture diff — computes structural changes between two architecture snapshots.
|
|
3
|
+
* Used by patrol for change detection and by diff command for comparison.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
export function diffArchitectures(prev, curr) {
|
|
8
|
+
const prevNodeMap = new Map();
|
|
9
|
+
for (const n of prev.nodes)
|
|
10
|
+
prevNodeMap.set(n.id, n);
|
|
11
|
+
const currNodeMap = new Map();
|
|
12
|
+
for (const n of curr.nodes)
|
|
13
|
+
currNodeMap.set(n.id, n);
|
|
14
|
+
const prevNodeIds = new Set(prev.nodes.map((n) => n.id));
|
|
15
|
+
const currNodeIds = new Set(curr.nodes.map((n) => n.id));
|
|
16
|
+
const addedNodes = curr.nodes
|
|
17
|
+
.filter((n) => !prevNodeIds.has(n.id))
|
|
18
|
+
.map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
|
|
19
|
+
const removedNodes = prev.nodes
|
|
20
|
+
.filter((n) => !currNodeIds.has(n.id))
|
|
21
|
+
.map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
|
|
22
|
+
const modifiedNodes = [];
|
|
23
|
+
for (const n of curr.nodes) {
|
|
24
|
+
const old = prevNodeMap.get(n.id);
|
|
25
|
+
if (!old)
|
|
26
|
+
continue;
|
|
27
|
+
for (const field of ["label", "type", "layer"]) {
|
|
28
|
+
if (old[field] !== n[field]) {
|
|
29
|
+
modifiedNodes.push({ id: n.id, field, from: old[field], to: n[field] });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const edgeKey = (e) => `${e.source}->${e.target}`;
|
|
34
|
+
const prevEdgeKeys = new Set(prev.edges.map(edgeKey));
|
|
35
|
+
const currEdgeKeys = new Set(curr.edges.map(edgeKey));
|
|
36
|
+
const addedEdges = curr.edges
|
|
37
|
+
.filter((e) => !prevEdgeKeys.has(edgeKey(e)))
|
|
38
|
+
.map((e) => ({ source: e.source, target: e.target, label: e.label }));
|
|
39
|
+
const removedEdges = prev.edges
|
|
40
|
+
.filter((e) => !currEdgeKeys.has(edgeKey(e)))
|
|
41
|
+
.map((e) => ({ source: e.source, target: e.target, label: e.label }));
|
|
42
|
+
return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
|
|
43
|
+
}
|
|
44
|
+
export function loadArchitectureJSON(rootDir) {
|
|
45
|
+
const archPath = path.join(rootDir, ".archbyte", "architecture.json");
|
|
46
|
+
if (!fs.existsSync(archPath))
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(archPath, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function hasStructuralChanges(diff) {
|
|
56
|
+
return (diff.addedNodes.length > 0 ||
|
|
57
|
+
diff.removedNodes.length > 0 ||
|
|
58
|
+
diff.modifiedNodes.length > 0 ||
|
|
59
|
+
diff.addedEdges.length > 0 ||
|
|
60
|
+
diff.removedEdges.length > 0);
|
|
61
|
+
}
|
package/dist/cli/auth.d.ts
CHANGED
|
@@ -5,10 +5,16 @@ interface Credentials {
|
|
|
5
5
|
expiresAt: string;
|
|
6
6
|
}
|
|
7
7
|
export type OAuthProvider = "github" | "google";
|
|
8
|
-
export
|
|
8
|
+
export type LoginProvider = OAuthProvider | "email";
|
|
9
|
+
export declare function handleLogin(provider?: LoginProvider): Promise<void>;
|
|
9
10
|
export declare function handleLoginWithToken(token: string): Promise<void>;
|
|
10
|
-
export declare function handleLogout(
|
|
11
|
+
export declare function handleLogout(options?: {
|
|
12
|
+
all?: boolean;
|
|
13
|
+
email?: string;
|
|
14
|
+
}): Promise<void>;
|
|
11
15
|
export declare function handleStatus(): Promise<void>;
|
|
16
|
+
export declare function handleAccounts(): Promise<void>;
|
|
17
|
+
export declare function handleAccountSwitch(email?: string): Promise<void>;
|
|
12
18
|
export declare function loadCredentials(): Credentials | null;
|
|
13
19
|
/**
|
|
14
20
|
* Get the tier from the JWT token payload. NEVER falls back to the
|