archbyte 0.2.9 → 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.
- 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.js +2 -2
- package/dist/cli/analyze.js +68 -23
- package/dist/cli/auth.js +2 -2
- 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 +39 -15
- package/dist/cli/yaml-io.d.ts +2 -0
- package/package.json +1 -1
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
import { extractJSON } from "../response-parser.js";
|
|
4
4
|
import { getPrompt } from "../../prompts.js";
|
|
5
5
|
import "../../prompt-data.js";
|
|
6
|
-
function formatFileTree(ctx) {
|
|
6
|
+
function formatFileTree(ctx, affectedPaths) {
|
|
7
7
|
// Show directories + key files only (not all 1000+ files) to stay within context limits
|
|
8
8
|
const lines = [];
|
|
9
|
-
const MAX_LINES = 300;
|
|
9
|
+
const MAX_LINES = affectedPaths ? 150 : 300;
|
|
10
|
+
function isAffectedSubtree(dirPath) {
|
|
11
|
+
if (!affectedPaths)
|
|
12
|
+
return true;
|
|
13
|
+
for (const p of affectedPaths) {
|
|
14
|
+
if (p.startsWith(dirPath + "/") || dirPath.startsWith(p + "/") || dirPath === p)
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
10
19
|
function walk(entries, indent) {
|
|
11
20
|
for (const e of entries) {
|
|
12
21
|
if (lines.length >= MAX_LINES)
|
|
@@ -14,6 +23,11 @@ function formatFileTree(ctx) {
|
|
|
14
23
|
if (e.type === "directory") {
|
|
15
24
|
const fileCount = e.children?.filter((c) => c.type === "file").length ?? 0;
|
|
16
25
|
const dirCount = e.children?.filter((c) => c.type === "directory").length ?? 0;
|
|
26
|
+
if (affectedPaths && indent >= 1 && !isAffectedSubtree(e.path)) {
|
|
27
|
+
// Collapse unchanged directories
|
|
28
|
+
lines.push(`${" ".repeat(indent)}${e.path}/ ... (${fileCount} files, unchanged)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
17
31
|
lines.push(`${" ".repeat(indent)}${e.path}/ (${fileCount} files, ${dirCount} dirs)`);
|
|
18
32
|
if (e.children)
|
|
19
33
|
walk(e.children, indent + 1);
|
|
@@ -48,8 +62,49 @@ export const componentIdentifier = {
|
|
|
48
62
|
buildPrompt(ctx, priorResults) {
|
|
49
63
|
const system = getPrompt("pipeline/component-identifier");
|
|
50
64
|
const incremental = priorResults?.["__incremental__"];
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
// Build set of affected paths for tree filtering
|
|
66
|
+
let affectedPaths;
|
|
67
|
+
if (incremental) {
|
|
68
|
+
affectedPaths = new Set();
|
|
69
|
+
// Include paths of affected components
|
|
70
|
+
for (const compId of incremental.affectedComponents) {
|
|
71
|
+
const comp = incremental.existingSpec.components[compId];
|
|
72
|
+
if (comp?.path)
|
|
73
|
+
affectedPaths.add(comp.path);
|
|
74
|
+
}
|
|
75
|
+
// Include paths of neighbor components
|
|
76
|
+
for (const compId of incremental.neighborComponents) {
|
|
77
|
+
const comp = incremental.existingSpec.components[compId];
|
|
78
|
+
if (comp?.path)
|
|
79
|
+
affectedPaths.add(comp.path);
|
|
80
|
+
}
|
|
81
|
+
// Include directories of changed files
|
|
82
|
+
for (const f of incremental.changedFiles) {
|
|
83
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
84
|
+
affectedPaths.add(dir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const fileTree = formatFileTree(ctx, affectedPaths);
|
|
88
|
+
// Filter configs: in incremental mode, only include configs from affected dirs + root configs
|
|
89
|
+
let configs;
|
|
90
|
+
if (incremental && affectedPaths) {
|
|
91
|
+
const filteredConfigs = ctx.codeSamples.configFiles.filter((cf) => {
|
|
92
|
+
const dir = cf.path.includes("/") ? cf.path.substring(0, cf.path.lastIndexOf("/")) : ".";
|
|
93
|
+
// Always include root configs
|
|
94
|
+
if (!cf.path.includes("/"))
|
|
95
|
+
return true;
|
|
96
|
+
// Include configs from affected directories
|
|
97
|
+
for (const p of affectedPaths) {
|
|
98
|
+
if (dir.startsWith(p) || p.startsWith(dir))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
configs = filteredConfigs.map((cf) => `--- ${cf.path} ---\n${cf.content}`).join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
configs = formatConfigs(ctx);
|
|
107
|
+
}
|
|
53
108
|
const structureInfo = [
|
|
54
109
|
`Project: ${ctx.structure.projectName || "(unknown)"}`,
|
|
55
110
|
`Language: ${ctx.structure.language}`,
|
|
@@ -62,9 +117,13 @@ export const componentIdentifier = {
|
|
|
62
117
|
const docNotes = ctx.docs.architectureNotes.length > 0
|
|
63
118
|
? `\n\nArchitecture notes from docs:\n${ctx.docs.architectureNotes.join("\n")}`
|
|
64
119
|
: "";
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
120
|
+
// In incremental mode, only include Docker info if infra files changed
|
|
121
|
+
let dockerInfo = "";
|
|
122
|
+
if (ctx.infra.docker.composeFile) {
|
|
123
|
+
if (!incremental || incremental.categories.infraFiles.length > 0) {
|
|
124
|
+
dockerInfo = `\n\nDocker Compose services:\n${ctx.infra.docker.services.map((s) => `- ${s.name}${s.image ? ` (image: ${s.image})` : ""}${s.buildContext ? ` (build: ${s.buildContext})` : ""}${s.dependsOn?.length ? ` depends_on: ${s.dependsOn.join(", ")}` : ""}`).join("\n")}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
68
127
|
let incrementalInfo = "";
|
|
69
128
|
if (incremental) {
|
|
70
129
|
const existing = Object.entries(incremental.existingSpec.components)
|
|
@@ -76,7 +135,7 @@ ${existing}
|
|
|
76
135
|
Changed files: ${incremental.changedFiles.join(", ")}
|
|
77
136
|
Affected components: ${incremental.affectedComponents.length > 0 ? incremental.affectedComponents.join(", ") : "none (check for new components)"}
|
|
78
137
|
|
|
79
|
-
IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones.
|
|
138
|
+
IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones. Focus: re-evaluate components in [${[...new Set([...incremental.affectedComponents, ...incremental.neighborComponents])].join(", ")}]. Return other components as-is.`;
|
|
80
139
|
}
|
|
81
140
|
const user = `Analyze this project and identify ALL architecturally significant components.
|
|
82
141
|
|
|
@@ -47,10 +47,36 @@ export const connectionMapper = {
|
|
|
47
47
|
parts.push(`- ${ar.method} ${ar.path} (${ar.handlerFile})`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
// Import map
|
|
51
|
-
const
|
|
50
|
+
// Import map — filter to affected + neighbor component dirs in incremental mode
|
|
51
|
+
const incremental = priorResults?.["__incremental__"];
|
|
52
|
+
let affectedDirs;
|
|
53
|
+
if (incremental) {
|
|
54
|
+
affectedDirs = new Set();
|
|
55
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
56
|
+
const comp = incremental.existingSpec.components[compId];
|
|
57
|
+
if (comp?.path)
|
|
58
|
+
affectedDirs.add(comp.path);
|
|
59
|
+
}
|
|
60
|
+
for (const f of incremental.changedFiles) {
|
|
61
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
62
|
+
affectedDirs.add(dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let importEntries = Object.entries(ctx.codeSamples.importMap);
|
|
66
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
67
|
+
importEntries = importEntries.filter(([file]) => {
|
|
68
|
+
const dir = file.includes("/") ? file.substring(0, file.lastIndexOf("/")) : ".";
|
|
69
|
+
for (const ad of affectedDirs) {
|
|
70
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
52
76
|
if (importEntries.length > 0) {
|
|
53
|
-
|
|
77
|
+
const totalEntries = Object.keys(ctx.codeSamples.importMap).length;
|
|
78
|
+
const label = affectedDirs ? `${importEntries.length} of ${totalEntries} files, filtered to affected dirs` : `${totalEntries} files`;
|
|
79
|
+
parts.push(`\n## Import Graph (${label})`);
|
|
54
80
|
// Summarize by directory to keep it concise
|
|
55
81
|
const dirImports = {};
|
|
56
82
|
for (const [file, imports] of importEntries) {
|
|
@@ -69,8 +95,8 @@ export const connectionMapper = {
|
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
|
-
// Docker depends_on
|
|
73
|
-
if (ctx.infra.docker.composeFile) {
|
|
98
|
+
// Docker depends_on — only if infra changed (or full scan)
|
|
99
|
+
if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.infraFiles.length > 0)) {
|
|
74
100
|
const deps = ctx.infra.docker.services
|
|
75
101
|
.filter((s) => s.dependsOn?.length)
|
|
76
102
|
.map((s) => `${s.name} → ${s.dependsOn.join(", ")}`);
|
|
@@ -78,15 +104,14 @@ export const connectionMapper = {
|
|
|
78
104
|
parts.push(`\n## Docker depends_on\n${deps.join("\n")}`);
|
|
79
105
|
}
|
|
80
106
|
}
|
|
81
|
-
// Kubernetes resources
|
|
82
|
-
if (ctx.infra.kubernetes.resources.length > 0) {
|
|
107
|
+
// Kubernetes resources — only if infra changed (or full scan)
|
|
108
|
+
if (ctx.infra.kubernetes.resources.length > 0 && (!incremental || incremental.categories.infraFiles.length > 0)) {
|
|
83
109
|
parts.push("\n## Kubernetes Resources");
|
|
84
110
|
for (const r of ctx.infra.kubernetes.resources) {
|
|
85
111
|
parts.push(`- ${r.kind}: ${r.name}${r.namespace ? ` (ns=${r.namespace})` : ""}`);
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
// Incremental context
|
|
89
|
-
const incremental = priorResults?.["__incremental__"];
|
|
90
115
|
if (incremental) {
|
|
91
116
|
const existingConns = incremental.existingSpec.connections
|
|
92
117
|
.map((c) => `- ${c.from} → ${c.to} (${c.type}${c.async ? ", async" : ""}): ${c.description}`)
|
|
@@ -11,6 +11,20 @@ export const flowDetector = {
|
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/flow-detector");
|
|
13
13
|
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build set of affected directories for filtering
|
|
15
|
+
let affectedDirs;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
affectedDirs = new Set();
|
|
18
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
19
|
+
const comp = incremental.existingSpec.components[compId];
|
|
20
|
+
if (comp?.path)
|
|
21
|
+
affectedDirs.add(comp.path);
|
|
22
|
+
}
|
|
23
|
+
for (const f of incremental.changedFiles) {
|
|
24
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
25
|
+
affectedDirs.add(dir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
const parts = [];
|
|
15
29
|
// Event patterns from static scanner
|
|
16
30
|
if (ctx.events.hasEDA) {
|
|
@@ -18,8 +32,23 @@ export const flowDetector = {
|
|
|
18
32
|
for (const p of ctx.events.patterns) {
|
|
19
33
|
parts.push(` Technology: ${p.technology} (dep: ${p.dependency})`);
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
// Filter events to affected directories in incremental mode
|
|
36
|
+
let events = ctx.events.events;
|
|
37
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
38
|
+
events = events.filter((e) => {
|
|
39
|
+
const dir = e.file.includes("/") ? e.file.substring(0, e.file.lastIndexOf("/")) : ".";
|
|
40
|
+
for (const ad of affectedDirs) {
|
|
41
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
parts.push(`\nEvent occurrences in affected files (${events.length} of ${ctx.events.events.length} total):`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
parts.push("\nEvent occurrences in code:");
|
|
50
|
+
}
|
|
51
|
+
for (const e of events.slice(0, 30)) {
|
|
23
52
|
parts.push(` [${e.type}] ${e.file}: ${e.pattern}`);
|
|
24
53
|
}
|
|
25
54
|
}
|
|
@@ -33,24 +62,48 @@ export const flowDetector = {
|
|
|
33
62
|
parts.push(` ${ep.method} ${ep.path} — ${ep.description}`);
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
|
-
// Route file samples
|
|
37
|
-
|
|
65
|
+
// Route file samples — filter to affected directories in incremental mode
|
|
66
|
+
let routeSamples = ctx.codeSamples.samples.filter((s) => s.category === "route-file");
|
|
67
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
68
|
+
const filtered = routeSamples.filter((s) => {
|
|
69
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
70
|
+
for (const ad of affectedDirs) {
|
|
71
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
if (filtered.length > 0)
|
|
77
|
+
routeSamples = filtered;
|
|
78
|
+
}
|
|
38
79
|
if (routeSamples.length > 0) {
|
|
39
80
|
parts.push("\nRoute file excerpts:");
|
|
40
81
|
for (const s of routeSamples.slice(0, 5)) {
|
|
41
82
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
42
83
|
}
|
|
43
84
|
}
|
|
44
|
-
// Entry points
|
|
45
|
-
|
|
85
|
+
// Entry points — filter to affected directories in incremental mode
|
|
86
|
+
let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
|
|
87
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
88
|
+
const filtered = entrySamples.filter((s) => {
|
|
89
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
90
|
+
for (const ad of affectedDirs) {
|
|
91
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
if (filtered.length > 0)
|
|
97
|
+
entrySamples = filtered;
|
|
98
|
+
}
|
|
46
99
|
if (entrySamples.length > 0) {
|
|
47
100
|
parts.push("\nEntry point excerpts:");
|
|
48
101
|
for (const s of entrySamples.slice(0, 3)) {
|
|
49
102
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
50
103
|
}
|
|
51
104
|
}
|
|
52
|
-
// Docker info for queue/broker identification
|
|
53
|
-
if (ctx.infra.docker.composeFile) {
|
|
105
|
+
// Docker info for queue/broker identification — only if event-related files changed
|
|
106
|
+
if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.eventFiles.length > 0 || incremental.categories.infraFiles.length > 0)) {
|
|
54
107
|
const queueServices = ctx.infra.docker.services.filter((s) => {
|
|
55
108
|
const img = (s.image ?? "").toLowerCase();
|
|
56
109
|
return img.includes("rabbit") || img.includes("kafka") || img.includes("redis") ||
|
|
@@ -11,6 +11,22 @@ export const serviceDescriber = {
|
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/service-describer");
|
|
13
13
|
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build affected dirs for filtering
|
|
15
|
+
let affectedDirs;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
affectedDirs = new Set();
|
|
18
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
19
|
+
const comp = incremental.existingSpec.components[compId];
|
|
20
|
+
if (comp?.path)
|
|
21
|
+
affectedDirs.add(comp.path);
|
|
22
|
+
}
|
|
23
|
+
for (const f of incremental.changedFiles) {
|
|
24
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
25
|
+
affectedDirs.add(dir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const hasInfraChanges = incremental ? incremental.categories.infraFiles.length > 0 : true;
|
|
29
|
+
const hasConfigChanges = incremental ? incremental.categories.configFiles.length > 0 : true;
|
|
14
30
|
const parts = [];
|
|
15
31
|
// Structure info
|
|
16
32
|
parts.push(`Project: ${ctx.structure.projectName || "(unknown)"}`);
|
|
@@ -24,8 +40,8 @@ export const serviceDescriber = {
|
|
|
24
40
|
if (ctx.docs.externalDependencies.length > 0) {
|
|
25
41
|
parts.push(`\nExternal dependencies mentioned: ${ctx.docs.externalDependencies.join(", ")}`);
|
|
26
42
|
}
|
|
27
|
-
// Docker services
|
|
28
|
-
if (ctx.infra.docker.composeFile) {
|
|
43
|
+
// Docker services — only include if infra/config files changed (or full scan)
|
|
44
|
+
if (ctx.infra.docker.composeFile && (hasInfraChanges || hasConfigChanges)) {
|
|
29
45
|
const svcInfo = ctx.infra.docker.services.map((s) => {
|
|
30
46
|
const details = [s.name];
|
|
31
47
|
if (s.image)
|
|
@@ -40,22 +56,34 @@ export const serviceDescriber = {
|
|
|
40
56
|
});
|
|
41
57
|
parts.push(`\nDocker services:\n${svcInfo.join("\n")}`);
|
|
42
58
|
}
|
|
43
|
-
// Env vars
|
|
44
|
-
if (ctx.envs.environments.length > 0) {
|
|
59
|
+
// Env vars — only include if infra/config files changed (or full scan)
|
|
60
|
+
if (ctx.envs.environments.length > 0 && (hasInfraChanges || hasConfigChanges)) {
|
|
45
61
|
for (const env of ctx.envs.environments) {
|
|
46
62
|
parts.push(`\nEnv file "${env.name}" variables: ${env.variables.join(", ")}`);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
|
-
// Code samples (entry points)
|
|
50
|
-
|
|
65
|
+
// Code samples (entry points) — filter to affected dirs in incremental mode
|
|
66
|
+
let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
|
|
67
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
68
|
+
const filtered = entrySamples.filter((s) => {
|
|
69
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
70
|
+
for (const ad of affectedDirs) {
|
|
71
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
if (filtered.length > 0)
|
|
77
|
+
entrySamples = filtered;
|
|
78
|
+
}
|
|
51
79
|
if (entrySamples.length > 0) {
|
|
52
80
|
parts.push("\nEntry point excerpts:");
|
|
53
81
|
for (const s of entrySamples.slice(0, 5)) {
|
|
54
82
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
|
-
// Cloud info
|
|
58
|
-
if (ctx.infra.cloud.provider) {
|
|
85
|
+
// Cloud info — only if infra changed (or full scan)
|
|
86
|
+
if (ctx.infra.cloud.provider && hasInfraChanges) {
|
|
59
87
|
parts.push(`\nCloud: ${ctx.infra.cloud.provider}, services: ${ctx.infra.cloud.services.join(", ")}`);
|
|
60
88
|
}
|
|
61
89
|
// Incremental context
|
|
@@ -10,15 +10,36 @@ export const validator = {
|
|
|
10
10
|
phase: "sequential",
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/validator");
|
|
13
|
+
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build set of relevant component IDs for filtering
|
|
15
|
+
let relevantComponents;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
relevantComponents = new Set([...incremental.affectedComponents, ...incremental.neighborComponents]);
|
|
18
|
+
}
|
|
13
19
|
const parts = [];
|
|
14
|
-
// Components
|
|
20
|
+
// Components — in incremental mode, show affected + neighbors in full, others as summary
|
|
15
21
|
const components = priorResults?.["component-identifier"];
|
|
16
22
|
if (components?.components) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
parts.push(
|
|
21
|
-
|
|
23
|
+
if (relevantComponents && relevantComponents.size > 0) {
|
|
24
|
+
const relevant = components.components.filter((c) => relevantComponents.has(c.id));
|
|
25
|
+
const others = components.components.length - relevant.length;
|
|
26
|
+
parts.push("## Components (affected + neighbors)");
|
|
27
|
+
for (const c of relevant) {
|
|
28
|
+
parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
|
|
29
|
+
parts.push(` description: ${c.description || "(none)"}`);
|
|
30
|
+
parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
|
|
31
|
+
}
|
|
32
|
+
if (others > 0) {
|
|
33
|
+
parts.push(`\n(${others} other unchanged components omitted)`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
parts.push("## Components");
|
|
38
|
+
for (const c of components.components) {
|
|
39
|
+
parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
|
|
40
|
+
parts.push(` description: ${c.description || "(none)"}`);
|
|
41
|
+
parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
44
|
}
|
|
24
45
|
// Services
|
|
@@ -35,11 +56,19 @@ export const validator = {
|
|
|
35
56
|
parts.push(`- ${svc.id}: ${svc.name} (${svc.type}) — ${svc.description}`);
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
|
-
// Connections
|
|
59
|
+
// Connections — in incremental mode, only show connections involving affected/neighbor components
|
|
39
60
|
const connections = priorResults?.["connection-mapper"];
|
|
40
61
|
if (connections?.connections) {
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
let conns = connections.connections;
|
|
63
|
+
if (relevantComponents && relevantComponents.size > 0) {
|
|
64
|
+
conns = conns.filter((c) => relevantComponents.has(c.from) || relevantComponents.has(c.to));
|
|
65
|
+
const omitted = connections.connections.length - conns.length;
|
|
66
|
+
parts.push(`\n## Connections (involving affected components${omitted > 0 ? `, ${omitted} others omitted` : ""})`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parts.push("\n## Connections");
|
|
70
|
+
}
|
|
71
|
+
for (const c of conns) {
|
|
43
72
|
parts.push(`- ${c.from} → ${c.to} (${c.type}) ${c.async ? "[async]" : ""}: ${c.description}`);
|
|
44
73
|
}
|
|
45
74
|
}
|
|
@@ -15,6 +15,79 @@ function isAuthError(err) {
|
|
|
15
15
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16
16
|
return /401|authentication_error|invalid.*api.?key|unauthorized|invalid.*key/i.test(msg);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Determine if an agent can be skipped based on what changed.
|
|
20
|
+
* Returns a skip reason string if skippable, or null if the agent must run.
|
|
21
|
+
*/
|
|
22
|
+
function shouldSkipAgent(agentId, inc) {
|
|
23
|
+
// Never skip if there are unmapped files — potential new components
|
|
24
|
+
if (inc.hasUnmappedFiles)
|
|
25
|
+
return null;
|
|
26
|
+
const cat = inc.categories;
|
|
27
|
+
const hasInfra = cat.infraFiles.length > 0;
|
|
28
|
+
const hasConfig = cat.configFiles.length > 0;
|
|
29
|
+
const hasEntryPoints = cat.entryPoints.length > 0;
|
|
30
|
+
const hasRoutes = cat.routeFiles.length > 0;
|
|
31
|
+
const hasEvents = cat.eventFiles.length > 0;
|
|
32
|
+
switch (agentId) {
|
|
33
|
+
case "service-describer":
|
|
34
|
+
// Skip when no infra/config/entry-point changes
|
|
35
|
+
if (!hasInfra && !hasConfig && !hasEntryPoints) {
|
|
36
|
+
return "no infra/config/entry-point changes";
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
case "flow-detector":
|
|
40
|
+
// Skip when no route/event/entry-point changes
|
|
41
|
+
if (!hasRoutes && !hasEvents && !hasEntryPoints) {
|
|
42
|
+
return "no route/event/entry-point changes";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
case "validator":
|
|
46
|
+
// Skip when ≤1 affected component and no infra changes
|
|
47
|
+
if (inc.affectedComponents.length <= 1 && !hasInfra) {
|
|
48
|
+
return `only ${inc.affectedComponents.length} component affected, no infra changes`;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
// component-identifier and connection-mapper never skip
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Build fallback data for a skipped agent from existing spec */
|
|
57
|
+
function getFallbackData(agentId, inc) {
|
|
58
|
+
switch (agentId) {
|
|
59
|
+
case "service-describer": {
|
|
60
|
+
const dbs = Object.entries(inc.existingSpec.databases).map(([id, d]) => ({
|
|
61
|
+
id, name: d.name, type: d.type, description: d.description, usedBy: [],
|
|
62
|
+
}));
|
|
63
|
+
const exts = Object.entries(inc.existingSpec["external-services"]).map(([id, s]) => ({
|
|
64
|
+
id, name: s.name, type: s.type, description: s.description, usedBy: [],
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
projectDescription: "",
|
|
68
|
+
primaryLanguage: "",
|
|
69
|
+
databases: dbs,
|
|
70
|
+
externalServices: exts,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "flow-detector":
|
|
74
|
+
return {
|
|
75
|
+
eventConnections: [],
|
|
76
|
+
apiRoutes: [],
|
|
77
|
+
};
|
|
78
|
+
case "validator":
|
|
79
|
+
return {
|
|
80
|
+
componentTypeCorrections: {},
|
|
81
|
+
componentDescriptions: {},
|
|
82
|
+
addedConnections: [],
|
|
83
|
+
removedConnectionKeys: [],
|
|
84
|
+
confidence: 0.8,
|
|
85
|
+
issues: [],
|
|
86
|
+
};
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
18
91
|
/**
|
|
19
92
|
* Run the multi-agent pipeline: 3 parallel fast agents → 2 sequential agents.
|
|
20
93
|
* Each agent gets a single chat() call with pre-collected static context.
|
|
@@ -22,6 +95,7 @@ function isAuthError(err) {
|
|
|
22
95
|
export async function runPipeline(ctx, provider, config, onProgress, incrementalContext) {
|
|
23
96
|
const agentResults = {};
|
|
24
97
|
const agentMeta = [];
|
|
98
|
+
const skippedAgents = [];
|
|
25
99
|
// Pass incremental context to agents via priorResults
|
|
26
100
|
if (incrementalContext) {
|
|
27
101
|
agentResults["__incremental__"] = incrementalContext;
|
|
@@ -30,10 +104,27 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
30
104
|
onProgress?.(`Phase 1: Running ${PARALLEL_AGENTS.length} agents in parallel...`);
|
|
31
105
|
// Pass agentResults to parallel agents too (contains __incremental__ if set)
|
|
32
106
|
const parallelPrior = incrementalContext ? agentResults : undefined;
|
|
33
|
-
|
|
107
|
+
// Check for agent skipping
|
|
108
|
+
const parallelTasks = PARALLEL_AGENTS.map((agent) => ({
|
|
109
|
+
agent,
|
|
110
|
+
skipReason: incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null,
|
|
111
|
+
}));
|
|
112
|
+
const parallelResults = await Promise.allSettled(parallelTasks.map(({ agent, skipReason }) => {
|
|
113
|
+
if (skipReason) {
|
|
114
|
+
// Skip this agent — use fallback data
|
|
115
|
+
skippedAgents.push(agent.id);
|
|
116
|
+
onProgress?.(` ${agent.name}: skipped (${skipReason})`);
|
|
117
|
+
const fallback = getFallbackData(agent.id, incrementalContext);
|
|
118
|
+
agentResults[agent.id] = fallback;
|
|
119
|
+
return Promise.resolve(null);
|
|
120
|
+
}
|
|
121
|
+
return runAgent(agent, ctx, provider, config, parallelPrior, onProgress);
|
|
122
|
+
}));
|
|
34
123
|
let authFailed = false;
|
|
35
|
-
for (let i = 0; i <
|
|
36
|
-
const agent =
|
|
124
|
+
for (let i = 0; i < parallelTasks.length; i++) {
|
|
125
|
+
const { agent, skipReason } = parallelTasks[i];
|
|
126
|
+
if (skipReason)
|
|
127
|
+
continue; // Already handled
|
|
37
128
|
const result = parallelResults[i];
|
|
38
129
|
if (result.status === "fulfilled" && result.value) {
|
|
39
130
|
agentResults[agent.id] = result.value.data;
|
|
@@ -55,6 +146,15 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
55
146
|
// === Phase 2: Sequential agents ===
|
|
56
147
|
onProgress?.(`Phase 2: Running ${SEQUENTIAL_AGENTS.length} agents sequentially...`);
|
|
57
148
|
for (const agent of SEQUENTIAL_AGENTS) {
|
|
149
|
+
// Check for agent skipping
|
|
150
|
+
const skipReason = incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null;
|
|
151
|
+
if (skipReason) {
|
|
152
|
+
skippedAgents.push(agent.id);
|
|
153
|
+
onProgress?.(` ${agent.name}: skipped (${skipReason})`);
|
|
154
|
+
const fallback = getFallbackData(agent.id, incrementalContext);
|
|
155
|
+
agentResults[agent.id] = fallback;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
58
158
|
try {
|
|
59
159
|
const result = await runAgent(agent, ctx, provider, config, agentResults, onProgress);
|
|
60
160
|
if (result) {
|
|
@@ -82,13 +182,29 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
82
182
|
if (merged.validation.repairs.length > 0) {
|
|
83
183
|
onProgress?.(`Applied ${merged.validation.repairs.length} deterministic repair(s)`);
|
|
84
184
|
}
|
|
85
|
-
// Log token usage
|
|
185
|
+
// Log per-agent + total token usage
|
|
86
186
|
const totalInput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.input ?? 0), 0);
|
|
87
187
|
const totalOutput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.output ?? 0), 0);
|
|
88
|
-
if (totalInput > 0) {
|
|
89
|
-
|
|
188
|
+
if (totalInput > 0 || skippedAgents.length > 0) {
|
|
189
|
+
// Log to stderr so progress bar doesn't clobber it
|
|
190
|
+
console.error("\n Token usage breakdown:");
|
|
191
|
+
for (const a of agentMeta) {
|
|
192
|
+
const inp = a.tokenUsage?.input ?? 0;
|
|
193
|
+
const out = a.tokenUsage?.output ?? 0;
|
|
194
|
+
const dur = (a.duration / 1000).toFixed(1);
|
|
195
|
+
console.error(` ${a.agentId.padEnd(22)} ${String(inp).padStart(6)} in ${String(out).padStart(6)} out ${dur}s`);
|
|
196
|
+
}
|
|
197
|
+
for (const id of skippedAgents) {
|
|
198
|
+
console.error(` ${id.padEnd(22)} 0 in 0 out SKIPPED`);
|
|
199
|
+
}
|
|
200
|
+
console.error(` ${"TOTAL".padEnd(22)} ${String(totalInput).padStart(6)} in ${String(totalOutput).padStart(6)} out`);
|
|
201
|
+
if (skippedAgents.length > 0) {
|
|
202
|
+
console.error(` Skipped ${skippedAgents.length} agent(s): ${skippedAgents.join(", ")}`);
|
|
203
|
+
}
|
|
204
|
+
console.error();
|
|
205
|
+
onProgress?.(`Token usage: ${totalInput} in / ${totalOutput} out${skippedAgents.length > 0 ? ` (${skippedAgents.length} skipped)` : ""}`);
|
|
90
206
|
}
|
|
91
|
-
return { ...merged, tokenUsage: { input: totalInput, output: totalOutput } };
|
|
207
|
+
return { ...merged, tokenUsage: { input: totalInput, output: totalOutput }, skippedAgents: skippedAgents.length > 0 ? skippedAgents : undefined };
|
|
92
208
|
}
|
|
93
209
|
// Agents that produce large structured output (lists of components/connections) need more tokens
|
|
94
210
|
const MAX_TOKENS = {
|
|
@@ -82,6 +82,14 @@ export interface ValidatorOutput {
|
|
|
82
82
|
confidence: number;
|
|
83
83
|
issues: string[];
|
|
84
84
|
}
|
|
85
|
+
export interface CategorizedChanges {
|
|
86
|
+
entryPoints: string[];
|
|
87
|
+
routeFiles: string[];
|
|
88
|
+
configFiles: string[];
|
|
89
|
+
infraFiles: string[];
|
|
90
|
+
eventFiles: string[];
|
|
91
|
+
sourceFiles: string[];
|
|
92
|
+
}
|
|
85
93
|
export interface IncrementalContext {
|
|
86
94
|
existingSpec: {
|
|
87
95
|
components: Record<string, {
|
|
@@ -123,6 +131,9 @@ export interface IncrementalContext {
|
|
|
123
131
|
};
|
|
124
132
|
changedFiles: string[];
|
|
125
133
|
affectedComponents: string[];
|
|
134
|
+
categories: CategorizedChanges;
|
|
135
|
+
neighborComponents: string[];
|
|
136
|
+
hasUnmappedFiles: boolean;
|
|
126
137
|
}
|
|
127
138
|
export interface PipelineAgentResult {
|
|
128
139
|
agentId: string;
|
|
@@ -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
|
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();
|
|
@@ -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) {
|
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"}`));
|
|
@@ -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
|
}
|
package/dist/cli/setup.js
CHANGED
|
@@ -16,7 +16,7 @@ const PROVIDERS = [
|
|
|
16
16
|
];
|
|
17
17
|
const PROVIDER_MODELS = {
|
|
18
18
|
anthropic: [
|
|
19
|
-
{ id: "", label: "Default (recommended)", hint: "
|
|
19
|
+
{ id: "", label: "Default (recommended)", hint: "Opus for all agents" },
|
|
20
20
|
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
|
|
21
21
|
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", hint: "Balanced, great quality" },
|
|
22
22
|
{ id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
|
|
@@ -292,8 +292,16 @@ export async function handleSetup() {
|
|
|
292
292
|
console.log(chalk.green(` ✓ Model: ${chosen.label}`));
|
|
293
293
|
}
|
|
294
294
|
else {
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
// Save the actual default model so config is explicit
|
|
296
|
+
const defaultModels = {
|
|
297
|
+
anthropic: "claude-opus-4-6",
|
|
298
|
+
openai: "gpt-5.2",
|
|
299
|
+
google: "gemini-2.5-pro",
|
|
300
|
+
ollama: "qwen2.5-coder",
|
|
301
|
+
};
|
|
302
|
+
const defaultModel = defaultModels[provider] ?? resolveModel(provider, "standard");
|
|
303
|
+
profiles[provider].model = defaultModel;
|
|
304
|
+
console.log(chalk.green(` ✓ Model: ${defaultModel} (default)`));
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
config.profiles = profiles;
|
|
@@ -329,13 +337,11 @@ export async function handleSetup() {
|
|
|
329
337
|
delete config.ollamaBaseUrl;
|
|
330
338
|
// Save config
|
|
331
339
|
saveConfig(config);
|
|
332
|
-
console.log(chalk.gray(`\n Config saved to ${CONFIG_PATH}`));
|
|
333
|
-
console.log(chalk.gray(" Your API key is stored locally on this machine and never sent to ArchByte."));
|
|
334
|
-
console.log(chalk.gray(" All model calls go directly from your machine to your provider."));
|
|
335
340
|
// Generate archbyte.yaml in .archbyte/ if it doesn't exist
|
|
336
341
|
const projectDir = process.cwd();
|
|
337
342
|
const archbyteDir = path.join(projectDir, ".archbyte");
|
|
338
343
|
const yamlPath = path.join(archbyteDir, "archbyte.yaml");
|
|
344
|
+
let yamlCreated = false;
|
|
339
345
|
if (!fs.existsSync(yamlPath)) {
|
|
340
346
|
if (!fs.existsSync(archbyteDir)) {
|
|
341
347
|
fs.mkdirSync(archbyteDir, { recursive: true });
|
|
@@ -355,21 +361,39 @@ export async function handleSetup() {
|
|
|
355
361
|
let template = fs.readFileSync(templatePath, "utf-8");
|
|
356
362
|
template = template.replace("name: my-project", `name: ${projectName}`);
|
|
357
363
|
fs.writeFileSync(yamlPath, template, "utf-8");
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
console.log(chalk.gray(` .archbyte/archbyte.yaml already exists`));
|
|
364
|
+
yamlCreated = true;
|
|
362
365
|
}
|
|
363
366
|
// Generate README.md in .archbyte/
|
|
364
367
|
writeArchbyteReadme(archbyteDir);
|
|
368
|
+
// ─── Summary ───
|
|
369
|
+
const dim = chalk.gray;
|
|
370
|
+
const sep = dim(" ───");
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(chalk.bold.green(" ✓ Setup complete"));
|
|
373
|
+
console.log();
|
|
374
|
+
console.log(sep);
|
|
375
|
+
console.log();
|
|
376
|
+
console.log(dim(" ~/.archbyte/") + " API keys, login, config " + dim("(global)"));
|
|
377
|
+
console.log(dim(" .archbyte/") + " Analysis, diagrams " + dim("(per project)"));
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(dim(" Your keys never leave this machine."));
|
|
380
|
+
console.log(dim(" All model calls go directly to your provider."));
|
|
365
381
|
console.log();
|
|
366
|
-
console.log(
|
|
382
|
+
console.log(sep);
|
|
367
383
|
console.log();
|
|
368
|
-
|
|
369
|
-
|
|
384
|
+
if (yamlCreated) {
|
|
385
|
+
console.log(chalk.green(" + ") + "Created " + chalk.cyan(".archbyte/archbyte.yaml"));
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(dim(" .archbyte/archbyte.yaml already exists"));
|
|
389
|
+
}
|
|
390
|
+
console.log();
|
|
391
|
+
console.log(sep);
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(" " + chalk.bold("Next"));
|
|
394
|
+
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
370
395
|
if (hasClaude || hasCodex) {
|
|
371
|
-
console.log();
|
|
372
|
-
console.log(chalk.bold(" MCP: ") + chalk.cyan("archbyte mcp install") + chalk.bold(" to use ArchByte from your AI tool."));
|
|
396
|
+
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
|
|
373
397
|
}
|
|
374
398
|
console.log();
|
|
375
399
|
}
|
package/dist/cli/yaml-io.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface ScanMetadata {
|
|
|
71
71
|
};
|
|
72
72
|
validation?: unknown;
|
|
73
73
|
gaps?: unknown;
|
|
74
|
+
incrementalMode?: boolean;
|
|
75
|
+
skippedAgents?: string[];
|
|
74
76
|
}
|
|
75
77
|
export declare function loadSpec(rootDir: string): ArchbyteSpec | null;
|
|
76
78
|
export declare function writeSpec(rootDir: string, spec: ArchbyteSpec): void;
|