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/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
- // Universal blocklist always exclude these patterns
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
- "src", "app", "lib", "core", "pkg", "internal", "api", "cmd",
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
- const MAX_SOURCE_FILES = 10;
66
- const MAX_FILE_BYTES = 80_000;
67
- export async function collectProjectFiles(cwd = process.cwd()) {
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
- // Collect config files
72
- for (const cfg of CONFIG_FILES) {
73
- const filePath = join(cwd, cfg.pattern);
74
- if (existsSync(filePath)) {
75
- try {
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
- // Check for .env (note existence but never read)
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
- // Root-level *.config.{js,ts,mjs} files
91
- for (const ext of ["js", "ts", "mjs"]) {
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 matches = await glob(`*.config.${ext}`, { cwd, nodir: true });
94
- for (const m of matches) {
95
- const filePath = join(cwd, m);
96
- try {
97
- const content = readFileSync(filePath, "utf8");
98
- configs[m] = firstLines(content, 100);
99
- }
100
- catch {
101
- skipped.push({ path: m, reason: "could not read" });
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
- // Root-level *.csproj files
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
- for (const m of csprojMatches) {
111
- try {
112
- configs[m] = readFileSync(join(cwd, m), "utf8");
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
- catch {
115
- skipped.push({ path: m, reason: "could not read" });
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
- catch { /* skip */ }
120
- // Collect source files — max 10, cost-aware
121
- const allSourceFiles = await findSourceFiles(cwd);
122
- // Step 1: entry points
123
- const entries = [];
124
- for (const dir of ENTRY_DIRS) {
125
- for (const file of allSourceFiles) {
126
- const base = basename(file, extname(file));
127
- const fileDir = dirname(file);
128
- if (ENTRY_BASENAMES.has(base) && (fileDir === dir || fileDir === ".")) {
129
- if (!entries.includes(file))
130
- entries.push(file);
131
- if (entries.length >= 3)
132
- break;
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
- if (entries.length >= 3)
136
- break;
202
+ catch { /* skip */ }
137
203
  }
138
- // Step 2: breadth sample from primary source directory
139
- let primaryDir = "";
140
- let maxCount = 0;
141
- for (const dir of PRIMARY_SOURCE_DIRS) {
142
- const count = allSourceFiles.filter(f => f.startsWith(dir + "/")).length;
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
- const breadthFiles = [];
149
- if (primaryDir) {
150
- const dirFiles = allSourceFiles
151
- .filter(f => f.startsWith(primaryDir + "/"))
152
- .filter(f => !entries.includes(f));
153
- // Sort smallest first highest signal-to-size ratio
154
- const withSize = dirFiles.map(f => {
155
- try {
156
- return { path: f, size: statSync(join(cwd, f)).size };
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
- catch {
159
- return { path: f, size: Infinity };
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
- // Step 3: fill remaining from other top-level dirs
169
- const selected = [...entries, ...breadthFiles];
170
- const remaining = allSourceFiles.filter(f => !selected.includes(f));
171
- for (const f of remaining) {
172
- if (selected.length >= MAX_SOURCE_FILES)
173
- break;
174
- selected.push(f);
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
- // Step 4: read and truncate
177
- for (const filePath of selected) {
178
- const fullPath = join(cwd, filePath);
250
+ // composer.json
251
+ if (configFiles.includes("composer.json")) {
179
252
  try {
180
- const stat = statSync(fullPath);
181
- if (stat.size > MAX_FILE_BYTES) {
182
- skipped.push({ path: filePath, reason: `too large (${(stat.size / 1024).toFixed(0)}KB)` });
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
- const raw = readFileSync(fullPath, "utf8");
186
- source.push({ path: filePath, content: truncateSource(raw, filePath) });
349
+ if (files.length > 5)
350
+ lines.push(`${prefix}... +${files.length - 5} more`);
187
351
  }
188
- catch {
189
- skipped.push({ path: filePath, reason: "could not read" });
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
- return { configs, source, skipped };
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
- async function findSourceFiles(cwd) {
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
- nodir: true,
199
- ignore: [...BLOCKED_DIRS].map(d => `${d}/**`),
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
- const ext = extname(f);
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(ext))
469
+ if (BLOCKED_EXTENSIONS.has(extname(f)))
209
470
  return false;
210
- if (SOURCE_EXTENSIONS.has(ext))
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 truncateSource(content, path) {
221
- const lines = content.split("\n");
222
- const total = lines.length;
223
- if (total <= 150)
224
- return content;
225
- if (total <= 400) {
226
- return lines.slice(0, 100).join("\n") + `\n[... ${total - 100} more lines truncated]`;
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 lines.slice(0, 60).join("\n") + `\n[... truncated — ${total} lines total]`;
493
+ return entries;
229
494
  }
230
- function firstLines(content, n) {
231
- const lines = content.split("\n");
232
- if (lines.length <= n)
233
- return content;
234
- return lines.slice(0, n).join("\n") + `\n[... ${lines.length - n} more lines]`;
235
- }
236
- function truncatePackageLock(content) {
237
- try {
238
- const pkg = JSON.parse(content);
239
- return JSON.stringify({
240
- name: pkg.name,
241
- version: pkg.version,
242
- lockfileVersion: pkg.lockfileVersion,
243
- }, null, 2);
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
- catch {
246
- return "[package-lock.json: could not parse]";
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 hasOnlyBarePackageJson = Object.keys(collected.configs).length === 1 &&
252
- "package.json" in collected.configs &&
253
- isBarePkgJson(collected.configs["package.json"]);
254
- const hasAnyConfig = Object.keys(collected.configs).length > 0;
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
- }