archbyte 0.2.8 → 0.3.0

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.
@@ -7,9 +7,9 @@ export const MODEL_MAP = {
7
7
  advanced: "claude-opus-4-6",
8
8
  },
9
9
  openai: {
10
- fast: "gpt-5.2-codex",
10
+ fast: "gpt-5.2",
11
11
  standard: "gpt-5.2",
12
- advanced: "o3",
12
+ advanced: "gpt-5.2",
13
13
  },
14
14
  google: {
15
15
  fast: "gemini-2.5-flash",
@@ -5,7 +5,7 @@ import chalk from "chalk";
5
5
  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
- import { getChangedFiles, mapFilesToComponents, shouldRunAgents, isGitAvailable } from "./incremental.js";
8
+ import { getChangedFiles, mapFilesToComponents, shouldRunAgents, isGitAvailable, categorizeChanges, computeNeighbors, getCommitCount } from "./incremental.js";
9
9
  import { progressBar } from "./ui.js";
10
10
  export async function handleAnalyze(options) {
11
11
  const rootDir = options.dir ? path.resolve(options.dir) : process.cwd();
@@ -115,30 +115,63 @@ export async function handleAnalyze(options) {
115
115
  const priorSpec = loadSpec(rootDir);
116
116
  const priorMeta = loadMetadata(rootDir);
117
117
  if (priorSpec && !options.force && isGitAvailable(rootDir) && priorMeta?.lastCommit) {
118
- const changedFiles = getChangedFiles(rootDir, priorMeta.lastCommit);
119
- if (changedFiles.length === 0) {
120
- console.log(chalk.green("No changes detected since last scan. Use --force to re-scan."));
118
+ // Staleness detection: force full scan after 20+ commits
119
+ const commitsSinceScan = getCommitCount(rootDir, priorMeta.lastCommit);
120
+ if (commitsSinceScan >= 20) {
121
+ console.log(chalk.yellow(`${commitsSinceScan} commits since last scan — forcing full re-scan for accuracy.`));
121
122
  console.log();
122
- return;
123
123
  }
124
- const { affected, unmapped } = mapFilesToComponents(changedFiles, priorSpec);
125
- if (!shouldRunAgents(affected, unmapped)) {
126
- console.log(chalk.green("Only config changes detected. No re-scan needed. Use --force to re-scan."));
124
+ else {
125
+ const changedFiles = getChangedFiles(rootDir, priorMeta.lastCommit);
126
+ if (changedFiles.length === 0) {
127
+ console.log(chalk.green("No changes detected since last scan. Use --force to re-scan."));
128
+ console.log();
129
+ return;
130
+ }
131
+ const { affected, unmapped } = mapFilesToComponents(changedFiles, priorSpec);
132
+ if (!shouldRunAgents(affected, unmapped)) {
133
+ console.log(chalk.green("Only config changes detected. No re-scan needed. Use --force to re-scan."));
134
+ console.log();
135
+ return;
136
+ }
137
+ const categories = categorizeChanges(changedFiles);
138
+ const neighborComponents = computeNeighbors(affected, priorSpec);
139
+ incrementalContext = {
140
+ existingSpec: priorSpec,
141
+ changedFiles,
142
+ affectedComponents: [...affected],
143
+ categories,
144
+ neighborComponents,
145
+ hasUnmappedFiles: unmapped.length > 0,
146
+ };
147
+ console.log(chalk.bold("Incremental scan:"));
148
+ console.log(chalk.gray(` Changed files: ${changedFiles.length}`));
149
+ console.log(chalk.gray(` Affected components: ${affected.size > 0 ? [...affected].join(", ") : "none (new files)"}`));
150
+ if (neighborComponents.length > 0) {
151
+ console.log(chalk.gray(` Neighbor components: ${neighborComponents.join(", ")}`));
152
+ }
153
+ if (unmapped.length > 0) {
154
+ console.log(chalk.gray(` Unmapped files: ${unmapped.length} (potential new components — all agents will run)`));
155
+ }
156
+ // Summarize categorized changes
157
+ const catSummary = [];
158
+ if (categories.entryPoints.length)
159
+ catSummary.push(`${categories.entryPoints.length} entry`);
160
+ if (categories.routeFiles.length)
161
+ catSummary.push(`${categories.routeFiles.length} route`);
162
+ if (categories.infraFiles.length)
163
+ catSummary.push(`${categories.infraFiles.length} infra`);
164
+ if (categories.configFiles.length)
165
+ catSummary.push(`${categories.configFiles.length} config`);
166
+ if (categories.eventFiles.length)
167
+ catSummary.push(`${categories.eventFiles.length} event`);
168
+ if (categories.sourceFiles.length)
169
+ catSummary.push(`${categories.sourceFiles.length} source`);
170
+ if (catSummary.length > 0) {
171
+ console.log(chalk.gray(` Categories: ${catSummary.join(", ")}`));
172
+ }
127
173
  console.log();
128
- return;
129
174
  }
130
- incrementalContext = {
131
- existingSpec: priorSpec,
132
- changedFiles,
133
- affectedComponents: [...affected],
134
- };
135
- console.log(chalk.bold("Incremental scan:"));
136
- console.log(chalk.gray(` Changed files: ${changedFiles.length}`));
137
- console.log(chalk.gray(` Affected components: ${affected.size > 0 ? [...affected].join(", ") : "none (new files)"}`));
138
- if (unmapped.length > 0) {
139
- console.log(chalk.gray(` Unmapped files: ${unmapped.length} (potential new components)`));
140
- }
141
- console.log();
142
175
  }
143
176
  // 4. Run static context collection → LLM pipeline
144
177
  const progress = progressBar(7);
@@ -235,13 +268,21 @@ export async function handleAnalyze(options) {
235
268
  if (result.tokenUsage) {
236
269
  meta.tokenUsage = { input: result.tokenUsage.input, output: result.tokenUsage.output };
237
270
  }
271
+ if (incrementalContext) {
272
+ meta.incrementalMode = true;
273
+ meta.changedFiles = incrementalContext.changedFiles.length;
274
+ meta.affectedComponents = incrementalContext.affectedComponents;
275
+ }
276
+ if (result.skippedAgents && result.skippedAgents.length > 0) {
277
+ meta.skippedAgents = result.skippedAgents;
278
+ }
238
279
  writeAnalysis(rootDir, analysis);
239
280
  writeAnalysisStatus(rootDir, "success");
240
281
  // Dual-write: archbyte.yaml + metadata.json
241
282
  const existingSpec = loadSpec(rootDir);
242
283
  const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
243
284
  writeSpec(rootDir, spec);
244
- writeScanMetadata(rootDir, duration, "pipeline", ctx.fileTree.totalFiles, result.tokenUsage);
285
+ writeScanMetadata(rootDir, duration, "pipeline", ctx.fileTree.totalFiles, result.tokenUsage, incrementalContext ? true : undefined, result.skippedAgents);
245
286
  progress.update(6, "Generating diagram...");
246
287
  await autoGenerate(rootDir, options);
247
288
  progress.done("Analysis complete");
@@ -283,7 +324,7 @@ function getGitCommit() {
283
324
  return undefined;
284
325
  }
285
326
  }
286
- function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage) {
327
+ function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage, incrementalMode, skippedAgents) {
287
328
  const meta = {
288
329
  analyzedAt: new Date().toISOString(),
289
330
  durationMs,
@@ -294,6 +335,10 @@ function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage)
294
335
  meta.filesScanned = filesScanned;
295
336
  if (tokenUsage)
296
337
  meta.tokenUsage = tokenUsage;
338
+ if (incrementalMode)
339
+ meta.incrementalMode = true;
340
+ if (skippedAgents && skippedAgents.length > 0)
341
+ meta.skippedAgents = skippedAgents;
297
342
  writeMetadata(rootDir, meta);
298
343
  }
299
344
  async function autoGenerate(rootDir, options) {
package/dist/cli/auth.js CHANGED
@@ -46,8 +46,8 @@ export async function handleLogin(provider) {
46
46
  console.log(chalk.green(`Logged in as ${chalk.bold(credentials.email)} (${credentials.tier} tier)`));
47
47
  console.log(chalk.gray(`Credentials saved to ${CREDENTIALS_PATH}`));
48
48
  console.log();
49
- console.log(chalk.gray("Your credentials and API keys are stored locally on your machine."));
50
- console.log(chalk.gray("ArchByte never transmits or stores your model provider keys."));
49
+ console.log(chalk.gray("Login credentials are stored in ~/.archbyte/ (global) never inside your project."));
50
+ console.log(chalk.gray("Your API keys and credentials never leave this machine."));
51
51
  }
52
52
  catch (err) {
53
53
  console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
@@ -21,3 +21,24 @@ export declare function shouldRunAgents(affected: Set<string>, unmapped: string[
21
21
  * Check if git is available and this is a git repo.
22
22
  */
23
23
  export declare function isGitAvailable(rootDir: string): boolean;
24
+ /**
25
+ * Categorize changed files by domain relevance for agent filtering.
26
+ */
27
+ export interface CategorizedChanges {
28
+ entryPoints: string[];
29
+ routeFiles: string[];
30
+ configFiles: string[];
31
+ infraFiles: string[];
32
+ eventFiles: string[];
33
+ sourceFiles: string[];
34
+ }
35
+ export declare function categorizeChanges(changedFiles: string[]): CategorizedChanges;
36
+ /**
37
+ * Find 1-layer neighbor components: components connected to affected components.
38
+ * Ensures cross-component connections aren't missed during incremental scans.
39
+ */
40
+ export declare function computeNeighbors(affectedComponents: Set<string>, spec: ArchbyteSpec): string[];
41
+ /**
42
+ * Count commits since a given commit hash. Used for staleness detection.
43
+ */
44
+ export declare function getCommitCount(rootDir: string, sinceCommit: string): number;
@@ -9,7 +9,7 @@ export function getChangedFiles(rootDir, sinceCommit) {
9
9
  try {
10
10
  const files = [];
11
11
  if (sinceCommit) {
12
- // Changed files since last scan commit
12
+ // Changed files since last scan commit (committed changes)
13
13
  const diff = execSync(`git diff --name-only ${sinceCommit}..HEAD`, {
14
14
  cwd: rootDir,
15
15
  encoding: "utf-8",
@@ -19,6 +19,30 @@ export function getChangedFiles(rootDir, sinceCommit) {
19
19
  files.push(...diff.split("\n").filter(Boolean));
20
20
  }
21
21
  }
22
+ // Include unstaged modifications to tracked files
23
+ const modified = execSync("git diff --name-only", {
24
+ cwd: rootDir,
25
+ encoding: "utf-8",
26
+ stdio: ["pipe", "pipe", "pipe"],
27
+ }).trim();
28
+ if (modified) {
29
+ for (const f of modified.split("\n").filter(Boolean)) {
30
+ if (!files.includes(f))
31
+ files.push(f);
32
+ }
33
+ }
34
+ // Include staged but uncommitted changes
35
+ const staged = execSync("git diff --name-only --cached", {
36
+ cwd: rootDir,
37
+ encoding: "utf-8",
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ }).trim();
40
+ if (staged) {
41
+ for (const f of staged.split("\n").filter(Boolean)) {
42
+ if (!files.includes(f))
43
+ files.push(f);
44
+ }
45
+ }
22
46
  // Also include untracked files
23
47
  const untracked = execSync("git ls-files --others --exclude-standard", {
24
48
  cwd: rootDir,
@@ -100,6 +124,72 @@ export function isGitAvailable(rootDir) {
100
124
  return false;
101
125
  }
102
126
  }
127
+ export function categorizeChanges(changedFiles) {
128
+ const categories = {
129
+ entryPoints: [],
130
+ routeFiles: [],
131
+ configFiles: [],
132
+ infraFiles: [],
133
+ eventFiles: [],
134
+ sourceFiles: [],
135
+ };
136
+ for (const file of changedFiles) {
137
+ const name = file.split("/").pop() ?? "";
138
+ const lower = file.toLowerCase();
139
+ if (isInfraFile(lower, name)) {
140
+ categories.infraFiles.push(file);
141
+ }
142
+ else if (isConfigFile(name)) {
143
+ categories.configFiles.push(file);
144
+ }
145
+ else if (isEntryPoint(name)) {
146
+ categories.entryPoints.push(file);
147
+ }
148
+ else if (isRouteFile(lower, name)) {
149
+ categories.routeFiles.push(file);
150
+ }
151
+ else if (isEventFile(lower, name)) {
152
+ categories.eventFiles.push(file);
153
+ }
154
+ else if (isSourceFile(name)) {
155
+ categories.sourceFiles.push(file);
156
+ }
157
+ // else: non-code file (docs, images, etc.) — ignored
158
+ }
159
+ return categories;
160
+ }
161
+ /**
162
+ * Find 1-layer neighbor components: components connected to affected components.
163
+ * Ensures cross-component connections aren't missed during incremental scans.
164
+ */
165
+ export function computeNeighbors(affectedComponents, spec) {
166
+ const neighbors = new Set();
167
+ for (const conn of spec.connections) {
168
+ if (affectedComponents.has(conn.from) && !affectedComponents.has(conn.to)) {
169
+ neighbors.add(conn.to);
170
+ }
171
+ if (affectedComponents.has(conn.to) && !affectedComponents.has(conn.from)) {
172
+ neighbors.add(conn.from);
173
+ }
174
+ }
175
+ return [...neighbors];
176
+ }
177
+ /**
178
+ * Count commits since a given commit hash. Used for staleness detection.
179
+ */
180
+ export function getCommitCount(rootDir, sinceCommit) {
181
+ try {
182
+ const output = execSync(`git rev-list --count ${sinceCommit}..HEAD`, {
183
+ cwd: rootDir,
184
+ encoding: "utf-8",
185
+ stdio: ["pipe", "pipe", "pipe"],
186
+ }).trim();
187
+ return parseInt(output, 10) || 0;
188
+ }
189
+ catch {
190
+ return 0;
191
+ }
192
+ }
103
193
  // Helpers
104
194
  function normalizePath(p) {
105
195
  // Remove trailing slashes, normalize
@@ -109,3 +199,29 @@ function isConfigOnly(filePath) {
109
199
  const name = filePath.split("/").pop() ?? "";
110
200
  return /^(\.gitignore|\.eslintrc|\.prettierrc|tsconfig|package\.json|package-lock|yarn\.lock|pnpm-lock|archbyte\.yaml|README|LICENSE|CHANGELOG|\.env)/i.test(name);
111
201
  }
202
+ function isInfraFile(lower, name) {
203
+ return /dockerfile|docker-compose|compose\.ya?ml|\.dockerignore/i.test(name) ||
204
+ lower.includes("k8s/") || lower.includes("kubernetes/") ||
205
+ lower.includes("terraform/") || lower.includes("helm/") ||
206
+ /\.(tf|hcl)$/.test(name);
207
+ }
208
+ function isConfigFile(name) {
209
+ return /^(package\.json|tsconfig|\.env|\.eslintrc|\.prettierrc|pyproject\.toml|Cargo\.toml|go\.mod|archbyte\.yaml)/i.test(name);
210
+ }
211
+ function isEntryPoint(name) {
212
+ return /^(index|main|app|server|entry)\.(ts|js|tsx|jsx|py|go|rs)$/i.test(name);
213
+ }
214
+ function isRouteFile(lower, name) {
215
+ return lower.includes("route") || lower.includes("router") ||
216
+ lower.includes("controller") || lower.includes("handler") ||
217
+ /\.(routes|controller|handler)\.(ts|js)$/i.test(name);
218
+ }
219
+ function isEventFile(lower, name) {
220
+ return lower.includes("event") || lower.includes("listener") ||
221
+ lower.includes("subscriber") || lower.includes("consumer") ||
222
+ lower.includes("producer") || lower.includes("queue") ||
223
+ /\.(events?|listener|subscriber|consumer|producer)\.(ts|js)$/i.test(name);
224
+ }
225
+ function isSourceFile(name) {
226
+ return /\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i.test(name);
227
+ }
package/dist/cli/serve.js CHANGED
@@ -2,12 +2,51 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import chalk from "chalk";
4
4
  import { DEFAULT_PORT } from "./constants.js";
5
+ import { select } from "./ui.js";
6
+ function askPort(prompt) {
7
+ return new Promise((resolve) => {
8
+ process.stdout.write(prompt);
9
+ const stdin = process.stdin;
10
+ const wasRaw = stdin.isRaw;
11
+ stdin.setRawMode(true);
12
+ stdin.resume();
13
+ stdin.setEncoding("utf8");
14
+ let input = "";
15
+ const onData = (data) => {
16
+ for (const ch of data) {
17
+ if (ch === "\n" || ch === "\r" || ch === "\u0004") {
18
+ stdin.setRawMode(wasRaw ?? false);
19
+ stdin.pause();
20
+ stdin.removeListener("data", onData);
21
+ process.stdout.write("\n");
22
+ resolve(input);
23
+ return;
24
+ }
25
+ else if (ch === "\u0003") {
26
+ process.stdout.write("\n");
27
+ process.exit(0);
28
+ }
29
+ else if (ch === "\u007F" || ch === "\b") {
30
+ if (input.length > 0) {
31
+ input = input.slice(0, -1);
32
+ process.stdout.write("\b \b");
33
+ }
34
+ }
35
+ else if (ch >= "0" && ch <= "9") {
36
+ input += ch;
37
+ process.stdout.write(ch);
38
+ }
39
+ }
40
+ };
41
+ stdin.on("data", onData);
42
+ });
43
+ }
5
44
  /**
6
45
  * Start the ArchByte UI server
7
46
  */
8
47
  export async function handleServe(options) {
9
48
  const rootDir = process.cwd();
10
- const port = options.port || DEFAULT_PORT;
49
+ let port = options.port || DEFAULT_PORT;
11
50
  const diagramPath = options.diagram
12
51
  ? path.resolve(rootDir, options.diagram)
13
52
  : path.join(rootDir, ".archbyte", "architecture.json");
@@ -25,7 +64,6 @@ export async function handleServe(options) {
25
64
  }
26
65
  console.log(chalk.cyan(`🏗️ ArchByte - See what agents build. As they build it.`));
27
66
  console.log(chalk.gray(`Diagram: ${path.relative(rootDir, diagramPath)}`));
28
- console.log(chalk.gray(`Port: ${port}`));
29
67
  console.log();
30
68
  // Check if diagram exists
31
69
  if (!fs.existsSync(diagramPath)) {
@@ -34,26 +72,66 @@ export async function handleServe(options) {
34
72
  console.log();
35
73
  }
36
74
  const { startServer } = await import("../server/src/index.js");
37
- const maxRetries = 10;
38
- for (let attempt = 0; attempt < maxRetries; attempt++) {
39
- const tryPort = port + attempt;
75
+ // Try to start, handle port conflicts interactively
76
+ while (true) {
40
77
  try {
41
- if (attempt > 0) {
42
- console.log(chalk.gray(`Port ${tryPort - 1} in use, trying ${tryPort}...`));
43
- }
44
- console.log(chalk.green(`UI: http://localhost:${tryPort}`));
78
+ console.log(chalk.green(`UI: http://localhost:${port}`));
45
79
  console.log();
46
80
  await startServer({
47
81
  name: projectName,
48
82
  diagramPath,
49
83
  workspaceRoot: rootDir,
50
- port: tryPort,
84
+ port,
51
85
  });
52
86
  return;
53
87
  }
54
88
  catch (error) {
55
89
  if (error instanceof Error && error.code === "EADDRINUSE") {
56
- continue;
90
+ console.log(chalk.yellow(` Port ${port} is already in use.`));
91
+ console.log();
92
+ if (!process.stdin.isTTY) {
93
+ // Non-interactive — just try next port
94
+ port++;
95
+ continue;
96
+ }
97
+ const nextPort = port + 1;
98
+ const choice = await select(" What would you like to do?", [
99
+ `Use next available port (${nextPort})`,
100
+ "Enter a custom port",
101
+ "Kill the process on this port and retry",
102
+ "Exit",
103
+ ]);
104
+ if (choice === 0) {
105
+ port = nextPort;
106
+ continue;
107
+ }
108
+ else if (choice === 1) {
109
+ const input = await askPort(chalk.bold(" Port: "));
110
+ const parsed = parseInt(input, 10);
111
+ if (!parsed || parsed < 1 || parsed > 65535) {
112
+ console.log(chalk.red(" Invalid port number."));
113
+ continue;
114
+ }
115
+ port = parsed;
116
+ continue;
117
+ }
118
+ else if (choice === 2) {
119
+ try {
120
+ const { execSync } = await import("child_process");
121
+ execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: "ignore" });
122
+ console.log(chalk.green(` Killed process on port ${port}.`));
123
+ // Small delay for OS to release the port
124
+ await new Promise((r) => setTimeout(r, 500));
125
+ continue;
126
+ }
127
+ catch {
128
+ console.log(chalk.red(` Could not kill process on port ${port}. Try manually: lsof -ti:${port} | xargs kill -9`));
129
+ continue;
130
+ }
131
+ }
132
+ else {
133
+ process.exit(0);
134
+ }
57
135
  }
58
136
  if (error instanceof Error) {
59
137
  console.error(chalk.red(`Error: ${error.message}`));
@@ -61,6 +139,4 @@ export async function handleServe(options) {
61
139
  process.exit(1);
62
140
  }
63
141
  }
64
- console.error(chalk.red(`All ports ${port}-${port + maxRetries - 1} are in use.`));
65
- process.exit(1);
66
142
  }