archbyte 0.5.0 → 0.5.2

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.
@@ -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, wranglerToml, vercelJson, netlifyToml, awsCdk, samTemplate, serverlessYml,] = await Promise.all([
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
- // Framework detection from deps
60
+ // Collect all dep names from root + common subdirectory package.json files
61
+ const allDepNames = [];
60
62
  if (pkg) {
61
- const allDeps = {
63
+ allDepNames.push(...Object.keys({
62
64
  ...pkg.dependencies,
63
65
  ...pkg.devDependencies,
64
- };
65
- const depNames = Object.keys(allDeps);
66
- // JS/TS frameworks
67
- if (depNames.includes("next"))
68
- result.framework = "Next.js";
69
- else if (depNames.includes("nuxt"))
70
- result.framework = "Nuxt";
71
- else if (depNames.includes("@nestjs/core"))
72
- result.framework = "NestJS";
73
- else if (depNames.includes("fastify"))
74
- result.framework = "Fastify";
75
- else if (depNames.includes("express"))
76
- result.framework = "Express";
77
- else if (depNames.includes("hono"))
78
- result.framework = "Hono";
79
- else if (depNames.includes("react"))
80
- result.framework = "React";
81
- else if (depNames.includes("vue"))
82
- result.framework = "Vue";
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
- // Python frameworks
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 (entry.name.startsWith(".") || EXCLUDED_DIRS.has(entry.name)) {
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);
@@ -50,10 +50,11 @@ export async function handleAnalyze(options) {
50
50
  progress.update(1, "Building analysis...");
51
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,
53
+ // Merge into existing data if it was produced by an agentic run,
54
54
  // preserving LLM-generated components/connections while refreshing
55
55
  // static data (environments, metadata, project info).
56
56
  const existingAnalysis = loadExistingAnalysis(rootDir);
57
+ const existingSpec = loadSpec(rootDir);
57
58
  const wasAgentic = existingAnalysis && existingAnalysis.metadata?.mode !== "static";
58
59
  const analysis = wasAgentic ? mergeStaticIntoExisting(existingAnalysis, freshAnalysis) : freshAnalysis;
59
60
  // Stamp scan metadata on analysis.json (backward compat)
@@ -62,10 +63,19 @@ export async function handleAnalyze(options) {
62
63
  ameta.mode = wasAgentic ? "static-refresh" : "static";
63
64
  writeAnalysis(rootDir, analysis);
64
65
  // Dual-write: archbyte.yaml + metadata.json
65
- const existingSpec = loadSpec(rootDir);
66
- const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
67
- writeSpec(rootDir, spec);
68
- writeScanMetadata(rootDir, duration, "static");
66
+ // When prior data came from an agentic run, only refresh static fields
67
+ // (project info, environments) — never overwrite LLM components/connections.
68
+ if (wasAgentic && existingSpec) {
69
+ const freshSpec = staticResultToSpec(result, rootDir, existingSpec.rules);
70
+ existingSpec.project = freshSpec.project;
71
+ existingSpec.environments = freshSpec.environments;
72
+ writeSpec(rootDir, existingSpec);
73
+ }
74
+ else {
75
+ const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
76
+ writeSpec(rootDir, spec);
77
+ }
78
+ writeScanMetadata(rootDir, duration, wasAgentic ? "static-refresh" : "static");
69
79
  progress.update(2, "Generating diagram...");
70
80
  await autoGenerate(rootDir, options);
71
81
  progress.done("Analysis complete");
package/dist/cli/run.js CHANGED
@@ -42,7 +42,7 @@ export async function handleRun(options) {
42
42
  console.log();
43
43
  console.log(sep);
44
44
  console.log();
45
- console.log(dim(" Docs ") + chalk.cyan("https://archbyte.heartbyte.io/setup.html"));
45
+ console.log(dim(" Docs ") + chalk.cyan("https://archbyte.heartbyte.io/setup"));
46
46
  console.log(dim(" Website ") + chalk.cyan("https://archbyte.heartbyte.io"));
47
47
  console.log();
48
48
  console.log(sep);