claude-setup 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -51
- package/dist/builder.js +117 -128
- package/dist/collect.d.ts +10 -1
- package/dist/collect.js +448 -179
- package/dist/commands/add.js +8 -7
- package/dist/commands/init.js +1 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +42 -0
- package/package.json +1 -1
- package/templates/add.md +12 -43
- package/templates/init.md +24 -107
- package/templates/remove.md +12 -34
- package/templates/sync.md +16 -47
package/dist/collect.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { readFileSync, existsSync, statSync } from "fs";
|
|
1
|
+
import { readFileSync, existsSync, statSync, readdirSync } from "fs";
|
|
2
2
|
import { glob } from "glob";
|
|
3
3
|
import { join, extname, basename, dirname } from "path";
|
|
4
|
-
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
// --- Blocklists ---
|
|
5
6
|
const BLOCKED_DIRS = new Set([
|
|
6
7
|
"node_modules", "vendor", ".venv", "venv", "env", "__pypackages__",
|
|
7
8
|
"dist", "build", "out", ".next", ".nuxt", ".svelte-kit", "__pycache__",
|
|
8
9
|
"target", ".git", ".cache", "coverage", ".nyc_output", ".pytest_cache",
|
|
9
|
-
".tox", "htmlcov", ".ruff_cache", "logs",
|
|
10
|
+
".tox", "htmlcov", ".ruff_cache", "logs", ".egg-info",
|
|
10
11
|
]);
|
|
11
12
|
const BLOCKED_EXTENSIONS = new Set([
|
|
12
13
|
".lock", ".pyc", ".pyo", ".class", ".o", ".a", ".so", ".dylib", ".dll",
|
|
@@ -21,195 +22,453 @@ const BLOCKED_EXTENSIONS = new Set([
|
|
|
21
22
|
]);
|
|
22
23
|
const BLOCKED_FILES = new Set([
|
|
23
24
|
"go.sum", "poetry.lock", "Pipfile.lock", "composer.lock",
|
|
24
|
-
".DS_Store", "Thumbs.db",
|
|
25
|
+
".DS_Store", "Thumbs.db", "package-lock.json",
|
|
25
26
|
]);
|
|
26
|
-
// Config files to read at root (or one level deep)
|
|
27
|
-
const CONFIG_FILES = [
|
|
28
|
-
{ pattern: "package.json" },
|
|
29
|
-
{ pattern: "package-lock.json", truncate: truncatePackageLock },
|
|
30
|
-
{ pattern: "pyproject.toml" },
|
|
31
|
-
{ pattern: "setup.py", truncate: (c) => firstLines(c, 60) },
|
|
32
|
-
{ pattern: "requirements.txt" },
|
|
33
|
-
{ pattern: "Pipfile" },
|
|
34
|
-
{ pattern: "go.mod" },
|
|
35
|
-
{ pattern: "Cargo.toml" },
|
|
36
|
-
{ pattern: "pom.xml", truncate: (c) => firstLines(c, 80) },
|
|
37
|
-
{ pattern: "build.gradle", truncate: (c) => firstLines(c, 80) },
|
|
38
|
-
{ pattern: "build.gradle.kts", truncate: (c) => firstLines(c, 80) },
|
|
39
|
-
{ pattern: "composer.json" },
|
|
40
|
-
{ pattern: "Gemfile" },
|
|
41
|
-
{ pattern: "turbo.json" },
|
|
42
|
-
{ pattern: "nx.json" },
|
|
43
|
-
{ pattern: "pnpm-workspace.yaml" },
|
|
44
|
-
{ pattern: "lerna.json" },
|
|
45
|
-
{ pattern: ".env.example" },
|
|
46
|
-
{ pattern: ".env.sample" },
|
|
47
|
-
{ pattern: ".env.template" },
|
|
48
|
-
{ pattern: "docker-compose.yml", truncate: (c) => c.length > 8000 ? firstLines(c, 100) + "\n[... truncated]" : c },
|
|
49
|
-
{ pattern: "docker-compose.yaml", truncate: (c) => c.length > 8000 ? firstLines(c, 100) + "\n[... truncated]" : c },
|
|
50
|
-
{ pattern: "Dockerfile", truncate: (c) => firstLines(c, 50) },
|
|
51
|
-
];
|
|
52
27
|
const SOURCE_EXTENSIONS = new Set([
|
|
53
28
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
54
29
|
".py", ".go", ".rs", ".java", ".kt", ".scala",
|
|
55
30
|
".rb", ".php", ".cs", ".swift", ".c", ".cpp", ".h",
|
|
56
31
|
".vue", ".svelte", ".astro",
|
|
57
32
|
]);
|
|
58
|
-
const ENTRY_BASENAMES = new Set([
|
|
59
|
-
"index", "main", "app", "server", "cmd", "cli", "mod", "run",
|
|
60
|
-
]);
|
|
33
|
+
const ENTRY_BASENAMES = new Set(["index", "main", "app", "server", "cmd", "cli", "mod", "run"]);
|
|
61
34
|
const ENTRY_DIRS = [".", "src", "app", "cmd", "bin"];
|
|
62
|
-
const PRIMARY_SOURCE_DIRS = [
|
|
63
|
-
|
|
35
|
+
const PRIMARY_SOURCE_DIRS = ["src", "app", "lib", "core", "pkg", "internal", "api", "cmd"];
|
|
36
|
+
// Config files the CLI knows how to find — but NOT what they mean
|
|
37
|
+
const KNOWN_CONFIG_FILES = [
|
|
38
|
+
"package.json", "pyproject.toml", "setup.py", "requirements.txt", "Pipfile",
|
|
39
|
+
"go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts",
|
|
40
|
+
"composer.json", "Gemfile", "turbo.json", "nx.json", "pnpm-workspace.yaml",
|
|
41
|
+
"lerna.json", ".env.example", ".env.sample", ".env.template",
|
|
42
|
+
"docker-compose.yml", "docker-compose.yaml", "Dockerfile",
|
|
43
|
+
"tsconfig.json", "Makefile",
|
|
64
44
|
];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
// --- Main collection function ---
|
|
46
|
+
export async function collectProjectFiles(cwd = process.cwd(), mode = "normal") {
|
|
47
|
+
const config = loadConfig(cwd);
|
|
48
|
+
const allBlocked = new Set([...BLOCKED_DIRS, ...config.extraBlockedDirs]);
|
|
68
49
|
const configs = {};
|
|
69
50
|
const source = [];
|
|
70
51
|
const skipped = [];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let content = readFileSync(filePath, "utf8");
|
|
77
|
-
if (cfg.truncate)
|
|
78
|
-
content = cfg.truncate(content);
|
|
79
|
-
configs[cfg.pattern] = content;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
skipped.push({ path: cfg.pattern, reason: "could not read" });
|
|
83
|
-
}
|
|
52
|
+
if (config.digestMode) {
|
|
53
|
+
const digest = await buildProjectDigest(cwd, config, allBlocked);
|
|
54
|
+
configs["__digest__"] = formatDigest(digest);
|
|
55
|
+
for (const [k, v] of Object.entries(digest.configs)) {
|
|
56
|
+
configs[k] = v;
|
|
84
57
|
}
|
|
85
58
|
}
|
|
86
|
-
|
|
59
|
+
else {
|
|
60
|
+
await collectRawConfigs(cwd, configs, skipped);
|
|
61
|
+
}
|
|
87
62
|
if (existsSync(join(cwd, ".env")) && !configs[".env"]) {
|
|
88
63
|
configs[".env"] = "[.env exists — not read for security]";
|
|
89
64
|
}
|
|
90
|
-
|
|
91
|
-
|
|
65
|
+
if (mode === "configOnly")
|
|
66
|
+
return { configs, source, skipped };
|
|
67
|
+
// Source sampling
|
|
68
|
+
const maxFiles = mode === "deep" ? config.maxSourceFiles : Math.min(config.maxSourceFiles, 10);
|
|
69
|
+
const allSourceFiles = await findSourceFiles(cwd, config.maxDepth, allBlocked);
|
|
70
|
+
const entries = selectEntryPoints(allSourceFiles, 3);
|
|
71
|
+
const breadthSample = selectBreadthSample(cwd, allSourceFiles, entries, maxFiles - entries.length, config.sourceDirs);
|
|
72
|
+
const selected = [...entries, ...breadthSample];
|
|
73
|
+
for (const filePath of selected) {
|
|
74
|
+
const fullPath = join(cwd, filePath);
|
|
92
75
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
76
|
+
const stat = statSync(fullPath);
|
|
77
|
+
if (stat.size > config.maxFileSizeKB * 1024) {
|
|
78
|
+
skipped.push({ path: filePath, reason: `${(stat.size / 1024).toFixed(0)}KB` });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const raw = readFileSync(fullPath, "utf8");
|
|
82
|
+
if (config.digestMode) {
|
|
83
|
+
const isEntry = entries.includes(filePath);
|
|
84
|
+
source.push({
|
|
85
|
+
path: filePath,
|
|
86
|
+
content: isEntry ? compactContent(raw) : extractSignatures(raw),
|
|
87
|
+
});
|
|
103
88
|
}
|
|
89
|
+
else {
|
|
90
|
+
source.push({ path: filePath, content: truncateSource(raw) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
skipped.push({ path: filePath, reason: "could not read" });
|
|
104
95
|
}
|
|
105
|
-
catch { /* glob error — skip */ }
|
|
106
96
|
}
|
|
107
|
-
|
|
97
|
+
return { configs, source, skipped };
|
|
98
|
+
}
|
|
99
|
+
// --- Digest builder ---
|
|
100
|
+
// ZERO domain knowledge. Extracts raw data only. Claude Code interprets.
|
|
101
|
+
async function buildProjectDigest(cwd, config, blockedDirs) {
|
|
102
|
+
const digest = {
|
|
103
|
+
configFilesFound: [],
|
|
104
|
+
deps: [],
|
|
105
|
+
scripts: [],
|
|
106
|
+
tree: "",
|
|
107
|
+
envVars: [],
|
|
108
|
+
configs: {},
|
|
109
|
+
};
|
|
110
|
+
// Which config files exist? Just the names — Claude Code infers the stack.
|
|
111
|
+
for (const name of KNOWN_CONFIG_FILES) {
|
|
112
|
+
if (existsSync(join(cwd, name))) {
|
|
113
|
+
digest.configFilesFound.push(name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Also check for *.config.{js,ts,mjs} and *.csproj
|
|
108
117
|
try {
|
|
118
|
+
for (const ext of ["js", "ts", "mjs"]) {
|
|
119
|
+
const matches = await glob(`*.config.${ext}`, { cwd, nodir: true });
|
|
120
|
+
digest.configFilesFound.push(...matches);
|
|
121
|
+
}
|
|
109
122
|
const csprojMatches = await glob("*.csproj", { cwd, nodir: true });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
digest.configFilesFound.push(...csprojMatches);
|
|
124
|
+
}
|
|
125
|
+
catch { /* skip */ }
|
|
126
|
+
// Extract dep names from whatever package manifest exists
|
|
127
|
+
// No interpretation — just the names as strings
|
|
128
|
+
digest.deps = extractDeps(cwd, digest.configFilesFound);
|
|
129
|
+
digest.scripts = extractScripts(cwd, digest.configFilesFound);
|
|
130
|
+
// Env var names from .env.example / .env.sample / .env.template
|
|
131
|
+
for (const envFile of [".env.example", ".env.sample", ".env.template"]) {
|
|
132
|
+
const envPath = join(cwd, envFile);
|
|
133
|
+
if (existsSync(envPath)) {
|
|
134
|
+
const lines = readFileSync(envPath, "utf8").split("\n");
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
137
|
+
if (match)
|
|
138
|
+
digest.envVars.push(match[1]);
|
|
113
139
|
}
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Directory tree
|
|
144
|
+
digest.tree = buildTree(cwd, blockedDirs, 3);
|
|
145
|
+
// Full content only for infra files that are usually small and high-signal
|
|
146
|
+
for (const dcFile of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
147
|
+
const dcPath = join(cwd, dcFile);
|
|
148
|
+
if (existsSync(dcPath)) {
|
|
149
|
+
const content = readFileSync(dcPath, "utf8");
|
|
150
|
+
if (content.length < 2000) {
|
|
151
|
+
digest.configs[dcFile] = content;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Just service block headers — no interpretation of what they are
|
|
155
|
+
const services = [...content.matchAll(/^\s{2}(\w[\w-]*):\s*$/gm)].map(m => m[1]);
|
|
156
|
+
digest.configs[dcFile] = `services: ${services.join(", ")}\n[${content.split("\n").length} lines total]`;
|
|
116
157
|
}
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
const dockerfilePath = join(cwd, "Dockerfile");
|
|
161
|
+
if (existsSync(dockerfilePath)) {
|
|
162
|
+
const lines = readFileSync(dockerfilePath, "utf8").split("\n");
|
|
163
|
+
const fromLines = lines.filter(l => /^FROM\s/i.test(l.trim()));
|
|
164
|
+
if (fromLines.length)
|
|
165
|
+
digest.configs["Dockerfile"] = fromLines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
// Small monorepo configs — full content
|
|
168
|
+
for (const f of ["turbo.json", "nx.json", "pnpm-workspace.yaml", "lerna.json"]) {
|
|
169
|
+
const p = join(cwd, f);
|
|
170
|
+
if (existsSync(p)) {
|
|
171
|
+
const content = readFileSync(p, "utf8");
|
|
172
|
+
if (content.length < 500)
|
|
173
|
+
digest.configs[f] = content;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
digest.deps = [...new Set(digest.deps)].filter(Boolean);
|
|
177
|
+
return digest;
|
|
178
|
+
}
|
|
179
|
+
// --- Raw data extractors — no domain knowledge ---
|
|
180
|
+
/** Extract dependency names from any package manifest. Just names, no meaning. */
|
|
181
|
+
function extractDeps(cwd, configFiles) {
|
|
182
|
+
const deps = [];
|
|
183
|
+
// package.json — extract keys from dependencies/devDependencies
|
|
184
|
+
if (configFiles.includes("package.json")) {
|
|
185
|
+
try {
|
|
186
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
187
|
+
deps.push(...Object.keys(pkg.dependencies ?? {}));
|
|
188
|
+
deps.push(...Object.keys(pkg.devDependencies ?? {}));
|
|
189
|
+
}
|
|
190
|
+
catch { /* skip */ }
|
|
191
|
+
}
|
|
192
|
+
// requirements.txt — each non-comment line is a dep
|
|
193
|
+
if (configFiles.includes("requirements.txt")) {
|
|
194
|
+
try {
|
|
195
|
+
const lines = readFileSync(join(cwd, "requirements.txt"), "utf8").split("\n");
|
|
196
|
+
for (const l of lines) {
|
|
197
|
+
const dep = l.trim().split(/[>=<!\[;\s]/)[0].trim();
|
|
198
|
+
if (dep && !dep.startsWith("#") && !dep.startsWith("-"))
|
|
199
|
+
deps.push(dep);
|
|
133
200
|
}
|
|
134
201
|
}
|
|
135
|
-
|
|
136
|
-
break;
|
|
202
|
+
catch { /* skip */ }
|
|
137
203
|
}
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (count > maxCount) {
|
|
144
|
-
maxCount = count;
|
|
145
|
-
primaryDir = dir;
|
|
204
|
+
// pyproject.toml — extract from dependencies sections
|
|
205
|
+
if (configFiles.includes("pyproject.toml")) {
|
|
206
|
+
try {
|
|
207
|
+
const content = readFileSync(join(cwd, "pyproject.toml"), "utf8");
|
|
208
|
+
deps.push(...extractTomlDeps(content));
|
|
146
209
|
}
|
|
210
|
+
catch { /* skip */ }
|
|
147
211
|
}
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
212
|
+
// go.mod — require block
|
|
213
|
+
if (configFiles.includes("go.mod")) {
|
|
214
|
+
try {
|
|
215
|
+
const content = readFileSync(join(cwd, "go.mod"), "utf8");
|
|
216
|
+
const requires = content.match(/require\s*\(([\s\S]*?)\)/)?.[1] ?? "";
|
|
217
|
+
for (const line of requires.split("\n")) {
|
|
218
|
+
const dep = line.trim().split(/\s/)[0];
|
|
219
|
+
if (dep)
|
|
220
|
+
deps.push(dep);
|
|
157
221
|
}
|
|
158
|
-
|
|
159
|
-
|
|
222
|
+
}
|
|
223
|
+
catch { /* skip */ }
|
|
224
|
+
}
|
|
225
|
+
// Cargo.toml — [dependencies] section
|
|
226
|
+
if (configFiles.includes("Cargo.toml")) {
|
|
227
|
+
try {
|
|
228
|
+
const content = readFileSync(join(cwd, "Cargo.toml"), "utf8");
|
|
229
|
+
const depsSection = content.match(/\[dependencies\]([\s\S]*?)(\[|$)/)?.[1] ?? "";
|
|
230
|
+
for (const line of depsSection.split("\n")) {
|
|
231
|
+
const dep = line.split("=")[0].trim();
|
|
232
|
+
if (dep && !dep.startsWith("#"))
|
|
233
|
+
deps.push(dep);
|
|
160
234
|
}
|
|
161
|
-
}).sort((a, b) => a.size - b.size);
|
|
162
|
-
for (const { path: p } of withSize) {
|
|
163
|
-
if (breadthFiles.length >= 5)
|
|
164
|
-
break;
|
|
165
|
-
breadthFiles.push(p);
|
|
166
235
|
}
|
|
236
|
+
catch { /* skip */ }
|
|
167
237
|
}
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
238
|
+
// Gemfile — gem lines
|
|
239
|
+
if (configFiles.includes("Gemfile")) {
|
|
240
|
+
try {
|
|
241
|
+
const lines = readFileSync(join(cwd, "Gemfile"), "utf8").split("\n");
|
|
242
|
+
for (const l of lines) {
|
|
243
|
+
const match = l.match(/^\s*gem\s+['"]([^'"]+)/);
|
|
244
|
+
if (match)
|
|
245
|
+
deps.push(match[1]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* skip */ }
|
|
175
249
|
}
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
const fullPath = join(cwd, filePath);
|
|
250
|
+
// composer.json
|
|
251
|
+
if (configFiles.includes("composer.json")) {
|
|
179
252
|
try {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
253
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "composer.json"), "utf8"));
|
|
254
|
+
deps.push(...Object.keys(pkg.require ?? {}));
|
|
255
|
+
deps.push(...Object.keys(pkg["require-dev"] ?? {}));
|
|
256
|
+
}
|
|
257
|
+
catch { /* skip */ }
|
|
258
|
+
}
|
|
259
|
+
return deps;
|
|
260
|
+
}
|
|
261
|
+
/** Extract script/task names */
|
|
262
|
+
function extractScripts(cwd, configFiles) {
|
|
263
|
+
const scripts = [];
|
|
264
|
+
if (configFiles.includes("package.json")) {
|
|
265
|
+
try {
|
|
266
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
267
|
+
scripts.push(...Object.keys(pkg.scripts ?? {}));
|
|
268
|
+
}
|
|
269
|
+
catch { /* skip */ }
|
|
270
|
+
}
|
|
271
|
+
if (configFiles.includes("Makefile")) {
|
|
272
|
+
try {
|
|
273
|
+
const content = readFileSync(join(cwd, "Makefile"), "utf8");
|
|
274
|
+
for (const m of content.matchAll(/^([a-zA-Z_][\w-]*):/gm)) {
|
|
275
|
+
scripts.push(m[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch { /* skip */ }
|
|
279
|
+
}
|
|
280
|
+
return scripts;
|
|
281
|
+
}
|
|
282
|
+
function extractTomlDeps(content) {
|
|
283
|
+
const deps = [];
|
|
284
|
+
const sections = content.match(/\[(?:project\.dependencies|tool\.poetry\.dependencies)\]([\s\S]*?)(?:\[|$)/g) ?? [];
|
|
285
|
+
for (const section of sections) {
|
|
286
|
+
for (const line of section.split("\n")) {
|
|
287
|
+
const match = line.match(/^(\w[\w-]*)/);
|
|
288
|
+
if (match && !line.startsWith("["))
|
|
289
|
+
deps.push(match[1]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const listMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/g);
|
|
293
|
+
for (const block of listMatch ?? []) {
|
|
294
|
+
for (const m of block.matchAll(/"([^">=<\[]+)/g)) {
|
|
295
|
+
deps.push(m[1].trim());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return deps;
|
|
299
|
+
}
|
|
300
|
+
// --- Digest formatter ---
|
|
301
|
+
function formatDigest(d) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
if (d.configFilesFound.length)
|
|
304
|
+
lines.push(`Config files: ${d.configFilesFound.join(", ")}`);
|
|
305
|
+
if (d.deps.length)
|
|
306
|
+
lines.push(`Deps: ${d.deps.join(", ")}`);
|
|
307
|
+
if (d.scripts.length)
|
|
308
|
+
lines.push(`Scripts: ${d.scripts.join(", ")}`);
|
|
309
|
+
if (d.envVars.length)
|
|
310
|
+
lines.push(`Env vars: ${d.envVars.join(", ")}`);
|
|
311
|
+
if (d.tree) {
|
|
312
|
+
lines.push("");
|
|
313
|
+
lines.push("Structure:");
|
|
314
|
+
lines.push(d.tree);
|
|
315
|
+
}
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
// --- Directory tree ---
|
|
319
|
+
function buildTree(cwd, blocked, maxDepth, prefix = "", depth = 0) {
|
|
320
|
+
if (depth > maxDepth)
|
|
321
|
+
return "";
|
|
322
|
+
const lines = [];
|
|
323
|
+
try {
|
|
324
|
+
const entries = readdirSync(cwd, { withFileTypes: true })
|
|
325
|
+
.filter(e => !blocked.has(e.name) && !e.name.startsWith("."))
|
|
326
|
+
.sort((a, b) => {
|
|
327
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
328
|
+
return -1;
|
|
329
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
330
|
+
return 1;
|
|
331
|
+
return a.name.localeCompare(b.name);
|
|
332
|
+
});
|
|
333
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
334
|
+
const files = entries.filter(e => e.isFile() && SOURCE_EXTENSIONS.has(extname(e.name)));
|
|
335
|
+
for (const dir of dirs) {
|
|
336
|
+
const subPath = join(cwd, dir.name);
|
|
337
|
+
const fileCount = countSourceFiles(subPath, blocked);
|
|
338
|
+
if (fileCount === 0)
|
|
183
339
|
continue;
|
|
340
|
+
lines.push(`${prefix}${dir.name}/ (${fileCount} files)`);
|
|
341
|
+
const subtree = buildTree(subPath, blocked, maxDepth, prefix + " ", depth + 1);
|
|
342
|
+
if (subtree)
|
|
343
|
+
lines.push(subtree);
|
|
344
|
+
}
|
|
345
|
+
if (depth === 0 && files.length > 0) {
|
|
346
|
+
for (const f of files.slice(0, 5)) {
|
|
347
|
+
lines.push(`${prefix}${f.name}`);
|
|
184
348
|
}
|
|
185
|
-
|
|
186
|
-
|
|
349
|
+
if (files.length > 5)
|
|
350
|
+
lines.push(`${prefix}... +${files.length - 5} more`);
|
|
187
351
|
}
|
|
188
|
-
|
|
189
|
-
|
|
352
|
+
}
|
|
353
|
+
catch { /* skip */ }
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|
|
356
|
+
function countSourceFiles(dir, blocked) {
|
|
357
|
+
let count = 0;
|
|
358
|
+
try {
|
|
359
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
360
|
+
for (const e of entries) {
|
|
361
|
+
if (blocked.has(e.name) || e.name.startsWith("."))
|
|
362
|
+
continue;
|
|
363
|
+
if (e.isFile() && SOURCE_EXTENSIONS.has(extname(e.name)))
|
|
364
|
+
count++;
|
|
365
|
+
else if (e.isDirectory())
|
|
366
|
+
count += countSourceFiles(join(dir, e.name), blocked);
|
|
190
367
|
}
|
|
191
368
|
}
|
|
192
|
-
|
|
369
|
+
catch { /* skip */ }
|
|
370
|
+
return count;
|
|
371
|
+
}
|
|
372
|
+
// --- Source file processing ---
|
|
373
|
+
function extractSignatures(content) {
|
|
374
|
+
const lines = content.split("\n");
|
|
375
|
+
const sigs = [];
|
|
376
|
+
for (const line of lines) {
|
|
377
|
+
const trimmed = line.trim();
|
|
378
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*"))
|
|
379
|
+
continue;
|
|
380
|
+
// Imports
|
|
381
|
+
if (/^import\s/.test(trimmed) || /^from\s/.test(trimmed) || /^require\(/.test(trimmed) ||
|
|
382
|
+
/^const\s+\w+\s*=\s*require/.test(trimmed) || /^use\s/.test(trimmed)) {
|
|
383
|
+
sigs.push(trimmed);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// Exports
|
|
387
|
+
if (/^export\s/.test(trimmed) || /^module\.exports/.test(trimmed)) {
|
|
388
|
+
sigs.push(trimmed.length > 120 ? trimmed.slice(0, 120) + "..." : trimmed);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Declarations
|
|
392
|
+
if (/^(export\s+)?(async\s+)?function\s/.test(trimmed) ||
|
|
393
|
+
/^(export\s+)?(abstract\s+)?class\s/.test(trimmed) ||
|
|
394
|
+
/^(export\s+)?interface\s/.test(trimmed) ||
|
|
395
|
+
/^(export\s+)?type\s/.test(trimmed) ||
|
|
396
|
+
/^(export\s+)?enum\s/.test(trimmed) ||
|
|
397
|
+
/^def\s/.test(trimmed) || /^class\s/.test(trimmed) ||
|
|
398
|
+
/^func\s/.test(trimmed) || /^fn\s/.test(trimmed) ||
|
|
399
|
+
/^pub\s/.test(trimmed) ||
|
|
400
|
+
/^(public|private|protected)\s/.test(trimmed)) {
|
|
401
|
+
sigs.push(trimmed.length > 120 ? trimmed.slice(0, 120) + "..." : trimmed);
|
|
402
|
+
}
|
|
403
|
+
if (trimmed.startsWith("@") && !trimmed.startsWith("@import")) {
|
|
404
|
+
sigs.push(trimmed);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (sigs.length === 0)
|
|
408
|
+
return `[${lines.length} lines, no extractable signatures]`;
|
|
409
|
+
return sigs.join("\n");
|
|
410
|
+
}
|
|
411
|
+
function compactContent(content) {
|
|
412
|
+
const lines = content.split("\n");
|
|
413
|
+
const kept = [];
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
const trimmed = line.trim();
|
|
416
|
+
if (!trimmed)
|
|
417
|
+
continue;
|
|
418
|
+
if (trimmed.startsWith("//") && !trimmed.startsWith("///"))
|
|
419
|
+
continue;
|
|
420
|
+
if (trimmed === "*" || trimmed === "/**" || trimmed === "*/")
|
|
421
|
+
continue;
|
|
422
|
+
if (trimmed.startsWith("* ") && !trimmed.startsWith("* @"))
|
|
423
|
+
continue;
|
|
424
|
+
if (trimmed.startsWith("#") && !trimmed.startsWith("#!") && !trimmed.startsWith("#include"))
|
|
425
|
+
continue;
|
|
426
|
+
kept.push(line);
|
|
427
|
+
}
|
|
428
|
+
if (kept.length > 80) {
|
|
429
|
+
return kept.slice(0, 60).join("\n") + `\n[... ${kept.length - 60} more lines]`;
|
|
430
|
+
}
|
|
431
|
+
return kept.join("\n");
|
|
432
|
+
}
|
|
433
|
+
function truncateSource(content) {
|
|
434
|
+
const lines = content.split("\n");
|
|
435
|
+
if (lines.length <= 100)
|
|
436
|
+
return content;
|
|
437
|
+
if (lines.length <= 300) {
|
|
438
|
+
return lines.slice(0, 80).join("\n") + `\n[... ${lines.length - 80} more lines]`;
|
|
439
|
+
}
|
|
440
|
+
return lines.slice(0, 50).join("\n") + `\n[... truncated — ${lines.length} lines total]`;
|
|
193
441
|
}
|
|
194
|
-
|
|
442
|
+
// --- Legacy raw config collection ---
|
|
443
|
+
async function collectRawConfigs(cwd, configs, skipped) {
|
|
444
|
+
for (const name of KNOWN_CONFIG_FILES) {
|
|
445
|
+
const p = join(cwd, name);
|
|
446
|
+
if (existsSync(p)) {
|
|
447
|
+
try {
|
|
448
|
+
const content = readFileSync(p, "utf8");
|
|
449
|
+
configs[name] = content.length > 4000 ? content.slice(0, 4000) + "\n[... truncated]" : content;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
skipped.push({ path: name, reason: "could not read" });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// --- Source file discovery ---
|
|
458
|
+
async function findSourceFiles(cwd, maxDepth, blocked) {
|
|
195
459
|
try {
|
|
196
460
|
const files = await glob("**/*", {
|
|
197
|
-
cwd,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
maxDepth: 5,
|
|
461
|
+
cwd, nodir: true,
|
|
462
|
+
ignore: [...blocked].map(d => `${d}/**`),
|
|
463
|
+
maxDepth,
|
|
201
464
|
});
|
|
202
465
|
return files
|
|
203
466
|
.filter(f => {
|
|
204
|
-
|
|
205
|
-
const base = basename(f);
|
|
206
|
-
if (BLOCKED_FILES.has(base))
|
|
467
|
+
if (BLOCKED_FILES.has(basename(f)))
|
|
207
468
|
return false;
|
|
208
|
-
if (BLOCKED_EXTENSIONS.has(
|
|
469
|
+
if (BLOCKED_EXTENSIONS.has(extname(f)))
|
|
209
470
|
return false;
|
|
210
|
-
|
|
211
|
-
return true;
|
|
212
|
-
return false;
|
|
471
|
+
return SOURCE_EXTENSIONS.has(extname(f));
|
|
213
472
|
})
|
|
214
473
|
.map(f => f.replace(/\\/g, "/"));
|
|
215
474
|
}
|
|
@@ -217,50 +476,60 @@ async function findSourceFiles(cwd) {
|
|
|
217
476
|
return [];
|
|
218
477
|
}
|
|
219
478
|
}
|
|
220
|
-
function
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
479
|
+
function selectEntryPoints(files, max) {
|
|
480
|
+
const entries = [];
|
|
481
|
+
for (const dir of ENTRY_DIRS) {
|
|
482
|
+
for (const file of files) {
|
|
483
|
+
if (entries.length >= max)
|
|
484
|
+
return entries;
|
|
485
|
+
const base = basename(file, extname(file));
|
|
486
|
+
const fileDir = dirname(file);
|
|
487
|
+
if (ENTRY_BASENAMES.has(base) && (fileDir === dir || fileDir === ".")) {
|
|
488
|
+
if (!entries.includes(file))
|
|
489
|
+
entries.push(file);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
227
492
|
}
|
|
228
|
-
return
|
|
493
|
+
return entries;
|
|
229
494
|
}
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
495
|
+
function selectBreadthSample(cwd, files, exclude, max, forceDirs) {
|
|
496
|
+
const selected = [];
|
|
497
|
+
const dirsToSample = forceDirs.length > 0 ? forceDirs : PRIMARY_SOURCE_DIRS;
|
|
498
|
+
for (const dir of dirsToSample) {
|
|
499
|
+
const dirFiles = files
|
|
500
|
+
.filter(f => f.startsWith(dir + "/"))
|
|
501
|
+
.filter(f => !exclude.includes(f) && !selected.includes(f));
|
|
502
|
+
const sorted = dirFiles.sort((a, b) => {
|
|
503
|
+
try {
|
|
504
|
+
return statSync(join(cwd, a)).size - statSync(join(cwd, b)).size;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
for (const f of sorted) {
|
|
511
|
+
if (selected.length >= max)
|
|
512
|
+
return selected;
|
|
513
|
+
selected.push(f);
|
|
514
|
+
}
|
|
244
515
|
}
|
|
245
|
-
|
|
246
|
-
|
|
516
|
+
for (const f of files) {
|
|
517
|
+
if (selected.length >= max)
|
|
518
|
+
break;
|
|
519
|
+
if (!exclude.includes(f) && !selected.includes(f))
|
|
520
|
+
selected.push(f);
|
|
247
521
|
}
|
|
522
|
+
return selected;
|
|
248
523
|
}
|
|
524
|
+
// --- Empty project detection ---
|
|
249
525
|
export function isEmptyProject(collected) {
|
|
250
526
|
const hasSource = collected.source.length > 0;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const hasAnyConfig =
|
|
527
|
+
const digestContent = collected.configs["__digest__"] ?? "";
|
|
528
|
+
const hasDeps = digestContent.includes("Deps:");
|
|
529
|
+
const rawConfigs = Object.keys(collected.configs).filter(k => k !== "__digest__" && k !== ".env");
|
|
530
|
+
const hasAnyConfig = rawConfigs.length > 0 || hasDeps;
|
|
531
|
+
// A bare package.json has no deps
|
|
532
|
+
const hasOnlyBarePackageJson = !hasDeps && digestContent.includes("package.json") &&
|
|
533
|
+
!digestContent.includes(","); // only one config file
|
|
255
534
|
return !hasSource && (!hasAnyConfig || hasOnlyBarePackageJson);
|
|
256
535
|
}
|
|
257
|
-
function isBarePkgJson(content) {
|
|
258
|
-
try {
|
|
259
|
-
const pkg = JSON.parse(content);
|
|
260
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
261
|
-
return Object.keys(deps).length === 0;
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
}
|