archbyte 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -25
- package/bin/archbyte.js +6 -41
- package/dist/agents/static/component-detector.js +71 -107
- package/dist/agents/static/connection-mapper.js +24 -25
- package/dist/agents/static/deep-drill.d.ts +72 -0
- package/dist/agents/static/deep-drill.js +388 -0
- package/dist/agents/static/doc-parser.js +73 -48
- package/dist/agents/static/env-detector.js +3 -6
- package/dist/agents/static/event-detector.js +20 -26
- package/dist/agents/static/infra-analyzer.js +15 -1
- package/dist/agents/static/structure-scanner.js +56 -57
- package/dist/agents/static/taxonomy.d.ts +19 -0
- package/dist/agents/static/taxonomy.js +147 -0
- package/dist/agents/tools/local-fs.js +5 -2
- package/dist/cli/analyze.js +49 -27
- package/dist/cli/license-gate.js +47 -19
- package/dist/cli/run.js +117 -1
- package/dist/cli/setup.d.ts +6 -1
- package/dist/cli/setup.js +35 -16
- package/dist/cli/shared.d.ts +0 -11
- package/dist/cli/shared.js +0 -61
- package/dist/cli/workflow.js +8 -15
- package/dist/server/src/index.js +276 -168
- package/package.json +2 -2
- package/templates/archbyte.yaml +28 -7
- package/ui/dist/assets/index-BQouokNH.css +1 -0
- package/ui/dist/assets/index-QllGSFhe.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/cli/arch-diff.d.ts +0 -38
- package/dist/cli/arch-diff.js +0 -61
- package/dist/cli/diff.d.ts +0 -10
- package/dist/cli/diff.js +0 -144
- package/dist/cli/patrol.d.ts +0 -18
- package/dist/cli/patrol.js +0 -596
- package/dist/cli/validate.d.ts +0 -53
- package/dist/cli/validate.js +0 -299
- package/ui/dist/assets/index-DDCNauh7.css +0 -1
- package/ui/dist/assets/index-DO4t5Xu1.js +0 -72
|
@@ -150,7 +150,7 @@ async function detectCI(tk, result) {
|
|
|
150
150
|
}
|
|
151
151
|
async function detectCloud(tk, result) {
|
|
152
152
|
// Check for IaC and cloud config in parallel
|
|
153
|
-
const [terraformFiles,
|
|
153
|
+
const [terraformFiles, wranglerTomlRoot, vercelJson, netlifyToml, awsCdk, samTemplate, serverlessYml,] = await Promise.all([
|
|
154
154
|
tk.globFiles("**/*.tf"),
|
|
155
155
|
tk.readFileSafe("wrangler.toml"),
|
|
156
156
|
tk.readJSON("vercel.json"),
|
|
@@ -159,6 +159,20 @@ async function detectCloud(tk, result) {
|
|
|
159
159
|
tk.readYAML("template.yaml"), // AWS SAM
|
|
160
160
|
tk.readYAML("serverless.yml"),
|
|
161
161
|
]);
|
|
162
|
+
// Also check subdirectories for wrangler.toml (e.g. cloud/, workers/, api/)
|
|
163
|
+
let wranglerToml = wranglerTomlRoot;
|
|
164
|
+
if (!wranglerToml) {
|
|
165
|
+
const rootEntries = await tk.listDir(".");
|
|
166
|
+
for (const entry of rootEntries) {
|
|
167
|
+
if (entry.type === "directory") {
|
|
168
|
+
const sub = await tk.readFileSafe(`${entry.name}/wrangler.toml`);
|
|
169
|
+
if (sub) {
|
|
170
|
+
wranglerToml = sub;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
162
176
|
if (terraformFiles.length > 0) {
|
|
163
177
|
result.cloud.iac = "Terraform";
|
|
164
178
|
// Try to detect provider from tf files
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Static Analysis — Structure Scanner
|
|
2
2
|
// Detects project language, framework, monorepo, package manager, entry points
|
|
3
|
+
import { categorizeDep } from "./taxonomy.js";
|
|
3
4
|
export async function scanStructure(tk) {
|
|
4
5
|
const result = {
|
|
5
6
|
projectName: "",
|
|
@@ -56,84 +57,82 @@ export async function scanStructure(tk) {
|
|
|
56
57
|
if (result.language === "unknown")
|
|
57
58
|
result.language = "Python";
|
|
58
59
|
}
|
|
59
|
-
//
|
|
60
|
+
// Collect all dep names from root + common subdirectory package.json files
|
|
61
|
+
const allDepNames = [];
|
|
60
62
|
if (pkg) {
|
|
61
|
-
|
|
63
|
+
allDepNames.push(...Object.keys({
|
|
62
64
|
...pkg.dependencies,
|
|
63
65
|
...pkg.devDependencies,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
else if (depNames.includes("svelte"))
|
|
84
|
-
result.framework = "Svelte";
|
|
85
|
-
else if (depNames.includes("angular"))
|
|
86
|
-
result.framework = "Angular";
|
|
87
|
-
// Test framework
|
|
88
|
-
if (depNames.includes("vitest"))
|
|
89
|
-
result.testFramework = "Vitest";
|
|
90
|
-
else if (depNames.includes("jest"))
|
|
91
|
-
result.testFramework = "Jest";
|
|
92
|
-
else if (depNames.includes("mocha"))
|
|
93
|
-
result.testFramework = "Mocha";
|
|
94
|
-
// Build system
|
|
95
|
-
if (depNames.includes("vite"))
|
|
96
|
-
result.buildSystem = "Vite";
|
|
97
|
-
else if (depNames.includes("webpack"))
|
|
98
|
-
result.buildSystem = "Webpack";
|
|
99
|
-
else if (depNames.includes("esbuild"))
|
|
100
|
-
result.buildSystem = "esbuild";
|
|
101
|
-
else if (depNames.includes("rollup"))
|
|
102
|
-
result.buildSystem = "Rollup";
|
|
103
|
-
// Monorepo detection
|
|
104
|
-
if (pkg.workspaces) {
|
|
105
|
-
result.isMonorepo = true;
|
|
106
|
-
result.monorepoTool = "npm-workspaces";
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
const UI_DIRS = ["ui", "client", "frontend", "web", "app", "packages"];
|
|
69
|
+
const subPkgs = await Promise.all(UI_DIRS.map((d) => tk.readJSON(`${d}/package.json`)));
|
|
70
|
+
for (const subPkg of subPkgs) {
|
|
71
|
+
if (!subPkg)
|
|
72
|
+
continue;
|
|
73
|
+
allDepNames.push(...Object.keys({
|
|
74
|
+
...subPkg.dependencies,
|
|
75
|
+
...subPkg.devDependencies,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
// Framework: first dep matching meta-framework, ui-framework, or http-framework
|
|
79
|
+
const frameworkCategories = new Set(["meta-framework", "ui-framework", "http-framework"]);
|
|
80
|
+
for (const dep of allDepNames) {
|
|
81
|
+
const cat = categorizeDep(dep);
|
|
82
|
+
if (cat && frameworkCategories.has(cat.category)) {
|
|
83
|
+
result.framework = cat.displayName;
|
|
84
|
+
break;
|
|
107
85
|
}
|
|
108
86
|
}
|
|
109
|
-
//
|
|
87
|
+
// Test framework: first dep matching test-framework
|
|
88
|
+
for (const dep of allDepNames) {
|
|
89
|
+
const cat = categorizeDep(dep);
|
|
90
|
+
if (cat?.category === "test-framework") {
|
|
91
|
+
result.testFramework = cat.displayName;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build system: first dep matching bundler
|
|
96
|
+
for (const dep of allDepNames) {
|
|
97
|
+
const cat = categorizeDep(dep);
|
|
98
|
+
if (cat?.category === "bundler") {
|
|
99
|
+
result.buildSystem = cat.displayName;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Monorepo detection
|
|
104
|
+
if (pkg?.workspaces) {
|
|
105
|
+
result.isMonorepo = true;
|
|
106
|
+
result.monorepoTool = "npm-workspaces";
|
|
107
|
+
}
|
|
108
|
+
// Python frameworks (text-based — no package.json)
|
|
110
109
|
if (pyProject) {
|
|
111
110
|
if (pyProject.includes("django"))
|
|
112
|
-
result.framework = "Django";
|
|
111
|
+
result.framework = result.framework ?? "Django";
|
|
113
112
|
else if (pyProject.includes("fastapi"))
|
|
114
|
-
result.framework = "FastAPI";
|
|
113
|
+
result.framework = result.framework ?? "FastAPI";
|
|
115
114
|
else if (pyProject.includes("flask"))
|
|
116
|
-
result.framework = "Flask";
|
|
115
|
+
result.framework = result.framework ?? "Flask";
|
|
117
116
|
}
|
|
118
117
|
// Rust frameworks
|
|
119
118
|
if (cargoToml) {
|
|
120
119
|
if (cargoToml.includes("axum"))
|
|
121
|
-
result.framework = "Axum";
|
|
120
|
+
result.framework = result.framework ?? "Axum";
|
|
122
121
|
else if (cargoToml.includes("actix"))
|
|
123
|
-
result.framework = "Actix";
|
|
122
|
+
result.framework = result.framework ?? "Actix";
|
|
124
123
|
else if (cargoToml.includes("rocket"))
|
|
125
|
-
result.framework = "Rocket";
|
|
126
|
-
result.buildSystem = "Cargo";
|
|
124
|
+
result.framework = result.framework ?? "Rocket";
|
|
125
|
+
result.buildSystem = result.buildSystem ?? "Cargo";
|
|
127
126
|
}
|
|
128
127
|
// Go frameworks
|
|
129
128
|
if (goMod) {
|
|
130
129
|
if (goMod.includes("gin-gonic"))
|
|
131
|
-
result.framework = "Gin";
|
|
130
|
+
result.framework = result.framework ?? "Gin";
|
|
132
131
|
else if (goMod.includes("labstack/echo"))
|
|
133
|
-
result.framework = "Echo";
|
|
132
|
+
result.framework = result.framework ?? "Echo";
|
|
134
133
|
else if (goMod.includes("go-fiber"))
|
|
135
|
-
result.framework = "Fiber";
|
|
136
|
-
result.buildSystem = "Go";
|
|
134
|
+
result.framework = result.framework ?? "Fiber";
|
|
135
|
+
result.buildSystem = result.buildSystem ?? "Go";
|
|
137
136
|
}
|
|
138
137
|
// Monorepo tools (check files in parallel)
|
|
139
138
|
const [nxJson, turboJson, lernaJson, pnpmWorkspace] = await Promise.all([
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface PackageCategory {
|
|
2
|
+
pattern: RegExp;
|
|
3
|
+
category: string;
|
|
4
|
+
role: string;
|
|
5
|
+
displayName?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const PACKAGE_TAXONOMY: PackageCategory[];
|
|
8
|
+
/** Role -> component type mapping (replaces DIR_TYPE_HINTS and detectTypeFromDeps) */
|
|
9
|
+
export declare const ROLE_TO_TYPE: Record<string, string>;
|
|
10
|
+
/** Manifest files that indicate a directory is an independent module */
|
|
11
|
+
export declare const MANIFEST_FILES: string[];
|
|
12
|
+
/** Categorize a single dependency name */
|
|
13
|
+
export declare function categorizeDep(name: string): (PackageCategory & {
|
|
14
|
+
displayName: string;
|
|
15
|
+
}) | null;
|
|
16
|
+
/** Categorize all deps from a manifest — returns only recognized deps */
|
|
17
|
+
export declare function categorizeAllDeps(depNames: string[]): Map<string, PackageCategory & {
|
|
18
|
+
displayName: string;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Static Analysis — Package Taxonomy
|
|
2
|
+
// Single source of truth for mapping package names to architectural categories.
|
|
3
|
+
// All scanners import from here instead of maintaining their own hardcoded lists.
|
|
4
|
+
export const PACKAGE_TAXONOMY = [
|
|
5
|
+
// Database clients
|
|
6
|
+
{ pattern: /^(pg|mysql2?|better-sqlite3|@libsql\/client|mongodb|mongoose)$/i, category: "database-client", role: "data", displayName: "PostgreSQL" },
|
|
7
|
+
{ pattern: /^(@prisma\/client|prisma|drizzle-orm|typeorm|sequelize|knex|mikro-orm)$/i, category: "orm", role: "data" },
|
|
8
|
+
// Cache
|
|
9
|
+
{ pattern: /^(redis|ioredis|@upstash\/redis|memcached|keyv)$/i, category: "cache-client", role: "data", displayName: "Redis" },
|
|
10
|
+
// Message queues
|
|
11
|
+
{ pattern: /^(kafkajs|amqplib|bullmq|bull|bee-queue|@aws-sdk\/client-sqs|@aws-sdk\/client-sns|@aws-sdk\/client-eventbridge|nats|mqtt|@google-cloud\/pubsub|@azure\/service-bus|@azure\/event-hubs|@confluentinc\/kafka-javascript)$/i, category: "queue-client", role: "messaging" },
|
|
12
|
+
// HTTP frameworks
|
|
13
|
+
{ pattern: /^(express|fastify|@nestjs\/core|hono|koa|@hapi\/hapi)$/i, category: "http-framework", role: "api" },
|
|
14
|
+
// Frontend frameworks
|
|
15
|
+
{ pattern: /^(react|react-dom|vue|svelte|@angular\/core|solid-js|preact)$/i, category: "ui-framework", role: "frontend" },
|
|
16
|
+
// Meta-frameworks
|
|
17
|
+
{ pattern: /^(next|nuxt|@remix-run\/node|gatsby|astro)$/i, category: "meta-framework", role: "frontend" },
|
|
18
|
+
// Cloud SDKs
|
|
19
|
+
{ pattern: /^@aws-sdk\//i, category: "cloud-sdk", role: "external", displayName: "AWS" },
|
|
20
|
+
{ pattern: /^@google-cloud\//i, category: "cloud-sdk", role: "external", displayName: "GCP" },
|
|
21
|
+
{ pattern: /^@azure\//i, category: "cloud-sdk", role: "external", displayName: "Azure" },
|
|
22
|
+
// AI/LLM SDKs
|
|
23
|
+
{ pattern: /^(@anthropic-ai\/sdk|openai|@google\/genai|@langchain\/.*)$/i, category: "ai-sdk", role: "external" },
|
|
24
|
+
// Payment
|
|
25
|
+
{ pattern: /^(stripe|@stripe)/i, category: "payment-sdk", role: "external", displayName: "Stripe" },
|
|
26
|
+
// Real-time
|
|
27
|
+
{ pattern: /^(socket\.io|ws|@socket\.io)/i, category: "realtime", role: "messaging" },
|
|
28
|
+
// SSE libraries
|
|
29
|
+
{ pattern: /^(sse-channel|better-sse)$/i, category: "sse", role: "messaging", displayName: "Server-Sent Events" },
|
|
30
|
+
// CLI frameworks
|
|
31
|
+
{ pattern: /^(commander|yargs|@oclif\/core|clipanion|cac)$/i, category: "cli-framework", role: "cli" },
|
|
32
|
+
// Build/bundlers
|
|
33
|
+
{ pattern: /^(vite|webpack|esbuild|rollup|parcel|tsup|turbopack)$/i, category: "bundler", role: "build" },
|
|
34
|
+
// Test frameworks
|
|
35
|
+
{ pattern: /^(vitest|jest|mocha|@playwright\/test|cypress)$/i, category: "test-framework", role: "test" },
|
|
36
|
+
// Secrets management
|
|
37
|
+
{ pattern: /^(dotenv|@aws-sdk\/client-secrets-manager|node-vault|vault|@google-cloud\/secret-manager|@azure\/keyvault-secrets|infisical-sdk)$/i, category: "secrets", role: "infra" },
|
|
38
|
+
// GraphQL
|
|
39
|
+
{ pattern: /^(graphql|@apollo\/server|@apollo\/client)$/i, category: "graphql", role: "api", displayName: "GraphQL" },
|
|
40
|
+
// CSS frameworks (architectural signal for UI)
|
|
41
|
+
{ pattern: /^(tailwindcss|@xyflow\/react)$/i, category: "ui-library", role: "frontend" },
|
|
42
|
+
];
|
|
43
|
+
// Display name overrides for specific packages (when pattern-level displayName is too broad)
|
|
44
|
+
const DISPLAY_NAME_OVERRIDES = {
|
|
45
|
+
"pg": "PostgreSQL",
|
|
46
|
+
"mysql2": "MySQL",
|
|
47
|
+
"mysql": "MySQL",
|
|
48
|
+
"mongodb": "MongoDB",
|
|
49
|
+
"mongoose": "MongoDB",
|
|
50
|
+
"redis": "Redis",
|
|
51
|
+
"ioredis": "Redis",
|
|
52
|
+
"@upstash/redis": "Redis",
|
|
53
|
+
"kafkajs": "Kafka",
|
|
54
|
+
"@confluentinc/kafka-javascript": "Kafka",
|
|
55
|
+
"amqplib": "RabbitMQ",
|
|
56
|
+
"bullmq": "BullMQ",
|
|
57
|
+
"bull": "Bull",
|
|
58
|
+
"bee-queue": "Bee-Queue",
|
|
59
|
+
"nats": "NATS",
|
|
60
|
+
"mqtt": "MQTT",
|
|
61
|
+
"@google-cloud/pubsub": "Google Pub/Sub",
|
|
62
|
+
"@aws-sdk/client-sqs": "AWS SQS",
|
|
63
|
+
"@aws-sdk/client-sns": "AWS SNS",
|
|
64
|
+
"@aws-sdk/client-eventbridge": "AWS EventBridge",
|
|
65
|
+
"@azure/service-bus": "Azure Service Bus",
|
|
66
|
+
"@azure/event-hubs": "Azure Event Hubs",
|
|
67
|
+
"socket.io": "Socket.IO",
|
|
68
|
+
"ws": "WebSocket",
|
|
69
|
+
"next": "Next.js",
|
|
70
|
+
"nuxt": "Nuxt",
|
|
71
|
+
"react": "React",
|
|
72
|
+
"react-dom": "React",
|
|
73
|
+
"vue": "Vue",
|
|
74
|
+
"svelte": "Svelte",
|
|
75
|
+
"@angular/core": "Angular",
|
|
76
|
+
"solid-js": "Solid",
|
|
77
|
+
"preact": "Preact",
|
|
78
|
+
"express": "Express",
|
|
79
|
+
"fastify": "Fastify",
|
|
80
|
+
"@nestjs/core": "NestJS",
|
|
81
|
+
"hono": "Hono",
|
|
82
|
+
"koa": "Koa",
|
|
83
|
+
"@prisma/client": "Prisma",
|
|
84
|
+
"prisma": "Prisma",
|
|
85
|
+
"drizzle-orm": "Drizzle",
|
|
86
|
+
"typeorm": "TypeORM",
|
|
87
|
+
"sequelize": "Sequelize",
|
|
88
|
+
"knex": "Knex",
|
|
89
|
+
"openai": "OpenAI",
|
|
90
|
+
"@anthropic-ai/sdk": "Anthropic",
|
|
91
|
+
"@google/genai": "Google AI",
|
|
92
|
+
"vite": "Vite",
|
|
93
|
+
"webpack": "Webpack",
|
|
94
|
+
"esbuild": "esbuild",
|
|
95
|
+
"rollup": "Rollup",
|
|
96
|
+
"vitest": "Vitest",
|
|
97
|
+
"jest": "Jest",
|
|
98
|
+
"mocha": "Mocha",
|
|
99
|
+
"@playwright/test": "Playwright",
|
|
100
|
+
"cypress": "Cypress",
|
|
101
|
+
"commander": "Commander.js",
|
|
102
|
+
"yargs": "Yargs",
|
|
103
|
+
"tailwindcss": "Tailwind CSS",
|
|
104
|
+
"@xyflow/react": "React Flow",
|
|
105
|
+
"graphql": "GraphQL",
|
|
106
|
+
"@apollo/server": "Apollo GraphQL",
|
|
107
|
+
"typescript": "TypeScript",
|
|
108
|
+
};
|
|
109
|
+
/** Role -> component type mapping (replaces DIR_TYPE_HINTS and detectTypeFromDeps) */
|
|
110
|
+
export const ROLE_TO_TYPE = {
|
|
111
|
+
frontend: "frontend",
|
|
112
|
+
api: "api",
|
|
113
|
+
data: "database",
|
|
114
|
+
messaging: "worker",
|
|
115
|
+
external: "service",
|
|
116
|
+
cli: "service",
|
|
117
|
+
build: "service",
|
|
118
|
+
test: "service",
|
|
119
|
+
infra: "service",
|
|
120
|
+
};
|
|
121
|
+
/** Manifest files that indicate a directory is an independent module */
|
|
122
|
+
export const MANIFEST_FILES = [
|
|
123
|
+
"package.json", "Cargo.toml", "go.mod", "pyproject.toml",
|
|
124
|
+
"requirements.txt", "setup.py", "pubspec.yaml", "build.gradle",
|
|
125
|
+
"pom.xml", "Gemfile", "Makefile", "Dockerfile", "wrangler.toml",
|
|
126
|
+
"tsconfig.json",
|
|
127
|
+
];
|
|
128
|
+
/** Categorize a single dependency name */
|
|
129
|
+
export function categorizeDep(name) {
|
|
130
|
+
const match = PACKAGE_TAXONOMY.find(t => t.pattern.test(name));
|
|
131
|
+
if (!match)
|
|
132
|
+
return null;
|
|
133
|
+
return {
|
|
134
|
+
...match,
|
|
135
|
+
displayName: DISPLAY_NAME_OVERRIDES[name] ?? match.displayName ?? name,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Categorize all deps from a manifest — returns only recognized deps */
|
|
139
|
+
export function categorizeAllDeps(depNames) {
|
|
140
|
+
const result = new Map();
|
|
141
|
+
for (const name of depNames) {
|
|
142
|
+
const cat = categorizeDep(name);
|
|
143
|
+
if (cat)
|
|
144
|
+
result.set(name, cat);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { resolve, relative, join } from "node:path";
|
|
3
3
|
import { EXCLUDED_DIRS } from "../static/excluded-dirs.js";
|
|
4
|
+
/** Dot-prefixed directories that should NOT be skipped during walks (CI/CD configs) */
|
|
5
|
+
const ALLOWED_DOT_DIRS = new Set([".github", ".circleci", ".gitlab"]);
|
|
4
6
|
export class LocalFSBackend {
|
|
5
7
|
root;
|
|
6
8
|
constructor(projectRoot) {
|
|
@@ -83,9 +85,10 @@ export class LocalFSBackend {
|
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
for (const entry of entries) {
|
|
86
|
-
if (
|
|
88
|
+
if (EXCLUDED_DIRS.has(entry.name))
|
|
89
|
+
continue;
|
|
90
|
+
if (entry.name.startsWith(".") && !ALLOWED_DOT_DIRS.has(entry.name))
|
|
87
91
|
continue;
|
|
88
|
-
}
|
|
89
92
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
90
93
|
if (entry.isDirectory()) {
|
|
91
94
|
await this.walk(base, relPath, regex, matches);
|
package/dist/cli/analyze.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveConfig } from "./config.js";
|
|
|
6
6
|
import { recordUsage } from "./license-gate.js";
|
|
7
7
|
import { staticResultToSpec, writeSpec, writeMetadata, loadSpec, loadMetadata } from "./yaml-io.js";
|
|
8
8
|
import { getChangedFiles, mapFilesToComponents, shouldRunAgents, isGitAvailable, categorizeChanges, computeNeighbors, getCommitCount } from "./incremental.js";
|
|
9
|
-
import { progressBar } from "./ui.js";
|
|
9
|
+
import { progressBar, confirm } from "./ui.js";
|
|
10
10
|
export async function handleAnalyze(options) {
|
|
11
11
|
const rootDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
12
12
|
const isStaticOnly = options.static || options.skipLlm;
|
|
@@ -48,12 +48,18 @@ export async function handleAnalyze(options) {
|
|
|
48
48
|
progress.update(0, `Static analysis: ${msg}`);
|
|
49
49
|
});
|
|
50
50
|
progress.update(1, "Building analysis...");
|
|
51
|
-
const
|
|
51
|
+
const freshAnalysis = buildAnalysisFromStatic(result, rootDir);
|
|
52
52
|
const duration = Date.now() - startTime;
|
|
53
|
+
// Merge into existing analysis if it was produced by an agentic run,
|
|
54
|
+
// preserving LLM-generated components/connections while refreshing
|
|
55
|
+
// static data (environments, metadata, project info).
|
|
56
|
+
const existingAnalysis = loadExistingAnalysis(rootDir);
|
|
57
|
+
const wasAgentic = existingAnalysis && existingAnalysis.metadata?.mode !== "static";
|
|
58
|
+
const analysis = wasAgentic ? mergeStaticIntoExisting(existingAnalysis, freshAnalysis) : freshAnalysis;
|
|
53
59
|
// Stamp scan metadata on analysis.json (backward compat)
|
|
54
60
|
const ameta = analysis.metadata;
|
|
55
61
|
ameta.durationMs = duration;
|
|
56
|
-
ameta.mode = "static";
|
|
62
|
+
ameta.mode = wasAgentic ? "static-refresh" : "static";
|
|
57
63
|
writeAnalysis(rootDir, analysis);
|
|
58
64
|
// Dual-write: archbyte.yaml + metadata.json
|
|
59
65
|
const existingSpec = loadSpec(rootDir);
|
|
@@ -79,28 +85,20 @@ export async function handleAnalyze(options) {
|
|
|
79
85
|
config.apiKey = options.apiKey;
|
|
80
86
|
}
|
|
81
87
|
if (!config) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
chalk.
|
|
93
|
-
|
|
94
|
-
|
|
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");
|
|
88
|
+
console.log(chalk.yellow("No model provider configured."));
|
|
89
|
+
console.log();
|
|
90
|
+
const shouldSetup = await confirm("Set up your AI provider now?");
|
|
91
|
+
if (shouldSetup) {
|
|
92
|
+
const { handleSetup } = await import("./setup.js");
|
|
93
|
+
await handleSetup();
|
|
94
|
+
config = resolveConfig();
|
|
95
|
+
}
|
|
96
|
+
if (!config) {
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(chalk.gray("Running static-only analysis (no AI)."));
|
|
99
|
+
console.log();
|
|
100
|
+
return handleAnalyze({ ...options, static: true });
|
|
101
|
+
}
|
|
104
102
|
}
|
|
105
103
|
const providerLabel = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
|
|
106
104
|
console.log(chalk.gray(`Provider: ${chalk.white(providerLabel)}`));
|
|
@@ -413,10 +411,34 @@ function printSummary(analysis, durationMs, mode, options) {
|
|
|
413
411
|
if (!options?.skipServeHint) {
|
|
414
412
|
console.log(` ${chalk.cyan("archbyte serve")} Open the interactive diagram`);
|
|
415
413
|
}
|
|
416
|
-
console.log(` ${chalk.cyan("archbyte validate")} Check architecture fitness rules ${chalk.yellow("[Pro]")}`);
|
|
417
|
-
console.log(` ${chalk.cyan("archbyte patrol")} Continuous architecture monitoring ${chalk.yellow("[Pro]")}`);
|
|
418
414
|
console.log();
|
|
419
415
|
}
|
|
416
|
+
// ─── Analysis merge helpers ───
|
|
417
|
+
function loadExistingAnalysis(rootDir) {
|
|
418
|
+
const p = path.join(rootDir, ".archbyte", "analysis.json");
|
|
419
|
+
try {
|
|
420
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Merge fresh static scan into an existing agentic analysis.
|
|
428
|
+
* Preserves: components, connections, flows, databases, externalServices (LLM-generated).
|
|
429
|
+
* Updates: project info, environments, metadata, c4/infra if present.
|
|
430
|
+
*/
|
|
431
|
+
function mergeStaticIntoExisting(existing, fresh) {
|
|
432
|
+
return {
|
|
433
|
+
...existing,
|
|
434
|
+
project: fresh.project,
|
|
435
|
+
environments: fresh.environments,
|
|
436
|
+
metadata: {
|
|
437
|
+
...(existing.metadata ?? {}),
|
|
438
|
+
...(fresh.metadata ?? {}),
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
420
442
|
// ─── Analysis converters ───
|
|
421
443
|
/**
|
|
422
444
|
* Convert a StaticAnalysisResult (from pipeline or static-only) into the
|
package/dist/cli/license-gate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { loadCredentials, cacheVerifiedTier, resetOfflineActions, checkOfflineAction } from "./auth.js";
|
|
3
3
|
import { API_BASE, NETWORK_TIMEOUT_MS } from "./constants.js";
|
|
4
|
+
import { confirm } from "./ui.js";
|
|
4
5
|
/**
|
|
5
6
|
* Pre-flight license check. Must be called before scan/analyze/generate.
|
|
6
7
|
*
|
|
@@ -16,25 +17,44 @@ import { API_BASE, NETWORK_TIMEOUT_MS } from "./constants.js";
|
|
|
16
17
|
* the 1-hour cache window. Exceeding limits blocks the action.
|
|
17
18
|
*/
|
|
18
19
|
export async function requireLicense(action) {
|
|
19
|
-
|
|
20
|
-
// Not logged in
|
|
20
|
+
let creds = loadCredentials();
|
|
21
|
+
// Not logged in — offer interactive login
|
|
21
22
|
if (!creds) {
|
|
22
|
-
console.
|
|
23
|
-
console.
|
|
24
|
-
console.
|
|
25
|
-
console.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.yellow("Not signed in."));
|
|
25
|
+
console.log(chalk.gray("Free tier includes unlimited scans. No credit card required."));
|
|
26
|
+
console.log();
|
|
27
|
+
const shouldLogin = await confirm("Sign in now?");
|
|
28
|
+
if (!shouldLogin)
|
|
29
|
+
process.exit(1);
|
|
30
|
+
const { handleLogin } = await import("./auth.js");
|
|
31
|
+
await handleLogin();
|
|
32
|
+
creds = loadCredentials();
|
|
33
|
+
if (!creds) {
|
|
34
|
+
console.error(chalk.red("Login failed. Please try again with `archbyte login`."));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
|
-
// Token expired locally
|
|
38
|
+
// Token expired locally — offer re-login
|
|
32
39
|
const expiry = new Date(creds.expiresAt);
|
|
33
40
|
if (isNaN(expiry.getTime()) || expiry < new Date()) {
|
|
34
|
-
console.
|
|
35
|
-
console.
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk.yellow("Session expired."));
|
|
43
|
+
const shouldRelogin = await confirm("Sign in again?");
|
|
44
|
+
if (!shouldRelogin)
|
|
45
|
+
process.exit(1);
|
|
46
|
+
const { handleLogin } = await import("./auth.js");
|
|
47
|
+
await handleLogin();
|
|
48
|
+
creds = loadCredentials();
|
|
49
|
+
if (!creds) {
|
|
50
|
+
console.error(chalk.red("Login failed. Please try again with `archbyte login`."));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const freshExpiry = new Date(creds.expiresAt);
|
|
54
|
+
if (isNaN(freshExpiry.getTime()) || freshExpiry < new Date()) {
|
|
55
|
+
console.error(chalk.red("Session still expired. Please try `archbyte login`."));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
38
58
|
}
|
|
39
59
|
// Check usage with server
|
|
40
60
|
try {
|
|
@@ -48,10 +68,18 @@ export async function requireLicense(action) {
|
|
|
48
68
|
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
49
69
|
});
|
|
50
70
|
if (res.status === 401) {
|
|
51
|
-
console.
|
|
52
|
-
console.
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(chalk.yellow("Session invalid."));
|
|
73
|
+
const shouldRelogin = await confirm("Sign in again?");
|
|
74
|
+
if (!shouldRelogin)
|
|
75
|
+
process.exit(1);
|
|
76
|
+
const { handleLogin } = await import("./auth.js");
|
|
77
|
+
await handleLogin();
|
|
78
|
+
creds = loadCredentials();
|
|
79
|
+
if (!creds)
|
|
80
|
+
process.exit(1);
|
|
81
|
+
// Re-check usage with fresh credentials
|
|
82
|
+
return requireLicense(action);
|
|
55
83
|
}
|
|
56
84
|
if (!res.ok) {
|
|
57
85
|
// Server error — enforce offline limits instead of allowing freely
|