@netanelyasi/agent-ready 0.2.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/LICENSE +21 -0
- package/README.md +457 -0
- package/dist/analyzers/scoreReadiness.d.ts +2 -0
- package/dist/analyzers/scoreReadiness.js +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +73 -0
- package/dist/generators/generate.d.ts +2 -0
- package/dist/generators/generate.js +482 -0
- package/dist/scanner/scanProject.d.ts +2 -0
- package/dist/scanner/scanProject.js +544 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/dist/utils/fs.d.ts +12 -0
- package/dist/utils/fs.js +102 -0
- package/package.json +51 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_IGNORES, listDirSafe, pathExists, readJson, readText, rel, walkFiles } from "../utils/fs.js";
|
|
4
|
+
const LANGUAGE_EXTS = {
|
|
5
|
+
TypeScript: [".ts", ".tsx"],
|
|
6
|
+
JavaScript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
7
|
+
Python: [".py"],
|
|
8
|
+
PHP: [".php"],
|
|
9
|
+
Java: [".java"],
|
|
10
|
+
"C#": [".cs"],
|
|
11
|
+
Go: [".go"],
|
|
12
|
+
Rust: [".rs"],
|
|
13
|
+
"C/C++": [".c", ".cc", ".cpp", ".h", ".hpp"],
|
|
14
|
+
};
|
|
15
|
+
export async function scanProject(rootInput) {
|
|
16
|
+
const root = path.resolve(rootInput);
|
|
17
|
+
const files = await walkFiles(root, { maxDepth: 6, maxFiles: 5000 });
|
|
18
|
+
const relativeFiles = files.map((file) => rel(root, file));
|
|
19
|
+
const packages = await detectPackages(root, files);
|
|
20
|
+
const rootPackage = packages.find((pkg) => pkg.path === "package.json");
|
|
21
|
+
const allDeps = mergeDeps(packages);
|
|
22
|
+
const frameworks = detectFrameworks(relativeFiles, allDeps);
|
|
23
|
+
const databases = detectDatabases(relativeFiles, allDeps);
|
|
24
|
+
const deployment = await detectDeployment(root, relativeFiles, allDeps);
|
|
25
|
+
const monorepo = detectMonorepo(relativeFiles, packages, rootPackage);
|
|
26
|
+
const packageManager = detectPackageManager(root, rootPackage, relativeFiles);
|
|
27
|
+
const commands = detectCommands(packages, packageManager);
|
|
28
|
+
const languages = detectLanguages(files);
|
|
29
|
+
const importantDirs = await summarizeImportantDirs(root, relativeFiles, monorepo.detected);
|
|
30
|
+
const noisyPaths = detectNoisyPaths(relativeFiles);
|
|
31
|
+
const existingHarness = {
|
|
32
|
+
claudeMd: await harnessFileState(root, "CLAUDE.md"),
|
|
33
|
+
codemap: await harnessFileState(root, "CODEMAP.md"),
|
|
34
|
+
aiIgnore: await harnessFileState(root, ".aiignore"),
|
|
35
|
+
claudeSettings: await harnessFileState(root, path.join(".claude", "settings.json")),
|
|
36
|
+
skillsDir: await pathExists(path.join(root, ".agent-ready", "skills")) || await pathExists(path.join(root, ".claude", "skills")),
|
|
37
|
+
};
|
|
38
|
+
const codeGraph = await analyzeCodeGraph(root, files, packages);
|
|
39
|
+
return {
|
|
40
|
+
root,
|
|
41
|
+
name: rootPackage?.name ?? path.basename(root),
|
|
42
|
+
packages,
|
|
43
|
+
packageManager,
|
|
44
|
+
languages,
|
|
45
|
+
frameworks,
|
|
46
|
+
databases,
|
|
47
|
+
deployment,
|
|
48
|
+
monorepo,
|
|
49
|
+
commands,
|
|
50
|
+
importantDirs,
|
|
51
|
+
noisyPaths,
|
|
52
|
+
existingHarness,
|
|
53
|
+
codeGraph,
|
|
54
|
+
traits: {
|
|
55
|
+
hasHebrewOrRtl: await detectHebrewOrRtl(files),
|
|
56
|
+
hasDocker: relativeFiles.some((file) => /(^|\/)Dockerfile$|docker-compose\.ya?ml$/.test(file)),
|
|
57
|
+
hasGithubActions: relativeFiles.some((file) => file.startsWith(".github/workflows/")),
|
|
58
|
+
hasTests: hasCommand(commands, "test") || relativeFiles.some((file) => /(__tests__|\.test\.|\.spec\.)/.test(file)),
|
|
59
|
+
hasTypeScript: languages.includes("TypeScript"),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function detectPackages(root, files) {
|
|
64
|
+
const packageFiles = files.filter((file) => path.basename(file) === "package.json" && !file.includes(`${path.sep}node_modules${path.sep}`));
|
|
65
|
+
const packages = [];
|
|
66
|
+
for (const file of packageFiles) {
|
|
67
|
+
const json = await readJson(file);
|
|
68
|
+
if (!json)
|
|
69
|
+
continue;
|
|
70
|
+
const workspaces = Array.isArray(json.workspaces) ? json.workspaces : json.workspaces?.packages;
|
|
71
|
+
packages.push({
|
|
72
|
+
path: rel(root, file),
|
|
73
|
+
name: json.name,
|
|
74
|
+
packageManager: json.packageManager,
|
|
75
|
+
scripts: json.scripts ?? {},
|
|
76
|
+
dependencies: json.dependencies ?? {},
|
|
77
|
+
devDependencies: json.devDependencies ?? {},
|
|
78
|
+
workspaces,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return packages.sort((a, b) => a.path.localeCompare(b.path));
|
|
82
|
+
}
|
|
83
|
+
function mergeDeps(packages) {
|
|
84
|
+
return Object.assign({}, ...packages.map((pkg) => ({ ...pkg.dependencies, ...pkg.devDependencies })));
|
|
85
|
+
}
|
|
86
|
+
function detectPackageManager(root, rootPackage, files) {
|
|
87
|
+
if (rootPackage?.packageManager)
|
|
88
|
+
return rootPackage.packageManager.split("@")[0];
|
|
89
|
+
if (files.includes("pnpm-lock.yaml"))
|
|
90
|
+
return "pnpm";
|
|
91
|
+
if (files.includes("yarn.lock"))
|
|
92
|
+
return "yarn";
|
|
93
|
+
if (files.includes("bun.lockb") || files.includes("bun.lock"))
|
|
94
|
+
return "bun";
|
|
95
|
+
if (files.includes("package-lock.json"))
|
|
96
|
+
return "npm";
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
function detectFrameworks(files, deps) {
|
|
100
|
+
const found = new Set();
|
|
101
|
+
if (deps.next || files.includes("next.config.js") || files.includes("next.config.ts"))
|
|
102
|
+
found.add("Next.js");
|
|
103
|
+
if (deps.react || deps["react-dom"])
|
|
104
|
+
found.add("React");
|
|
105
|
+
if (deps.vue || deps.nuxt)
|
|
106
|
+
found.add(deps.nuxt ? "Nuxt" : "Vue");
|
|
107
|
+
if (deps.svelte || deps["@sveltejs/kit"])
|
|
108
|
+
found.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
|
|
109
|
+
if (deps.vite || files.some((file) => file.startsWith("vite.config.")))
|
|
110
|
+
found.add("Vite");
|
|
111
|
+
if (deps.express)
|
|
112
|
+
found.add("Express");
|
|
113
|
+
if (deps["@nestjs/core"])
|
|
114
|
+
found.add("NestJS");
|
|
115
|
+
if (files.includes("pyproject.toml") || files.includes("requirements.txt"))
|
|
116
|
+
found.add("Python");
|
|
117
|
+
if (files.includes("manage.py"))
|
|
118
|
+
found.add("Django");
|
|
119
|
+
if (files.includes("artisan") || files.includes("composer.json"))
|
|
120
|
+
found.add("PHP/Laravel or Composer");
|
|
121
|
+
if (files.some((file) => file.endsWith(".csproj") || file.endsWith(".sln")))
|
|
122
|
+
found.add(".NET");
|
|
123
|
+
if (files.includes("go.mod"))
|
|
124
|
+
found.add("Go Module");
|
|
125
|
+
if (files.includes("Cargo.toml"))
|
|
126
|
+
found.add("Rust/Cargo");
|
|
127
|
+
return [...found];
|
|
128
|
+
}
|
|
129
|
+
function detectDatabases(files, deps) {
|
|
130
|
+
const found = new Set();
|
|
131
|
+
if (deps["@supabase/supabase-js"] || files.some((file) => file.startsWith("supabase/")))
|
|
132
|
+
found.add("Supabase");
|
|
133
|
+
if (deps.prisma || files.some((file) => file.startsWith("prisma/")))
|
|
134
|
+
found.add("Prisma");
|
|
135
|
+
if (deps["drizzle-orm"] || files.some((file) => file.includes("drizzle")))
|
|
136
|
+
found.add("Drizzle");
|
|
137
|
+
if (deps.pg || deps.postgres)
|
|
138
|
+
found.add("PostgreSQL");
|
|
139
|
+
if (deps.mysql2 || deps.mysql)
|
|
140
|
+
found.add("MySQL");
|
|
141
|
+
if (deps.mongoose || deps.mongodb)
|
|
142
|
+
found.add("MongoDB");
|
|
143
|
+
return [...found];
|
|
144
|
+
}
|
|
145
|
+
async function detectDeployment(root, files, deps) {
|
|
146
|
+
const found = new Set();
|
|
147
|
+
if (files.includes("vercel.json") || deps.vercel)
|
|
148
|
+
found.add("Vercel");
|
|
149
|
+
if (files.some((file) => /(^|\/)Dockerfile$|docker-compose\.ya?ml$/.test(file)))
|
|
150
|
+
found.add("Docker");
|
|
151
|
+
if (files.some((file) => file.startsWith(".github/workflows/")))
|
|
152
|
+
found.add("GitHub Actions");
|
|
153
|
+
if (await pathExists(path.join(root, "netlify.toml")))
|
|
154
|
+
found.add("Netlify");
|
|
155
|
+
if (files.includes("wrangler.toml"))
|
|
156
|
+
found.add("Cloudflare Workers");
|
|
157
|
+
return [...found];
|
|
158
|
+
}
|
|
159
|
+
function detectMonorepo(files, packages, rootPackage) {
|
|
160
|
+
const tools = new Set();
|
|
161
|
+
if (files.includes("turbo.json"))
|
|
162
|
+
tools.add("Turborepo");
|
|
163
|
+
if (files.includes("nx.json"))
|
|
164
|
+
tools.add("Nx");
|
|
165
|
+
if (files.includes("pnpm-workspace.yaml"))
|
|
166
|
+
tools.add("pnpm workspaces");
|
|
167
|
+
if (rootPackage?.workspaces?.length)
|
|
168
|
+
tools.add("package workspaces");
|
|
169
|
+
const workspaceGlobs = rootPackage?.workspaces ?? (files.includes("pnpm-workspace.yaml") ? ["apps/*", "packages/*", "services/*"] : []);
|
|
170
|
+
return { detected: tools.size > 0 || packages.length > 1, tools: [...tools], workspaceGlobs };
|
|
171
|
+
}
|
|
172
|
+
function detectCommands(packages, projectPackageManager) {
|
|
173
|
+
const map = {};
|
|
174
|
+
const names = ["dev", "build", "test", "lint", "typecheck", "format"];
|
|
175
|
+
for (const pkg of packages) {
|
|
176
|
+
const dir = path.posix.dirname(pkg.path) === "." ? "." : path.posix.dirname(pkg.path);
|
|
177
|
+
const pm = pkg.packageManager?.split("@")[0] ?? projectPackageManager ?? "npm";
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
const scriptName = scriptForCommand(pkg.scripts, name);
|
|
180
|
+
if (!scriptName)
|
|
181
|
+
continue;
|
|
182
|
+
const prefix = dir === "." ? "" : `cd ${dir} && `;
|
|
183
|
+
const command = `${prefix}${pm} run ${scriptName}`;
|
|
184
|
+
map[name] = [...(map[name] ?? []), command];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return map;
|
|
188
|
+
}
|
|
189
|
+
function scriptForCommand(scripts, name) {
|
|
190
|
+
const candidates = {
|
|
191
|
+
dev: ["dev", "start:dev", "serve"],
|
|
192
|
+
build: ["build", "compile"],
|
|
193
|
+
test: ["test", "test:unit", "unit"],
|
|
194
|
+
lint: ["lint", "eslint"],
|
|
195
|
+
typecheck: ["typecheck", "type-check", "check", "tsc"],
|
|
196
|
+
format: ["format", "prettier"],
|
|
197
|
+
};
|
|
198
|
+
return findScript(scripts, candidates[name]);
|
|
199
|
+
}
|
|
200
|
+
function findScript(scripts, candidates) {
|
|
201
|
+
return candidates.find((candidate) => scripts[candidate]);
|
|
202
|
+
}
|
|
203
|
+
function hasCommand(commands, name) {
|
|
204
|
+
return Boolean(commands[name]?.length);
|
|
205
|
+
}
|
|
206
|
+
function detectLanguages(files) {
|
|
207
|
+
const counts = new Map();
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const ext = path.extname(file);
|
|
210
|
+
for (const [language, exts] of Object.entries(LANGUAGE_EXTS)) {
|
|
211
|
+
if (exts.includes(ext))
|
|
212
|
+
counts.set(language, (counts.get(language) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([language]) => language);
|
|
216
|
+
}
|
|
217
|
+
async function summarizeImportantDirs(root, files, isMonorepo) {
|
|
218
|
+
const candidates = ["apps", "packages", "services", "src", "lib", "components", "app", "pages", "api", "server", "client", "prisma", "supabase", "scripts", "docs", ".github"];
|
|
219
|
+
const summaries = [];
|
|
220
|
+
for (const candidate of candidates) {
|
|
221
|
+
const full = path.join(root, candidate);
|
|
222
|
+
try {
|
|
223
|
+
const stat = await fs.stat(full);
|
|
224
|
+
if (!stat.isDirectory())
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const children = (await listDirSafe(full)).filter((child) => !DEFAULT_IGNORES.has(child)).slice(0, isMonorepo ? 20 : 12);
|
|
231
|
+
summaries.push({ path: candidate, reason: reasonForDir(candidate), children });
|
|
232
|
+
}
|
|
233
|
+
if (!summaries.length) {
|
|
234
|
+
const top = [...new Set(files.map((file) => file.split("/")[0]))].filter((entry) => !entry.includes(".")).slice(0, 12);
|
|
235
|
+
summaries.push({ path: ".", reason: "Project root", children: top });
|
|
236
|
+
}
|
|
237
|
+
return summaries;
|
|
238
|
+
}
|
|
239
|
+
function reasonForDir(dir) {
|
|
240
|
+
const reasons = {
|
|
241
|
+
apps: "Application workspace(s)",
|
|
242
|
+
packages: "Shared package workspace(s)",
|
|
243
|
+
services: "Service workspace(s)",
|
|
244
|
+
src: "Main source code",
|
|
245
|
+
lib: "Shared library code",
|
|
246
|
+
components: "UI components",
|
|
247
|
+
app: "Application routes or app source",
|
|
248
|
+
pages: "Page routes",
|
|
249
|
+
api: "API handlers",
|
|
250
|
+
server: "Backend/server code",
|
|
251
|
+
client: "Frontend/client code",
|
|
252
|
+
prisma: "Prisma schema and migrations",
|
|
253
|
+
supabase: "Supabase config, migrations, or functions",
|
|
254
|
+
scripts: "Automation scripts",
|
|
255
|
+
docs: "Documentation",
|
|
256
|
+
".github": "GitHub workflows and repository automation",
|
|
257
|
+
};
|
|
258
|
+
return reasons[dir] ?? "Important project directory";
|
|
259
|
+
}
|
|
260
|
+
async function harnessFileState(root, relativePath) {
|
|
261
|
+
const fullPath = path.join(root, relativePath);
|
|
262
|
+
const exists = await pathExists(fullPath);
|
|
263
|
+
if (!exists)
|
|
264
|
+
return { exists: false, generatedByAgentReady: false, countsAsMaintainerAuthored: false };
|
|
265
|
+
const text = await readText(fullPath);
|
|
266
|
+
const generatedByAgentReady = Boolean(text && /generated by `?agent-ready`?|agentReady|Generated by agent-ready/i.test(text));
|
|
267
|
+
return { exists: true, generatedByAgentReady, countsAsMaintainerAuthored: !generatedByAgentReady };
|
|
268
|
+
}
|
|
269
|
+
async function analyzeCodeGraph(root, files, packages) {
|
|
270
|
+
const sourceFiles = files
|
|
271
|
+
.filter((file) => /\.(tsx?|jsx?|mjs|cjs|py|go|rs)$/.test(file))
|
|
272
|
+
.filter((file) => !file.endsWith(".d.ts"))
|
|
273
|
+
.filter((file) => !file.includes(`${path.sep}dist${path.sep}`) && !file.includes(`${path.sep}node_modules${path.sep}`) && !file.includes(`${path.sep}target${path.sep}`))
|
|
274
|
+
.slice(0, 2000);
|
|
275
|
+
const sourceSet = new Set(sourceFiles.map((file) => rel(root, file)));
|
|
276
|
+
const goModulePath = await readGoModulePath(root);
|
|
277
|
+
const entryPoints = detectEntryPoints(root, packages, sourceSet);
|
|
278
|
+
const importEdges = [];
|
|
279
|
+
const externalImportMap = new Map();
|
|
280
|
+
for (const file of sourceFiles) {
|
|
281
|
+
const text = await readText(file);
|
|
282
|
+
if (!text)
|
|
283
|
+
continue;
|
|
284
|
+
const from = rel(root, file);
|
|
285
|
+
for (const specifier of extractImportSpecifiers(text, from)) {
|
|
286
|
+
const resolved = resolveImport(from, specifier, sourceSet, goModulePath);
|
|
287
|
+
if (resolved) {
|
|
288
|
+
importEdges.push({ from, to: resolved, specifier, resolved: true });
|
|
289
|
+
}
|
|
290
|
+
else if (isProbablyLocalSpecifier(specifier, from, sourceSet, goModulePath)) {
|
|
291
|
+
importEdges.push({ from, to: specifier, specifier, resolved: false });
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const packageName = externalPackageName(specifier);
|
|
295
|
+
const importedBy = externalImportMap.get(packageName) ?? new Set();
|
|
296
|
+
importedBy.add(from);
|
|
297
|
+
externalImportMap.set(packageName, importedBy);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const inbound = new Map();
|
|
302
|
+
const outbound = new Map();
|
|
303
|
+
for (const edge of importEdges) {
|
|
304
|
+
outbound.set(edge.from, (outbound.get(edge.from) ?? 0) + 1);
|
|
305
|
+
if (edge.resolved)
|
|
306
|
+
inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
|
|
307
|
+
}
|
|
308
|
+
const centralFiles = [...sourceSet]
|
|
309
|
+
.map((file) => ({ path: file, inbound: inbound.get(file) ?? 0, outbound: outbound.get(file) ?? 0 }))
|
|
310
|
+
.filter((file) => file.inbound > 0 || file.outbound > 0)
|
|
311
|
+
.sort((a, b) => b.inbound - a.inbound || b.outbound - a.outbound)
|
|
312
|
+
.slice(0, 20);
|
|
313
|
+
const externalImports = [...externalImportMap.entries()]
|
|
314
|
+
.map(([packageName, importedBy]) => ({ packageName, importedBy: [...importedBy].sort().slice(0, 12) }))
|
|
315
|
+
.sort((a, b) => b.importedBy.length - a.importedBy.length || a.packageName.localeCompare(b.packageName))
|
|
316
|
+
.slice(0, 30);
|
|
317
|
+
return {
|
|
318
|
+
entryPoints,
|
|
319
|
+
importEdges: importEdges.filter((edge) => edge.resolved).slice(0, 120),
|
|
320
|
+
centralFiles,
|
|
321
|
+
externalImports,
|
|
322
|
+
unresolvedRelativeImports: importEdges.filter((edge) => !edge.resolved).slice(0, 30),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function detectEntryPoints(root, packages, sourceSet) {
|
|
326
|
+
const entries = new Map();
|
|
327
|
+
for (const pkg of packages) {
|
|
328
|
+
const dir = path.posix.dirname(pkg.path) === "." ? "." : path.posix.dirname(pkg.path);
|
|
329
|
+
const packageRoot = dir === "." ? "" : `${dir}/`;
|
|
330
|
+
const scripts = Object.values(pkg.scripts).join("\n");
|
|
331
|
+
const candidates = [
|
|
332
|
+
["bin", "CLI binary", "package.json bin/main entry"],
|
|
333
|
+
["cli.ts", "CLI binary", "conventional CLI entry"],
|
|
334
|
+
["cli.js", "CLI binary", "conventional CLI entry"],
|
|
335
|
+
["src/cli.ts", "CLI binary", "conventional CLI entry"],
|
|
336
|
+
["src/cli.js", "CLI binary", "conventional CLI entry"],
|
|
337
|
+
["src/index.ts", "library entry", "conventional package entry"],
|
|
338
|
+
["src/index.js", "library entry", "conventional package entry"],
|
|
339
|
+
["index.ts", "library entry", "conventional package entry"],
|
|
340
|
+
["index.js", "library entry", "conventional package entry"],
|
|
341
|
+
["src/main.ts", "application entry", "conventional app entry"],
|
|
342
|
+
["src/main.js", "application entry", "conventional app entry"],
|
|
343
|
+
["src/server.ts", "server entry", "conventional server entry"],
|
|
344
|
+
["src/server.js", "server entry", "conventional server entry"],
|
|
345
|
+
["app/page.tsx", "Next.js route", "App Router page"],
|
|
346
|
+
["app/layout.tsx", "Next.js layout", "App Router layout"],
|
|
347
|
+
["src/app/page.tsx", "Next.js route", "App Router page"],
|
|
348
|
+
["pages/index.tsx", "Next.js route", "Pages Router index"],
|
|
349
|
+
["main.py", "Python entry", "conventional Python entry"],
|
|
350
|
+
["app.py", "Python app", "conventional Python app entry"],
|
|
351
|
+
["src/main.py", "Python entry", "conventional Python entry"],
|
|
352
|
+
["cmd/main.go", "Go command", "conventional Go command entry"],
|
|
353
|
+
["main.go", "Go command", "conventional Go command entry"],
|
|
354
|
+
["src/main.rs", "Rust binary", "conventional Rust binary entry"],
|
|
355
|
+
["src/lib.rs", "Rust library", "conventional Rust library entry"],
|
|
356
|
+
];
|
|
357
|
+
for (const [candidate, kind, reason] of candidates) {
|
|
358
|
+
const file = `${packageRoot}${candidate}`;
|
|
359
|
+
if (sourceSet.has(file))
|
|
360
|
+
entries.set(file, { path: file, kind, reason });
|
|
361
|
+
}
|
|
362
|
+
for (const source of sourceSet) {
|
|
363
|
+
if (!source.startsWith(packageRoot))
|
|
364
|
+
continue;
|
|
365
|
+
const local = source.slice(packageRoot.length);
|
|
366
|
+
if (/^pages\/api\/.+\.(tsx?|jsx?)$/.test(local))
|
|
367
|
+
entries.set(source, { path: source, kind: "API route", reason: "Next.js API route" });
|
|
368
|
+
if (/^app\/.+\/(page|route)\.(tsx?|jsx?)$/.test(local))
|
|
369
|
+
entries.set(source, { path: source, kind: "Next.js route", reason: "App Router route/page" });
|
|
370
|
+
if (/^cmd\/[^/]+\/main\.go$/.test(local))
|
|
371
|
+
entries.set(source, { path: source, kind: "Go command", reason: "cmd/*/main.go" });
|
|
372
|
+
if (/^src\/bin\/[^/]+\.rs$/.test(local))
|
|
373
|
+
entries.set(source, { path: source, kind: "Rust binary", reason: "Cargo src/bin entry" });
|
|
374
|
+
}
|
|
375
|
+
if (/tsx\s+src\/cli\.ts|node\s+dist\/cli\.js/.test(scripts)) {
|
|
376
|
+
const cli = `${packageRoot}src/cli.ts`;
|
|
377
|
+
if (sourceSet.has(cli))
|
|
378
|
+
entries.set(cli, { path: cli, kind: "CLI binary", reason: "package script invokes CLI" });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return [...entries.values()].sort((a, b) => a.path.localeCompare(b.path)).slice(0, 40);
|
|
382
|
+
}
|
|
383
|
+
function extractImportSpecifiers(text, from) {
|
|
384
|
+
const specifiers = new Set();
|
|
385
|
+
const ext = path.posix.extname(from);
|
|
386
|
+
if (/\.(tsx?|jsx?|mjs|cjs)$/.test(ext)) {
|
|
387
|
+
const patterns = [
|
|
388
|
+
/import\s+(?:type\s+)?(?:[^'";]+?\s+from\s+)?["']([^"']+)["']/g,
|
|
389
|
+
/export\s+(?:type\s+)?(?:[^'";]+?\s+from\s+)["']([^"']+)["']/g,
|
|
390
|
+
/import\(\s*["']([^"']+)["']\s*\)/g,
|
|
391
|
+
/require\(\s*["']([^"']+)["']\s*\)/g,
|
|
392
|
+
];
|
|
393
|
+
for (const pattern of patterns)
|
|
394
|
+
for (const match of text.matchAll(pattern))
|
|
395
|
+
specifiers.add(match[1]);
|
|
396
|
+
}
|
|
397
|
+
if (ext === ".py") {
|
|
398
|
+
for (const match of text.matchAll(/^\s*import\s+([\w.]+)(?:\s+as\s+\w+)?/gm))
|
|
399
|
+
specifiers.add(match[1]);
|
|
400
|
+
for (const match of text.matchAll(/^\s*from\s+([\w.]+|\.+[\w.]*)\s+import\s+([\w*,\s]+)/gm)) {
|
|
401
|
+
const moduleName = match[1];
|
|
402
|
+
const imported = match[2].split(",").map((item) => item.trim().split(/\s+as\s+/)[0]).filter(Boolean);
|
|
403
|
+
specifiers.add(moduleName);
|
|
404
|
+
if (moduleName.startsWith("."))
|
|
405
|
+
for (const item of imported)
|
|
406
|
+
if (item !== "*" && /^[A-Za-z_]\w*$/.test(item))
|
|
407
|
+
specifiers.add(`${moduleName}.${item}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (ext === ".go") {
|
|
411
|
+
for (const match of text.matchAll(/import\s+(?:[\w.]+\s+)?"([^"]+)"/g))
|
|
412
|
+
specifiers.add(match[1]);
|
|
413
|
+
for (const block of text.matchAll(/import\s*\(([^)]+)\)/gs)) {
|
|
414
|
+
for (const match of block[1].matchAll(/(?:[\w.]+\s+)?"([^"]+)"/g))
|
|
415
|
+
specifiers.add(match[1]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (ext === ".rs") {
|
|
419
|
+
for (const match of text.matchAll(/^\s*(?:pub\s+)?mod\s+([A-Za-z_]\w*)\s*;/gm))
|
|
420
|
+
specifiers.add(`./${match[1]}`);
|
|
421
|
+
for (const match of text.matchAll(/^\s*(?:pub\s+)?use\s+([^;]+);/gm)) {
|
|
422
|
+
const first = match[1].trim().split(/::|\s+/)[0];
|
|
423
|
+
if (first)
|
|
424
|
+
specifiers.add(match[1].trim());
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return [...specifiers];
|
|
428
|
+
}
|
|
429
|
+
async function readGoModulePath(root) {
|
|
430
|
+
const goMod = await readText(path.join(root, "go.mod"));
|
|
431
|
+
return goMod?.match(/^module\s+(.+)$/m)?.[1]?.trim();
|
|
432
|
+
}
|
|
433
|
+
function resolveImport(from, specifier, sourceSet, goModulePath) {
|
|
434
|
+
const ext = path.posix.extname(from);
|
|
435
|
+
if (specifier.startsWith("."))
|
|
436
|
+
return resolveRelativeLikeImport(from, specifier, sourceSet, ext);
|
|
437
|
+
if (ext === ".py")
|
|
438
|
+
return resolvePythonAbsoluteImport(specifier, sourceSet);
|
|
439
|
+
if (ext === ".go" && goModulePath && specifier.startsWith(`${goModulePath}/`))
|
|
440
|
+
return resolveGoModuleImport(specifier.slice(goModulePath.length + 1), sourceSet);
|
|
441
|
+
if (ext === ".rs" && /^(crate|self|super)::/.test(specifier))
|
|
442
|
+
return resolveRustPathImport(from, specifier, sourceSet);
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
function resolveRelativeLikeImport(from, specifier, sourceSet, ext) {
|
|
446
|
+
if (ext === ".py")
|
|
447
|
+
return resolvePythonRelativeImport(from, specifier, sourceSet);
|
|
448
|
+
const fromDir = path.posix.dirname(from);
|
|
449
|
+
const base = path.posix.normalize(path.posix.join(fromDir, specifier));
|
|
450
|
+
const withoutJsRuntimeExt = base.replace(/\.(js|mjs|cjs|jsx)$/, "");
|
|
451
|
+
const candidates = [
|
|
452
|
+
base,
|
|
453
|
+
withoutJsRuntimeExt,
|
|
454
|
+
`${withoutJsRuntimeExt}.ts`, `${withoutJsRuntimeExt}.tsx`, `${withoutJsRuntimeExt}.js`, `${withoutJsRuntimeExt}.jsx`, `${withoutJsRuntimeExt}.mjs`, `${withoutJsRuntimeExt}.cjs`,
|
|
455
|
+
`${withoutJsRuntimeExt}.py`, `${withoutJsRuntimeExt}.go`, `${withoutJsRuntimeExt}.rs`,
|
|
456
|
+
`${withoutJsRuntimeExt}/index.ts`, `${withoutJsRuntimeExt}/index.tsx`, `${withoutJsRuntimeExt}/index.js`, `${withoutJsRuntimeExt}/index.jsx`,
|
|
457
|
+
`${withoutJsRuntimeExt}/__init__.py`, `${withoutJsRuntimeExt}/mod.rs`,
|
|
458
|
+
];
|
|
459
|
+
return [...new Set(candidates)].find((candidate) => sourceSet.has(candidate));
|
|
460
|
+
}
|
|
461
|
+
function resolvePythonRelativeImport(from, specifier, sourceSet) {
|
|
462
|
+
const leadingDots = specifier.match(/^\.+/)?.[0].length ?? 0;
|
|
463
|
+
const rest = specifier.slice(leadingDots).replaceAll(".", "/");
|
|
464
|
+
let dir = path.posix.dirname(from);
|
|
465
|
+
for (let i = 1; i < leadingDots; i += 1)
|
|
466
|
+
dir = path.posix.dirname(dir);
|
|
467
|
+
const base = rest ? path.posix.join(dir, rest) : dir;
|
|
468
|
+
return resolvePythonModulePath(base, sourceSet);
|
|
469
|
+
}
|
|
470
|
+
function resolvePythonAbsoluteImport(specifier, sourceSet) {
|
|
471
|
+
const modulePath = specifier.replaceAll(".", "/");
|
|
472
|
+
return resolvePythonModulePath(modulePath, sourceSet) ?? resolvePythonModulePath(`src/${modulePath}`, sourceSet);
|
|
473
|
+
}
|
|
474
|
+
function resolvePythonModulePath(base, sourceSet) {
|
|
475
|
+
const candidates = [`${base}.py`, `${base}/__init__.py`];
|
|
476
|
+
return candidates.find((candidate) => sourceSet.has(candidate));
|
|
477
|
+
}
|
|
478
|
+
function resolveGoModuleImport(localPath, sourceSet) {
|
|
479
|
+
const normalized = localPath.replace(/^\/+/, "");
|
|
480
|
+
const candidates = [...sourceSet].filter((file) => file.startsWith(`${normalized}/`) && file.endsWith(".go") && !file.endsWith("_test.go"));
|
|
481
|
+
return candidates.sort()[0];
|
|
482
|
+
}
|
|
483
|
+
function resolveRustPathImport(from, specifier, sourceSet) {
|
|
484
|
+
const parts = specifier.split("::").map((part) => part.replace(/[{}*\s].*$/, "")).filter(Boolean);
|
|
485
|
+
const scope = parts.shift();
|
|
486
|
+
let baseParts = parts;
|
|
487
|
+
if (scope === "crate") {
|
|
488
|
+
// crate::foo::bar usually maps from src/foo/bar.rs or src/foo.rs.
|
|
489
|
+
}
|
|
490
|
+
else if (scope === "self") {
|
|
491
|
+
const dir = path.posix.dirname(from);
|
|
492
|
+
const base = path.posix.join(dir, ...baseParts);
|
|
493
|
+
return resolveRustModulePath(base, sourceSet);
|
|
494
|
+
}
|
|
495
|
+
else if (scope === "super") {
|
|
496
|
+
const dir = path.posix.dirname(path.posix.dirname(from));
|
|
497
|
+
const base = path.posix.join(dir, ...baseParts);
|
|
498
|
+
return resolveRustModulePath(base, sourceSet);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
baseParts = [scope ?? "", ...baseParts].filter(Boolean);
|
|
502
|
+
}
|
|
503
|
+
const base = path.posix.join("src", ...baseParts);
|
|
504
|
+
return resolveRustModulePath(base, sourceSet);
|
|
505
|
+
}
|
|
506
|
+
function resolveRustModulePath(base, sourceSet) {
|
|
507
|
+
const candidates = [`${base}.rs`, `${base}/mod.rs`, `${base}/lib.rs`];
|
|
508
|
+
return candidates.find((candidate) => sourceSet.has(candidate));
|
|
509
|
+
}
|
|
510
|
+
function isProbablyLocalSpecifier(specifier, from, sourceSet, goModulePath) {
|
|
511
|
+
const ext = path.posix.extname(from);
|
|
512
|
+
return specifier.startsWith(".") || (ext === ".go" && Boolean(goModulePath && specifier.startsWith(`${goModulePath}/`))) || (ext === ".rs" && /^(crate|self|super)::/.test(specifier)) || (ext === ".py" && Boolean(resolvePythonAbsoluteImport(specifier, sourceSet)));
|
|
513
|
+
}
|
|
514
|
+
function externalPackageName(specifier) {
|
|
515
|
+
if (specifier.startsWith("@"))
|
|
516
|
+
return specifier.split("/").slice(0, 2).join("/");
|
|
517
|
+
if (specifier.includes("::"))
|
|
518
|
+
return specifier.split("::")[0];
|
|
519
|
+
if (specifier.includes("."))
|
|
520
|
+
return specifier.split(".")[0];
|
|
521
|
+
return specifier.split("/")[0];
|
|
522
|
+
}
|
|
523
|
+
function detectNoisyPaths(files) {
|
|
524
|
+
const noisy = new Set();
|
|
525
|
+
for (const file of files) {
|
|
526
|
+
const first = file.split("/")[0];
|
|
527
|
+
if (["node_modules", ".next", "dist", "build", "coverage", ".turbo", "vendor", "target"].includes(first))
|
|
528
|
+
noisy.add(first);
|
|
529
|
+
if (file.endsWith(".generated.ts") || file.includes("/generated/"))
|
|
530
|
+
noisy.add("generated");
|
|
531
|
+
}
|
|
532
|
+
return [...noisy];
|
|
533
|
+
}
|
|
534
|
+
async function detectHebrewOrRtl(files) {
|
|
535
|
+
const textFiles = files.filter((file) => /\.(ts|tsx|js|jsx|md|css|html|json)$/.test(file)).slice(0, 300);
|
|
536
|
+
for (const file of textFiles) {
|
|
537
|
+
const text = await readText(file);
|
|
538
|
+
if (!text)
|
|
539
|
+
continue;
|
|
540
|
+
if (/[\u0590-\u05FF]/.test(text) || /dir=["']rtl["']|direction:\s*rtl/.test(text))
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type CommandName = "dev" | "build" | "test" | "lint" | "typecheck" | "format";
|
|
2
|
+
export interface PackageInfo {
|
|
3
|
+
path: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
packageManager?: string;
|
|
6
|
+
scripts: Record<string, string>;
|
|
7
|
+
dependencies: Record<string, string>;
|
|
8
|
+
devDependencies: Record<string, string>;
|
|
9
|
+
workspaces?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ProjectScan {
|
|
12
|
+
root: string;
|
|
13
|
+
name: string;
|
|
14
|
+
packages: PackageInfo[];
|
|
15
|
+
packageManager?: string;
|
|
16
|
+
languages: string[];
|
|
17
|
+
frameworks: string[];
|
|
18
|
+
databases: string[];
|
|
19
|
+
deployment: string[];
|
|
20
|
+
monorepo: {
|
|
21
|
+
detected: boolean;
|
|
22
|
+
tools: string[];
|
|
23
|
+
workspaceGlobs: string[];
|
|
24
|
+
};
|
|
25
|
+
commands: Partial<Record<CommandName, string[]>>;
|
|
26
|
+
importantDirs: DirectorySummary[];
|
|
27
|
+
noisyPaths: string[];
|
|
28
|
+
existingHarness: ExistingHarness;
|
|
29
|
+
codeGraph: CodeGraph;
|
|
30
|
+
traits: {
|
|
31
|
+
hasHebrewOrRtl: boolean;
|
|
32
|
+
hasDocker: boolean;
|
|
33
|
+
hasGithubActions: boolean;
|
|
34
|
+
hasTests: boolean;
|
|
35
|
+
hasTypeScript: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface DirectorySummary {
|
|
39
|
+
path: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
children: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface ExistingHarness {
|
|
44
|
+
claudeMd: HarnessFileState;
|
|
45
|
+
codemap: HarnessFileState;
|
|
46
|
+
aiIgnore: HarnessFileState;
|
|
47
|
+
claudeSettings: HarnessFileState;
|
|
48
|
+
skillsDir: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface HarnessFileState {
|
|
51
|
+
exists: boolean;
|
|
52
|
+
generatedByAgentReady: boolean;
|
|
53
|
+
countsAsMaintainerAuthored: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface CodeGraph {
|
|
56
|
+
entryPoints: EntryPoint[];
|
|
57
|
+
importEdges: ImportEdge[];
|
|
58
|
+
centralFiles: CentralFile[];
|
|
59
|
+
externalImports: ExternalImport[];
|
|
60
|
+
unresolvedRelativeImports: ImportEdge[];
|
|
61
|
+
}
|
|
62
|
+
export interface EntryPoint {
|
|
63
|
+
path: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
}
|
|
67
|
+
export interface ImportEdge {
|
|
68
|
+
from: string;
|
|
69
|
+
to: string;
|
|
70
|
+
specifier: string;
|
|
71
|
+
resolved: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface CentralFile {
|
|
74
|
+
path: string;
|
|
75
|
+
inbound: number;
|
|
76
|
+
outbound: number;
|
|
77
|
+
}
|
|
78
|
+
export interface ExternalImport {
|
|
79
|
+
packageName: string;
|
|
80
|
+
importedBy: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface GeneratedFile {
|
|
83
|
+
path: string;
|
|
84
|
+
content: string;
|
|
85
|
+
kind: "create" | "propose" | "overwrite";
|
|
86
|
+
}
|
|
87
|
+
export interface InitOptions {
|
|
88
|
+
dryRun: boolean;
|
|
89
|
+
force: boolean;
|
|
90
|
+
verbose: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface ReadinessScore {
|
|
93
|
+
score: number;
|
|
94
|
+
strengths: string[];
|
|
95
|
+
missing: string[];
|
|
96
|
+
warnings: string[];
|
|
97
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const DEFAULT_IGNORES: Set<string>;
|
|
2
|
+
export declare function pathExists(filePath: string): Promise<boolean>;
|
|
3
|
+
export declare function readJson<T = unknown>(filePath: string): Promise<T | undefined>;
|
|
4
|
+
export declare function readText(filePath: string): Promise<string | undefined>;
|
|
5
|
+
export declare function safeWriteFile(filePath: string, content: string, force: boolean): Promise<"created" | "overwritten" | "proposed">;
|
|
6
|
+
export declare function listDirSafe(dir: string): Promise<string[]>;
|
|
7
|
+
export declare function walkFiles(root: string, options?: {
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
maxFiles?: number;
|
|
10
|
+
includeHidden?: boolean;
|
|
11
|
+
}): Promise<string[]>;
|
|
12
|
+
export declare function rel(root: string, target: string): string;
|