@praneeth_54/agentdoctor 2.0.1 → 3.0.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/CHANGELOG.md +123 -1
- package/README.md +510 -260
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +246 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +28 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/loop.d.ts +53 -0
- package/dist/agent/loop.js +253 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +83 -0
- package/dist/agent/runtime.js +293 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +20 -0
- package/dist/agent/tools/execute.js +365 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +69 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +9 -0
- package/dist/ai/redact.js +61 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +29 -0
- package/dist/cli/commands/agent.js +164 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +153 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +233 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +333 -43
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/intelligence/graph/build.js +36 -8
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +91 -26
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +320 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +24 -0
- package/dist/product/decisions/ledger.js +110 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +42 -0
- package/dist/product/discovery/roots.js +215 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +193 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +69 -0
- package/dist/product/index.js +35 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +100 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/security/paths.js +43 -15
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
4
|
+
function npmPackageNameFromKey(key) {
|
|
5
|
+
if (!key || key === "")
|
|
6
|
+
return null;
|
|
7
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
8
|
+
const tail = idx >= 0 ? key.slice(idx + "node_modules/".length) : key;
|
|
9
|
+
if (!tail || (tail.startsWith("@") === false && tail.includes("@"))) {
|
|
10
|
+
const at = tail.indexOf("@");
|
|
11
|
+
if (at > 0)
|
|
12
|
+
return tail.slice(0, at);
|
|
13
|
+
}
|
|
14
|
+
return tail.split("/").filter(Boolean).pop() ?? null;
|
|
15
|
+
}
|
|
16
|
+
export function parsePackageLockJson(content, lockfilePath) {
|
|
17
|
+
const limitations = [];
|
|
18
|
+
let data;
|
|
19
|
+
try {
|
|
20
|
+
data = JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {
|
|
24
|
+
lockfilePath,
|
|
25
|
+
format: "npm",
|
|
26
|
+
packages: [],
|
|
27
|
+
limitations: ["package-lock.json is not valid JSON"],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const obj = data;
|
|
31
|
+
const packages = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
if (obj.packages && typeof obj.packages === "object") {
|
|
34
|
+
for (const [key, entry] of Object.entries(obj.packages)) {
|
|
35
|
+
const version = entry?.version?.trim();
|
|
36
|
+
if (!version)
|
|
37
|
+
continue;
|
|
38
|
+
const name = entry.name?.trim() ||
|
|
39
|
+
npmPackageNameFromKey(key) ||
|
|
40
|
+
(key === "" ? null : key.replace(/^node_modules\//, ""));
|
|
41
|
+
if (!name)
|
|
42
|
+
continue;
|
|
43
|
+
const dedupe = `${name}@${version}`;
|
|
44
|
+
if (seen.has(dedupe))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(dedupe);
|
|
47
|
+
packages.push({
|
|
48
|
+
name,
|
|
49
|
+
version,
|
|
50
|
+
lockfile: lockfilePath,
|
|
51
|
+
truth: "VERIFIED",
|
|
52
|
+
evidence: [{ path: lockfilePath, excerpt: `packages["${key}"].version=${version}` }],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (obj.dependencies && typeof obj.dependencies === "object") {
|
|
57
|
+
limitations.push("npm lockfile v1 dependencies tree — shallow direct entries only");
|
|
58
|
+
for (const [name, entry] of Object.entries(obj.dependencies)) {
|
|
59
|
+
const version = entry?.version?.trim();
|
|
60
|
+
if (!version)
|
|
61
|
+
continue;
|
|
62
|
+
packages.push({
|
|
63
|
+
name,
|
|
64
|
+
version,
|
|
65
|
+
lockfile: lockfilePath,
|
|
66
|
+
truth: "VERIFIED",
|
|
67
|
+
evidence: [{ path: lockfilePath, excerpt: `dependencies.${name}=${version}` }],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
limitations.push("Unrecognized package-lock.json shape");
|
|
73
|
+
}
|
|
74
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
75
|
+
return { lockfilePath, format: "npm", packages, limitations };
|
|
76
|
+
}
|
|
77
|
+
export function parseYarnLock(content, lockfilePath) {
|
|
78
|
+
const packages = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
let currentKey = null;
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
const header = /^([^\s].+):$/.exec(line);
|
|
85
|
+
if (header) {
|
|
86
|
+
currentKey = header[1].trim();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (currentKey && /^\s+version\s+/.test(line)) {
|
|
90
|
+
const verMatch = /version\s+"([^"]+)"/.exec(line);
|
|
91
|
+
if (!verMatch)
|
|
92
|
+
continue;
|
|
93
|
+
const version = verMatch[1];
|
|
94
|
+
const resolvedName = (() => {
|
|
95
|
+
const first = currentKey.split(",")[0].trim();
|
|
96
|
+
const at = first.lastIndexOf("@");
|
|
97
|
+
if (at <= 0)
|
|
98
|
+
return first;
|
|
99
|
+
return first.slice(0, at);
|
|
100
|
+
})();
|
|
101
|
+
const dedupe = `${resolvedName}@${version}`;
|
|
102
|
+
if (seen.has(dedupe))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(dedupe);
|
|
105
|
+
packages.push({
|
|
106
|
+
name: resolvedName,
|
|
107
|
+
version,
|
|
108
|
+
lockfile: lockfilePath,
|
|
109
|
+
truth: "VERIFIED",
|
|
110
|
+
evidence: [{ path: lockfilePath, line: i + 1, excerpt: line.trim().slice(0, 120) }],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (line.trim() === "" && currentKey) {
|
|
114
|
+
currentKey = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
118
|
+
return {
|
|
119
|
+
lockfilePath,
|
|
120
|
+
format: "yarn",
|
|
121
|
+
packages,
|
|
122
|
+
limitations: ["yarn.lock parsing uses version blocks — workspace aliases may be incomplete"],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function parsePnpmLockYaml(content, lockfilePath) {
|
|
126
|
+
const packages = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const limitations = [
|
|
129
|
+
"pnpm-lock.yaml parsing is heuristic (packages: keys) — not a full YAML AST",
|
|
130
|
+
];
|
|
131
|
+
const pkgSection = /(?:^|\n)packages:\s*\n([\s\S]*?)(?:\n\S|\n*$)/.exec(content);
|
|
132
|
+
const body = pkgSection?.[1] ?? content;
|
|
133
|
+
for (const line of body.split(/\r?\n/)) {
|
|
134
|
+
const keyMatch = /^\s{2}(['"]?)(\/[^'":]+@[^'":]+)\1:\s*$/.exec(line);
|
|
135
|
+
if (!keyMatch)
|
|
136
|
+
continue;
|
|
137
|
+
const key = keyMatch[2];
|
|
138
|
+
const at = key.lastIndexOf("@");
|
|
139
|
+
if (at <= 1)
|
|
140
|
+
continue;
|
|
141
|
+
const name = key.slice(1, at);
|
|
142
|
+
const version = key.slice(at + 1);
|
|
143
|
+
const dedupe = `${name}@${version}`;
|
|
144
|
+
if (seen.has(dedupe))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(dedupe);
|
|
147
|
+
packages.push({
|
|
148
|
+
name,
|
|
149
|
+
version,
|
|
150
|
+
lockfile: lockfilePath,
|
|
151
|
+
truth: "VERIFIED",
|
|
152
|
+
evidence: [{ path: lockfilePath, excerpt: key.slice(0, 120) }],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
156
|
+
return { lockfilePath, format: "pnpm", packages, limitations };
|
|
157
|
+
}
|
|
158
|
+
export async function parseLockfileAtPath(root, relativePath, maxBytes) {
|
|
159
|
+
const abs = path.join(root, relativePath);
|
|
160
|
+
const base = path.basename(relativePath).toLowerCase();
|
|
161
|
+
const text = await readTextFile(abs, maxBytes);
|
|
162
|
+
if (text === null)
|
|
163
|
+
return null;
|
|
164
|
+
if (base === "package-lock.json" || base === "npm-shrinkwrap.json") {
|
|
165
|
+
return parsePackageLockJson(text, relativePath);
|
|
166
|
+
}
|
|
167
|
+
if (base === "yarn.lock") {
|
|
168
|
+
return parseYarnLock(text, relativePath);
|
|
169
|
+
}
|
|
170
|
+
if (base === "pnpm-lock.yaml") {
|
|
171
|
+
return parsePnpmLockYaml(text, relativePath);
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
export async function parseAllLockfiles(root, lockfilePaths, maxBytes) {
|
|
176
|
+
const all = [];
|
|
177
|
+
const parsedLockfiles = [];
|
|
178
|
+
const limitations = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const rel of lockfilePaths) {
|
|
181
|
+
const parsed = await parseLockfileAtPath(root, rel, maxBytes);
|
|
182
|
+
if (!parsed || parsed.packages.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
parsedLockfiles.push(rel);
|
|
185
|
+
limitations.push(...parsed.limitations);
|
|
186
|
+
for (const p of parsed.packages) {
|
|
187
|
+
const key = `${p.name}@${p.version}::${p.lockfile}`;
|
|
188
|
+
if (seen.has(key))
|
|
189
|
+
continue;
|
|
190
|
+
seen.add(key);
|
|
191
|
+
all.push(p);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
195
|
+
return { packages: all, parsedLockfiles, limitations };
|
|
196
|
+
}
|
|
197
|
+
/** Test helper: read lockfile from disk */
|
|
198
|
+
export async function readLockfileFile(filePath) {
|
|
199
|
+
return fs.readFile(filePath, "utf8");
|
|
200
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TruthLabel } from "../truth.js";
|
|
2
|
+
export interface ProjectCandidate {
|
|
3
|
+
root: string;
|
|
4
|
+
score: number;
|
|
5
|
+
markers: string[];
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectDiscoveryReport {
|
|
10
|
+
cwd: string;
|
|
11
|
+
home: string;
|
|
12
|
+
candidates: ProjectCandidate[];
|
|
13
|
+
selected: ProjectCandidate | null;
|
|
14
|
+
blocked: boolean;
|
|
15
|
+
blockReason: string | null;
|
|
16
|
+
estimatedEntries: number;
|
|
17
|
+
limitations: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface DiscoverProjectOptions {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Maximum directory entries to count before aborting deep scans. */
|
|
22
|
+
maxEntries?: number;
|
|
23
|
+
/** Prefer this path if it is a valid project. */
|
|
24
|
+
prefer?: string;
|
|
25
|
+
/** Auto-select highest score when exactly one strong candidate. */
|
|
26
|
+
autoSelect?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
30
|
+
* or unbounded parent trees.
|
|
31
|
+
*/
|
|
32
|
+
export declare function discoverProjectRoots(options?: DiscoverProjectOptions): Promise<ProjectDiscoveryReport>;
|
|
33
|
+
export declare function resolveStartedProjectRoot(pathArg: string | undefined, options?: {
|
|
34
|
+
maxEntries?: number;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
ok: true;
|
|
37
|
+
root: string;
|
|
38
|
+
discovery: ProjectDiscoveryReport;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
discovery: ProjectDiscoveryReport;
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_IGNORE_DIRECTORIES } from "../../constants.js";
|
|
5
|
+
import { isDirectory, pathExists } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
/** Markers that strongly suggest a software project root. */
|
|
8
|
+
const PROJECT_MARKERS = [
|
|
9
|
+
"package.json",
|
|
10
|
+
"composer.json",
|
|
11
|
+
"pyproject.toml",
|
|
12
|
+
"Cargo.toml",
|
|
13
|
+
"go.mod",
|
|
14
|
+
"pom.xml",
|
|
15
|
+
"build.gradle",
|
|
16
|
+
"build.gradle.kts",
|
|
17
|
+
"Gemfile",
|
|
18
|
+
"mix.exs",
|
|
19
|
+
"pubspec.yaml",
|
|
20
|
+
".git",
|
|
21
|
+
];
|
|
22
|
+
const SUSPICIOUS_DIR_NAMES = new Set([
|
|
23
|
+
"desktop",
|
|
24
|
+
"downloads",
|
|
25
|
+
"documents",
|
|
26
|
+
"pictures",
|
|
27
|
+
"movies",
|
|
28
|
+
"music",
|
|
29
|
+
"library",
|
|
30
|
+
"applications",
|
|
31
|
+
]);
|
|
32
|
+
function normalizeHome(home) {
|
|
33
|
+
return path.resolve(home);
|
|
34
|
+
}
|
|
35
|
+
function isUnderHomeSubtree(abs, home, leaf) {
|
|
36
|
+
const target = path.resolve(home, leaf);
|
|
37
|
+
const resolved = path.resolve(abs);
|
|
38
|
+
return resolved === target || resolved.startsWith(target + path.sep);
|
|
39
|
+
}
|
|
40
|
+
async function countEntriesShallow(dir, budget) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
const queue = [dir];
|
|
43
|
+
const skip = DEFAULT_IGNORE_DIRECTORIES;
|
|
44
|
+
while (queue.length > 0 && count < budget) {
|
|
45
|
+
const current = queue.pop();
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
count += 1;
|
|
55
|
+
if (count >= budget)
|
|
56
|
+
return count;
|
|
57
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
58
|
+
continue;
|
|
59
|
+
if (skip.has(entry.name) || entry.name.startsWith("."))
|
|
60
|
+
continue;
|
|
61
|
+
queue.push(path.join(current, entry.name));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
async function markersAt(dir) {
|
|
67
|
+
const found = [];
|
|
68
|
+
for (const marker of PROJECT_MARKERS) {
|
|
69
|
+
if (await pathExists(path.join(dir, marker))) {
|
|
70
|
+
found.push(marker);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
function scoreMarkers(markers) {
|
|
76
|
+
let score = 0;
|
|
77
|
+
for (const m of markers) {
|
|
78
|
+
if (m === ".git")
|
|
79
|
+
score += 3;
|
|
80
|
+
else if (m === "package.json" || m === "composer.json" || m === "pyproject.toml")
|
|
81
|
+
score += 5;
|
|
82
|
+
else
|
|
83
|
+
score += 4;
|
|
84
|
+
}
|
|
85
|
+
return score;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
89
|
+
* or unbounded parent trees.
|
|
90
|
+
*/
|
|
91
|
+
export async function discoverProjectRoots(options = {}) {
|
|
92
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
93
|
+
const home = normalizeHome(os.homedir());
|
|
94
|
+
const maxEntries = options.maxEntries ?? 25_000;
|
|
95
|
+
const limitations = [
|
|
96
|
+
"Discovery uses shallow marker probes and bounded entry counts — not a full repository index.",
|
|
97
|
+
"Candidates are VERIFIED only when marker files/directories exist on disk.",
|
|
98
|
+
];
|
|
99
|
+
const baselower = path.basename(cwd).toLowerCase();
|
|
100
|
+
if (cwd === home ||
|
|
101
|
+
SUSPICIOUS_DIR_NAMES.has(baselower) ||
|
|
102
|
+
isUnderHomeSubtree(cwd, home, "Desktop") ||
|
|
103
|
+
isUnderHomeSubtree(cwd, home, "Downloads") ||
|
|
104
|
+
isUnderHomeSubtree(cwd, home, "Documents")) {
|
|
105
|
+
const estimatedEntries = await countEntriesShallow(cwd, Math.min(maxEntries, 5_000));
|
|
106
|
+
return {
|
|
107
|
+
cwd,
|
|
108
|
+
home,
|
|
109
|
+
candidates: [],
|
|
110
|
+
selected: null,
|
|
111
|
+
blocked: true,
|
|
112
|
+
blockReason: "Refusing to scan home / Desktop / Downloads / Documents (or similarly broad user folders) as a project root. Navigate into a project directory or pass an explicit project path.",
|
|
113
|
+
estimatedEntries,
|
|
114
|
+
limitations,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const estimatedEntries = await countEntriesShallow(cwd, maxEntries + 1);
|
|
118
|
+
if (estimatedEntries > maxEntries) {
|
|
119
|
+
return {
|
|
120
|
+
cwd,
|
|
121
|
+
home,
|
|
122
|
+
candidates: [],
|
|
123
|
+
selected: null,
|
|
124
|
+
blocked: true,
|
|
125
|
+
blockReason: `Directory tree exceeds safe scan budget (${maxEntries} entries). Narrow to a project subdirectory or raise --max-entries deliberately.`,
|
|
126
|
+
estimatedEntries,
|
|
127
|
+
limitations,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const candidates = [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
async function consider(dir, reason) {
|
|
133
|
+
const resolved = path.resolve(dir);
|
|
134
|
+
if (seen.has(resolved))
|
|
135
|
+
return;
|
|
136
|
+
if (!(await isDirectory(resolved)))
|
|
137
|
+
return;
|
|
138
|
+
if (resolved === home)
|
|
139
|
+
return;
|
|
140
|
+
const markers = await markersAt(resolved);
|
|
141
|
+
if (markers.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
seen.add(resolved);
|
|
144
|
+
candidates.push({
|
|
145
|
+
root: resolved,
|
|
146
|
+
score: scoreMarkers(markers),
|
|
147
|
+
markers,
|
|
148
|
+
truth: "VERIFIED",
|
|
149
|
+
reason,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (options.prefer) {
|
|
153
|
+
await consider(options.prefer, "explicit prefer path");
|
|
154
|
+
}
|
|
155
|
+
await consider(cwd, "current working directory");
|
|
156
|
+
// Walk up a few parents looking for markers (bounded).
|
|
157
|
+
let parent = path.dirname(cwd);
|
|
158
|
+
for (let i = 0; i < 4; i++) {
|
|
159
|
+
if (parent === home || parent === path.dirname(parent))
|
|
160
|
+
break;
|
|
161
|
+
await consider(parent, "parent directory with project markers");
|
|
162
|
+
parent = path.dirname(parent);
|
|
163
|
+
}
|
|
164
|
+
// Immediate children that look like projects (monorepo packages / sibling apps).
|
|
165
|
+
try {
|
|
166
|
+
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
169
|
+
continue;
|
|
170
|
+
if (DEFAULT_IGNORE_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
|
|
171
|
+
continue;
|
|
172
|
+
await consider(path.join(cwd, entry.name), "child directory with project markers");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
limitations.push("Could not list child directories for candidate discovery.");
|
|
177
|
+
}
|
|
178
|
+
candidates.sort((a, b) => b.score - a.score || a.root.localeCompare(b.root));
|
|
179
|
+
let selected = null;
|
|
180
|
+
if (options.prefer) {
|
|
181
|
+
selected = candidates.find((c) => c.root === path.resolve(options.prefer)) ?? null;
|
|
182
|
+
}
|
|
183
|
+
else if (options.autoSelect !== false) {
|
|
184
|
+
const strong = candidates.filter((c) => c.score >= 5);
|
|
185
|
+
if (strong.length === 1) {
|
|
186
|
+
selected = strong[0];
|
|
187
|
+
}
|
|
188
|
+
else if (candidates.length === 1) {
|
|
189
|
+
selected = candidates[0];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
cwd,
|
|
194
|
+
home,
|
|
195
|
+
candidates,
|
|
196
|
+
selected,
|
|
197
|
+
blocked: false,
|
|
198
|
+
blockReason: null,
|
|
199
|
+
estimatedEntries,
|
|
200
|
+
limitations,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export async function resolveStartedProjectRoot(pathArg, options) {
|
|
204
|
+
const prefer = pathArg ? resolveRepoRoot(pathArg) : undefined;
|
|
205
|
+
const discovery = await discoverProjectRoots({
|
|
206
|
+
cwd: prefer ?? process.cwd(),
|
|
207
|
+
...(prefer !== undefined ? { prefer } : {}),
|
|
208
|
+
...(options?.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
|
|
209
|
+
autoSelect: true,
|
|
210
|
+
});
|
|
211
|
+
if (discovery.blocked || !discovery.selected) {
|
|
212
|
+
return { ok: false, discovery };
|
|
213
|
+
}
|
|
214
|
+
return { ok: true, root: discovery.selected.root, discovery };
|
|
215
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TruthLabel } from "../truth.js";
|
|
2
|
+
export interface ProjectDna {
|
|
3
|
+
fingerprint: string;
|
|
4
|
+
name: string;
|
|
5
|
+
root: string;
|
|
6
|
+
projectType: string;
|
|
7
|
+
languages: string[];
|
|
8
|
+
frameworks: string[];
|
|
9
|
+
frontend: string[];
|
|
10
|
+
backend: string[];
|
|
11
|
+
database: string[];
|
|
12
|
+
packageManagers: string[];
|
|
13
|
+
buildSystems: string[];
|
|
14
|
+
testFrameworks: string[];
|
|
15
|
+
infrastructure: string[];
|
|
16
|
+
cloudIndicators: string[];
|
|
17
|
+
git: {
|
|
18
|
+
present: boolean;
|
|
19
|
+
truth: TruthLabel;
|
|
20
|
+
};
|
|
21
|
+
monorepo: {
|
|
22
|
+
isMonorepo: boolean;
|
|
23
|
+
tool: string;
|
|
24
|
+
packages: Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}>;
|
|
28
|
+
truth: TruthLabel;
|
|
29
|
+
};
|
|
30
|
+
services: string[];
|
|
31
|
+
packages: string[];
|
|
32
|
+
applications: string[];
|
|
33
|
+
externalIntegrations: string[];
|
|
34
|
+
aiCodingTools: string[];
|
|
35
|
+
counts: {
|
|
36
|
+
files: number;
|
|
37
|
+
sourceFiles: number;
|
|
38
|
+
testFiles: number;
|
|
39
|
+
configFiles: number;
|
|
40
|
+
};
|
|
41
|
+
configurationStructure: string[];
|
|
42
|
+
truth: TruthLabel;
|
|
43
|
+
limitations: string[];
|
|
44
|
+
generatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic Project DNA from repository detectors + file inventory.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildProjectDna(rootInput: string): Promise<ProjectDna>;
|
|
50
|
+
export declare function persistProjectDna(rootInput: string, dna?: ProjectDna): Promise<ProjectDna>;
|