archbyte 0.2.9 → 0.3.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/dist/agents/index.d.ts +0 -1
- package/dist/agents/index.js +0 -1
- package/dist/agents/pipeline/agents/component-identifier.js +67 -8
- package/dist/agents/pipeline/agents/connection-mapper.js +33 -8
- package/dist/agents/pipeline/agents/flow-detector.js +61 -8
- package/dist/agents/pipeline/agents/service-describer.js +36 -8
- package/dist/agents/pipeline/agents/validator.js +38 -9
- package/dist/agents/pipeline/index.d.ts +1 -0
- package/dist/agents/pipeline/index.js +123 -7
- package/dist/agents/pipeline/types.d.ts +11 -0
- package/dist/agents/providers/openai.js +6 -6
- package/dist/agents/runtime/types.d.ts +1 -1
- package/dist/agents/runtime/types.js +2 -7
- package/dist/cli/analyze.js +73 -29
- package/dist/cli/auth.js +2 -2
- package/dist/cli/config.js +1 -1
- package/dist/cli/incremental.d.ts +21 -0
- package/dist/cli/incremental.js +117 -1
- package/dist/cli/serve.js +89 -13
- package/dist/cli/setup.js +49 -27
- package/dist/cli/yaml-io.d.ts +2 -0
- package/package.json +1 -1
- package/ui/dist/assets/{index-pYTLuCyK.js → index-Bdr9FnaA.js} +1 -1
- package/ui/dist/index.html +1 -1
- package/dist/agents/providers/ollama.d.ts +0 -9
- package/dist/agents/providers/ollama.js +0 -162
|
@@ -7,12 +7,12 @@ export class OpenAIProvider {
|
|
|
7
7
|
}
|
|
8
8
|
async chat(params) {
|
|
9
9
|
const messages = [
|
|
10
|
-
{ role: "
|
|
10
|
+
{ role: "developer", content: params.system },
|
|
11
11
|
...params.messages.map((m) => this.toOpenAIMessage(m)),
|
|
12
12
|
];
|
|
13
13
|
const response = await this.client.chat.completions.create({
|
|
14
|
-
model: params.model ?? "gpt-
|
|
15
|
-
|
|
14
|
+
model: params.model ?? "gpt-5.2",
|
|
15
|
+
max_completion_tokens: params.maxTokens ?? 8192,
|
|
16
16
|
messages,
|
|
17
17
|
...(params.tools?.length
|
|
18
18
|
? {
|
|
@@ -54,12 +54,12 @@ export class OpenAIProvider {
|
|
|
54
54
|
}
|
|
55
55
|
async *stream(params) {
|
|
56
56
|
const messages = [
|
|
57
|
-
{ role: "
|
|
57
|
+
{ role: "developer", content: params.system },
|
|
58
58
|
...params.messages.map((m) => this.toOpenAIMessage(m)),
|
|
59
59
|
];
|
|
60
60
|
const stream = await this.client.chat.completions.create({
|
|
61
|
-
model: params.model ?? "gpt-
|
|
62
|
-
|
|
61
|
+
model: params.model ?? "gpt-5.2",
|
|
62
|
+
max_completion_tokens: params.maxTokens ?? 8192,
|
|
63
63
|
messages,
|
|
64
64
|
stream: true,
|
|
65
65
|
...(params.tools?.length
|
|
@@ -97,7 +97,7 @@ export interface License {
|
|
|
97
97
|
expiresAt: string;
|
|
98
98
|
isValid: boolean;
|
|
99
99
|
}
|
|
100
|
-
export type ProviderName = "anthropic" | "openai" | "google"
|
|
100
|
+
export type ProviderName = "anthropic" | "openai" | "google";
|
|
101
101
|
export interface ProviderProfile {
|
|
102
102
|
apiKey?: string;
|
|
103
103
|
model?: string;
|
|
@@ -7,20 +7,15 @@ export const MODEL_MAP = {
|
|
|
7
7
|
advanced: "claude-opus-4-6",
|
|
8
8
|
},
|
|
9
9
|
openai: {
|
|
10
|
-
fast: "gpt-5.2
|
|
10
|
+
fast: "gpt-5.2",
|
|
11
11
|
standard: "gpt-5.2",
|
|
12
|
-
advanced: "
|
|
12
|
+
advanced: "gpt-5.2",
|
|
13
13
|
},
|
|
14
14
|
google: {
|
|
15
15
|
fast: "gemini-2.5-flash",
|
|
16
16
|
standard: "gemini-2.5-pro",
|
|
17
17
|
advanced: "gemini-2.5-pro",
|
|
18
18
|
},
|
|
19
|
-
ollama: {
|
|
20
|
-
fast: "qwen2.5-coder",
|
|
21
|
-
standard: "qwen2.5-coder",
|
|
22
|
-
advanced: "qwen2.5-coder",
|
|
23
|
-
},
|
|
24
19
|
};
|
|
25
20
|
export function resolveModel(provider, tier, overrides, model) {
|
|
26
21
|
if (overrides?.[tier])
|
package/dist/cli/analyze.js
CHANGED
|
@@ -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();
|
|
@@ -93,7 +93,7 @@ export async function handleAnalyze(options) {
|
|
|
93
93
|
console.error(chalk.gray(" archbyte analyze --static"));
|
|
94
94
|
console.error();
|
|
95
95
|
console.error(chalk.bold("Supported providers:"));
|
|
96
|
-
console.error(chalk.gray(" anthropic, openai, google
|
|
96
|
+
console.error(chalk.gray(" anthropic, openai, google"));
|
|
97
97
|
process.exit(1);
|
|
98
98
|
}
|
|
99
99
|
console.log(chalk.gray(`Provider: ${chalk.white(config.provider)}`));
|
|
@@ -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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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) {
|
|
@@ -317,11 +362,10 @@ function printSummary(analysis, durationMs, mode) {
|
|
|
317
362
|
console.log(chalk.gray(` Components: ${components.length}`));
|
|
318
363
|
console.log(chalk.gray(` Connections: ${connections.length}`));
|
|
319
364
|
console.log();
|
|
320
|
-
console.log(chalk.bold("Next
|
|
321
|
-
console.log(
|
|
322
|
-
console.log(
|
|
323
|
-
console.log(
|
|
324
|
-
console.log(chalk.gray(` ${chalk.cyan("archbyte patrol")} -- continuous architecture monitoring ${chalk.yellow("[Pro]")}`));
|
|
365
|
+
console.log(chalk.bold("Next"));
|
|
366
|
+
console.log(` ${chalk.cyan("archbyte serve")} Open the interactive diagram`);
|
|
367
|
+
console.log(` ${chalk.cyan("archbyte validate")} Check architecture fitness rules ${chalk.yellow("[Pro]")}`);
|
|
368
|
+
console.log(` ${chalk.cyan("archbyte patrol")} Continuous architecture monitoring ${chalk.yellow("[Pro]")}`);
|
|
325
369
|
console.log();
|
|
326
370
|
}
|
|
327
371
|
// ─── Analysis converters ───
|
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("
|
|
50
|
-
console.log(chalk.gray("
|
|
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"}`));
|
package/dist/cli/config.js
CHANGED
|
@@ -184,7 +184,7 @@ function maskKey(key) {
|
|
|
184
184
|
export function resolveConfig() {
|
|
185
185
|
const config = loadConfig();
|
|
186
186
|
const provider = process.env.ARCHBYTE_PROVIDER ?? config.provider;
|
|
187
|
-
// Reject unknown providers
|
|
187
|
+
// Reject unknown providers
|
|
188
188
|
if (provider && !VALID_PROVIDERS.includes(provider)) {
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
@@ -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;
|
package/dist/cli/incremental.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
const tryPort = port + attempt;
|
|
75
|
+
// Try to start, handle port conflicts interactively
|
|
76
|
+
while (true) {
|
|
40
77
|
try {
|
|
41
|
-
|
|
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
|
|
84
|
+
port,
|
|
51
85
|
});
|
|
52
86
|
return;
|
|
53
87
|
}
|
|
54
88
|
catch (error) {
|
|
55
89
|
if (error instanceof Error && error.code === "EADDRINUSE") {
|
|
56
|
-
|
|
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
|
}
|