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.
Files changed (38) hide show
  1. package/README.md +9 -25
  2. package/bin/archbyte.js +6 -41
  3. package/dist/agents/static/component-detector.js +71 -107
  4. package/dist/agents/static/connection-mapper.js +24 -25
  5. package/dist/agents/static/deep-drill.d.ts +72 -0
  6. package/dist/agents/static/deep-drill.js +388 -0
  7. package/dist/agents/static/doc-parser.js +73 -48
  8. package/dist/agents/static/env-detector.js +3 -6
  9. package/dist/agents/static/event-detector.js +20 -26
  10. package/dist/agents/static/infra-analyzer.js +15 -1
  11. package/dist/agents/static/structure-scanner.js +56 -57
  12. package/dist/agents/static/taxonomy.d.ts +19 -0
  13. package/dist/agents/static/taxonomy.js +147 -0
  14. package/dist/agents/tools/local-fs.js +5 -2
  15. package/dist/cli/analyze.js +49 -27
  16. package/dist/cli/license-gate.js +47 -19
  17. package/dist/cli/run.js +117 -1
  18. package/dist/cli/setup.d.ts +6 -1
  19. package/dist/cli/setup.js +35 -16
  20. package/dist/cli/shared.d.ts +0 -11
  21. package/dist/cli/shared.js +0 -61
  22. package/dist/cli/workflow.js +8 -15
  23. package/dist/server/src/index.js +276 -168
  24. package/package.json +2 -2
  25. package/templates/archbyte.yaml +28 -7
  26. package/ui/dist/assets/index-BQouokNH.css +1 -0
  27. package/ui/dist/assets/index-QllGSFhe.js +72 -0
  28. package/ui/dist/index.html +2 -2
  29. package/dist/cli/arch-diff.d.ts +0 -38
  30. package/dist/cli/arch-diff.js +0 -61
  31. package/dist/cli/diff.d.ts +0 -10
  32. package/dist/cli/diff.js +0 -144
  33. package/dist/cli/patrol.d.ts +0 -18
  34. package/dist/cli/patrol.js +0 -596
  35. package/dist/cli/validate.d.ts +0 -53
  36. package/dist/cli/validate.js +0 -299
  37. package/ui/dist/assets/index-DDCNauh7.css +0 -1
  38. 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, 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);
@@ -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 analysis = buildAnalysisFromStatic(result, rootDir);
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
- 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");
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
@@ -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
- const creds = loadCredentials();
20
- // Not logged in
20
+ let creds = loadCredentials();
21
+ // Not logged in — offer interactive login
21
22
  if (!creds) {
22
- console.error();
23
- console.error(chalk.red("Authentication required."));
24
- console.error();
25
- console.error(chalk.gray("Sign in or create a free account:"));
26
- console.error(chalk.gray(" archbyte login"));
27
- console.error();
28
- console.error(chalk.gray("Free tier includes unlimited scans. No credit card required."));
29
- process.exit(1);
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 (treat invalid dates as expired — fail-closed)
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.error();
35
- console.error(chalk.red("Session expired."));
36
- console.error(chalk.gray("Run `archbyte login` to refresh your session."));
37
- process.exit(1);
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.error();
52
- console.error(chalk.red("Session invalid. Please log in again."));
53
- console.error(chalk.gray(" archbyte login"));
54
- process.exit(1);
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