aether-code 0.42.0 → 0.43.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/README.md +2 -0
- package/bin/aether-code.js +514 -514
- package/package.json +75 -75
- package/src/agent.js +54 -4
- package/src/project-context.js +712 -707
- package/src/render.js +254 -254
- package/src/repl.js +444 -444
- package/src/sessions.js +167 -167
- package/src/setup.js +141 -141
- package/src/tools.js +1288 -1288
- package/src/version.js +15 -15
package/src/project-context.js
CHANGED
|
@@ -1,707 +1,712 @@
|
|
|
1
|
-
// Project context scanner. Reads the cwd on startup to give the model
|
|
2
|
-
// immediate understanding of what project it's working in — tech stack,
|
|
3
|
-
// file structure, available scripts, dependencies, and any project-specific
|
|
4
|
-
// instructions. This is the single biggest quality-of-life improvement for
|
|
5
|
-
// a weaker model: instead of guessing, it KNOWS.
|
|
6
|
-
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
|
|
10
|
-
const SKIP_DIRS = new Set([
|
|
11
|
-
"node_modules", ".git", ".next", "dist", "build", "__pycache__",
|
|
12
|
-
".venv", "venv", "env", ".env", ".idea", ".vscode", "coverage",
|
|
13
|
-
".cache", ".turbo", "target", "out", ".output", ".nuxt", ".svelte-kit",
|
|
14
|
-
"vendor", "bower_components", ".parcel-cache", ".webpack",
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
const FRAMEWORK_SIGNATURES = {
|
|
18
|
-
"next.config.js": "Next.js",
|
|
19
|
-
"next.config.mjs": "Next.js",
|
|
20
|
-
"next.config.ts": "Next.js",
|
|
21
|
-
"nuxt.config.ts": "Nuxt",
|
|
22
|
-
"svelte.config.js": "SvelteKit",
|
|
23
|
-
"astro.config.mjs": "Astro",
|
|
24
|
-
"remix.config.js": "Remix",
|
|
25
|
-
"angular.json": "Angular",
|
|
26
|
-
"vue.config.js": "Vue CLI",
|
|
27
|
-
"vite.config.ts": "Vite",
|
|
28
|
-
"vite.config.js": "Vite",
|
|
29
|
-
"webpack.config.js": "Webpack",
|
|
30
|
-
"tsconfig.json": "TypeScript",
|
|
31
|
-
"Cargo.toml": "Rust (Cargo)",
|
|
32
|
-
"go.mod": "Go",
|
|
33
|
-
"pyproject.toml": "Python",
|
|
34
|
-
"setup.py": "Python (setuptools)",
|
|
35
|
-
"requirements.txt": "Python",
|
|
36
|
-
"Gemfile": "Ruby (Bundler)",
|
|
37
|
-
"pom.xml": "Java (Maven)",
|
|
38
|
-
"build.gradle": "Java/Kotlin (Gradle)",
|
|
39
|
-
"CMakeLists.txt": "C/C++ (CMake)",
|
|
40
|
-
"Makefile": "Make",
|
|
41
|
-
"Dockerfile": "Docker",
|
|
42
|
-
"docker-compose.yml": "Docker Compose",
|
|
43
|
-
"docker-compose.yaml": "Docker Compose",
|
|
44
|
-
".env": "dotenv config",
|
|
45
|
-
"prisma/schema.prisma": "Prisma ORM",
|
|
46
|
-
"drizzle.config.ts": "Drizzle ORM",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const DEP_FRAMEWORKS = {
|
|
50
|
-
next: "Next.js",
|
|
51
|
-
react: "React",
|
|
52
|
-
vue: "Vue",
|
|
53
|
-
svelte: "Svelte",
|
|
54
|
-
angular: "Angular",
|
|
55
|
-
express: "Express",
|
|
56
|
-
fastify: "Fastify",
|
|
57
|
-
koa: "Koa",
|
|
58
|
-
hono: "Hono",
|
|
59
|
-
"socket.io": "Socket.IO",
|
|
60
|
-
prisma: "Prisma",
|
|
61
|
-
drizzle: "Drizzle",
|
|
62
|
-
mongoose: "Mongoose",
|
|
63
|
-
sequelize: "Sequelize",
|
|
64
|
-
tailwindcss: "Tailwind CSS",
|
|
65
|
-
"styled-components": "styled-components",
|
|
66
|
-
electron: "Electron",
|
|
67
|
-
jest: "Jest",
|
|
68
|
-
vitest: "Vitest",
|
|
69
|
-
mocha: "Mocha",
|
|
70
|
-
playwright: "Playwright",
|
|
71
|
-
cypress: "Cypress",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function readJsonSafe(filePath) {
|
|
75
|
-
try {
|
|
76
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readTextSafe(filePath, maxBytes = 2000) {
|
|
83
|
-
try {
|
|
84
|
-
const stat = fs.statSync(filePath);
|
|
85
|
-
if (!stat.isFile() || stat.size > 500_000) return null;
|
|
86
|
-
const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
87
|
-
const fd = fs.openSync(filePath, "r");
|
|
88
|
-
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
89
|
-
fs.closeSync(fd);
|
|
90
|
-
return buf.toString("utf8", 0, bytesRead);
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function listTopLevel(cwd) {
|
|
97
|
-
let entries;
|
|
98
|
-
try {
|
|
99
|
-
entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
100
|
-
} catch {
|
|
101
|
-
return { dirs: [], files: [] };
|
|
102
|
-
}
|
|
103
|
-
const dirs = [];
|
|
104
|
-
const files = [];
|
|
105
|
-
for (const e of entries) {
|
|
106
|
-
if (e.name.startsWith(".") && e.name !== ".env" && e.name !== ".gitignore") continue;
|
|
107
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
108
|
-
if (e.isDirectory()) {
|
|
109
|
-
dirs.push(e.name + "/");
|
|
110
|
-
} else if (e.isFile()) {
|
|
111
|
-
files.push(e.name);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
dirs.sort();
|
|
115
|
-
files.sort();
|
|
116
|
-
return { dirs, files };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function listSecondLevel(cwd, dirName) {
|
|
120
|
-
const full = path.join(cwd, dirName);
|
|
121
|
-
let entries;
|
|
122
|
-
try {
|
|
123
|
-
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
124
|
-
} catch {
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
127
|
-
const items = [];
|
|
128
|
-
for (const e of entries) {
|
|
129
|
-
if (e.name.startsWith(".")) continue;
|
|
130
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
131
|
-
items.push(e.isDirectory() ? e.name + "/" : e.name);
|
|
132
|
-
}
|
|
133
|
-
items.sort();
|
|
134
|
-
return items.slice(0, 30);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function detectFrameworks(cwd, topFiles) {
|
|
138
|
-
const found = new Set();
|
|
139
|
-
for (const file of topFiles) {
|
|
140
|
-
if (FRAMEWORK_SIGNATURES[file]) found.add(FRAMEWORK_SIGNATURES[file]);
|
|
141
|
-
}
|
|
142
|
-
if (fs.existsSync(path.join(cwd, "prisma", "schema.prisma"))) {
|
|
143
|
-
found.add("Prisma ORM");
|
|
144
|
-
}
|
|
145
|
-
return [...found];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function extractPackageInfo(cwd) {
|
|
149
|
-
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
150
|
-
if (!pkg) return null;
|
|
151
|
-
const info = { name: pkg.name, version: pkg.version };
|
|
152
|
-
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
153
|
-
info.scripts = Object.keys(pkg.scripts);
|
|
154
|
-
}
|
|
155
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
156
|
-
const detectedLibs = [];
|
|
157
|
-
for (const [dep, label] of Object.entries(DEP_FRAMEWORKS)) {
|
|
158
|
-
if (allDeps[dep]) detectedLibs.push(label);
|
|
159
|
-
}
|
|
160
|
-
if (detectedLibs.length > 0) info.frameworks = detectedLibs;
|
|
161
|
-
const depCount = Object.keys(pkg.dependencies || {}).length;
|
|
162
|
-
const devCount = Object.keys(pkg.devDependencies || {}).length;
|
|
163
|
-
if (depCount || devCount) {
|
|
164
|
-
info.depCounts = `${depCount} deps, ${devCount} devDeps`;
|
|
165
|
-
}
|
|
166
|
-
return info;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function extractCargoInfo(cwd) {
|
|
170
|
-
const raw = readTextSafe(path.join(cwd, "Cargo.toml"), 1500);
|
|
171
|
-
if (!raw) return null;
|
|
172
|
-
const name = raw.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
173
|
-
const version = raw.match(/^version\s*=\s*"(.+?)"/m)?.[1];
|
|
174
|
-
return { name, version, type: "Rust" };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function extractGoInfo(cwd) {
|
|
178
|
-
const raw = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
179
|
-
if (!raw) return null;
|
|
180
|
-
const mod = raw.match(/^module\s+(\S+)/m)?.[1];
|
|
181
|
-
const goVer = raw.match(/^go\s+(\S+)/m)?.[1];
|
|
182
|
-
return { module: mod, goVersion: goVer, type: "Go" };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function extractPythonInfo(cwd) {
|
|
186
|
-
const pyproject = readTextSafe(path.join(cwd, "pyproject.toml"), 1500);
|
|
187
|
-
if (pyproject) {
|
|
188
|
-
const name = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
189
|
-
return { name, type: "Python" };
|
|
190
|
-
}
|
|
191
|
-
if (fs.existsSync(path.join(cwd, "requirements.txt"))) {
|
|
192
|
-
return { type: "Python (requirements.txt)" };
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function scanProject(cwd) {
|
|
198
|
-
const { dirs, files } = listTopLevel(cwd);
|
|
199
|
-
if (dirs.length === 0 && files.length === 0) {
|
|
200
|
-
return "[project]\nEmpty directory — no files detected.\n[/project]\n";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const lines = ["[project]"];
|
|
204
|
-
|
|
205
|
-
const frameworks = detectFrameworks(cwd, files);
|
|
206
|
-
if (frameworks.length > 0) {
|
|
207
|
-
lines.push(`stack: ${frameworks.join(", ")}`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const pkg = extractPackageInfo(cwd);
|
|
211
|
-
const cargo = extractCargoInfo(cwd);
|
|
212
|
-
const goInfo = extractGoInfo(cwd);
|
|
213
|
-
const pyInfo = extractPythonInfo(cwd);
|
|
214
|
-
|
|
215
|
-
if (pkg) {
|
|
216
|
-
if (pkg.name) lines.push(`package: ${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`);
|
|
217
|
-
if (pkg.frameworks) lines.push(`libraries: ${pkg.frameworks.join(", ")}`);
|
|
218
|
-
if (pkg.depCounts) lines.push(`dependencies: ${pkg.depCounts}`);
|
|
219
|
-
if (pkg.scripts) lines.push(`scripts: ${pkg.scripts.join(", ")}`);
|
|
220
|
-
}
|
|
221
|
-
if (cargo) {
|
|
222
|
-
lines.push(`crate: ${cargo.name || "unknown"}${cargo.version ? `@${cargo.version}` : ""}`);
|
|
223
|
-
}
|
|
224
|
-
if (goInfo) {
|
|
225
|
-
lines.push(`module: ${goInfo.module || "unknown"}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
226
|
-
}
|
|
227
|
-
if (pyInfo) {
|
|
228
|
-
lines.push(`python project: ${pyInfo.name || pyInfo.type}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
lines.push("");
|
|
232
|
-
lines.push("structure:");
|
|
233
|
-
for (const d of dirs) {
|
|
234
|
-
lines.push(` ${d}`);
|
|
235
|
-
const children = listSecondLevel(cwd, d.replace(/\/$/, ""));
|
|
236
|
-
for (const child of children.slice(0, 15)) {
|
|
237
|
-
lines.push(` ${child}`);
|
|
238
|
-
}
|
|
239
|
-
if (children.length > 15) {
|
|
240
|
-
lines.push(` ... (${children.length - 15} more)`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
for (const f of files) {
|
|
244
|
-
lines.push(` ${f}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const contextFile = path.join(cwd, ".aether", "context.md");
|
|
248
|
-
const context = readTextSafe(contextFile, 3000);
|
|
249
|
-
if (context) {
|
|
250
|
-
lines.push("");
|
|
251
|
-
lines.push("project-instructions:");
|
|
252
|
-
lines.push(context.trim());
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const readme = readTextSafe(path.join(cwd, "README.md"), 1500);
|
|
256
|
-
if (readme) {
|
|
257
|
-
lines.push("");
|
|
258
|
-
lines.push("readme-excerpt:");
|
|
259
|
-
lines.push(readme.trim());
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
lines.push("[/project]");
|
|
263
|
-
return lines.join("\n") + "\n";
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ─────────── /init — deep project indexing ───────────
|
|
267
|
-
|
|
268
|
-
const KEY_FILES = [
|
|
269
|
-
"README.md", "CLAUDE.md", ".env.example", "Dockerfile",
|
|
270
|
-
"docker-compose.yml", "docker-compose.yaml",
|
|
271
|
-
];
|
|
272
|
-
|
|
273
|
-
const ENTRY_PATTERNS = [
|
|
274
|
-
/^(src\/)?index\.[jt]sx?$/,
|
|
275
|
-
/^(src\/)?main\.[jt]sx?$/,
|
|
276
|
-
/^(src\/)?app\.[jt]sx?$/,
|
|
277
|
-
/^(src\/)?server\.[jt]sx?$/,
|
|
278
|
-
/^(bin|cli|cmd)\/[^/]+\.[jt]sx?$/,
|
|
279
|
-
/^(pages|app)\/_app\.[jt]sx?$/,
|
|
280
|
-
/^(pages|app)\/layout\.[jt]sx?$/,
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
const CONFIG_FILES = [
|
|
284
|
-
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
285
|
-
"vite.config.ts", "vite.config.js", "webpack.config.js",
|
|
286
|
-
"tsconfig.json", "tailwind.config.js", "tailwind.config.ts",
|
|
287
|
-
"eslint.config.js", ".eslintrc.js", ".eslintrc.json",
|
|
288
|
-
"jest.config.js", "jest.config.ts", "vitest.config.ts",
|
|
289
|
-
"prisma/schema.prisma", "drizzle.config.ts",
|
|
290
|
-
];
|
|
291
|
-
|
|
292
|
-
function collectSourceFiles(dir, base, maxFiles = 500) {
|
|
293
|
-
const results = [];
|
|
294
|
-
function walk(current, rel) {
|
|
295
|
-
if (results.length >= maxFiles) return;
|
|
296
|
-
let entries;
|
|
297
|
-
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
298
|
-
for (const e of entries) {
|
|
299
|
-
if (results.length >= maxFiles) return;
|
|
300
|
-
if (e.name.startsWith(".") && e.name !== ".env.example") continue;
|
|
301
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
302
|
-
const fullPath = path.join(current, e.name);
|
|
303
|
-
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
|
304
|
-
if (e.isDirectory()) {
|
|
305
|
-
walk(fullPath, relPath);
|
|
306
|
-
} else if (e.isFile()) {
|
|
307
|
-
let size = 0;
|
|
308
|
-
try { size = fs.statSync(fullPath).size; } catch { /* skip */ }
|
|
309
|
-
results.push({ path: relPath, size });
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
walk(dir, "");
|
|
314
|
-
return results;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function identifyKeyFiles(allFiles, cwd) {
|
|
318
|
-
const key = [];
|
|
319
|
-
for (const f of allFiles) {
|
|
320
|
-
const name = f.path.split("/").pop();
|
|
321
|
-
if (KEY_FILES.includes(name)) { key.push(f.path); continue; }
|
|
322
|
-
if (CONFIG_FILES.includes(f.path)) { key.push(f.path); continue; }
|
|
323
|
-
for (const pat of ENTRY_PATTERNS) {
|
|
324
|
-
if (pat.test(f.path)) { key.push(f.path); break; }
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return key;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function summarizeFile(filePath, maxLines = 30) {
|
|
331
|
-
try {
|
|
332
|
-
const stat = fs.statSync(filePath);
|
|
333
|
-
if (!stat.isFile() || stat.size > 200_000) return null;
|
|
334
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
335
|
-
const lines = content.split("\n");
|
|
336
|
-
const exports = [];
|
|
337
|
-
const imports = [];
|
|
338
|
-
for (const line of lines.slice(0, 100)) {
|
|
339
|
-
if (/^export\s+(default\s+)?(function|class|const|let|async)\s+(\w+)/m.test(line)) {
|
|
340
|
-
const match = line.match(/^export\s+(?:default\s+)?(?:function|class|const|let|async\s+function)\s+(\w+)/);
|
|
341
|
-
if (match) exports.push(match[1]);
|
|
342
|
-
}
|
|
343
|
-
if (/^import\s/.test(line)) {
|
|
344
|
-
const match = line.match(/from\s+["']([^"']+)["']/);
|
|
345
|
-
if (match && !match[1].startsWith(".")) imports.push(match[1]);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const summary = { lines: lines.length };
|
|
349
|
-
if (exports.length > 0) summary.exports = exports.slice(0, 15);
|
|
350
|
-
if (imports.length > 0) summary.externalImports = [...new Set(imports)].slice(0, 10);
|
|
351
|
-
const firstComment = content.match(/^\/\/\s*(.+)/m);
|
|
352
|
-
if (firstComment) summary.description = firstComment[1].trim().slice(0, 120);
|
|
353
|
-
return summary;
|
|
354
|
-
} catch {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function detectConventions(allFiles, cwd) {
|
|
360
|
-
const conventions = [];
|
|
361
|
-
const extensions = new Set();
|
|
362
|
-
for (const f of allFiles) {
|
|
363
|
-
const ext = path.extname(f.path);
|
|
364
|
-
if (ext) extensions.add(ext);
|
|
365
|
-
}
|
|
366
|
-
if (extensions.has(".ts") || extensions.has(".tsx")) conventions.push("TypeScript");
|
|
367
|
-
if (extensions.has(".jsx") || extensions.has(".tsx")) conventions.push("JSX/TSX (React-style)");
|
|
368
|
-
const hasTest = allFiles.some((f) =>
|
|
369
|
-
/\.(test|spec)\.[jt]sx?$/.test(f.path) || f.path.startsWith("test/") || f.path.startsWith("__tests__/"),
|
|
370
|
-
);
|
|
371
|
-
if (hasTest) {
|
|
372
|
-
const testDirs = [];
|
|
373
|
-
if (allFiles.some((f) => f.path.startsWith("test/"))) testDirs.push("test/");
|
|
374
|
-
if (allFiles.some((f) => f.path.startsWith("__tests__/"))) testDirs.push("__tests__/");
|
|
375
|
-
const colocated = allFiles.some((f) => /src\/.*\.(test|spec)\.[jt]sx?$/.test(f.path));
|
|
376
|
-
if (colocated) testDirs.push("colocated in src/");
|
|
377
|
-
conventions.push(`Tests: ${testDirs.join(", ") || "detected"}`);
|
|
378
|
-
}
|
|
379
|
-
if (fs.existsSync(path.join(cwd, ".prettierrc")) || fs.existsSync(path.join(cwd, ".prettierrc.json"))) {
|
|
380
|
-
conventions.push("Prettier formatting");
|
|
381
|
-
}
|
|
382
|
-
if (fs.existsSync(path.join(cwd, ".eslintrc.js")) || fs.existsSync(path.join(cwd, "eslint.config.js"))) {
|
|
383
|
-
conventions.push("ESLint");
|
|
384
|
-
}
|
|
385
|
-
return conventions;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function buildDirectoryTree(allFiles, maxDepth = 3) {
|
|
389
|
-
const tree = {};
|
|
390
|
-
for (const f of allFiles) {
|
|
391
|
-
const parts = f.path.split("/");
|
|
392
|
-
let node = tree;
|
|
393
|
-
for (let i = 0; i < Math.min(parts.length - 1, maxDepth); i++) {
|
|
394
|
-
if (!node[parts[i]]) node[parts[i]] = {};
|
|
395
|
-
node = node[parts[i]];
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function render(node, indent = "") {
|
|
399
|
-
const lines = [];
|
|
400
|
-
const entries = Object.keys(node).sort();
|
|
401
|
-
for (const key of entries) {
|
|
402
|
-
const children = Object.keys(node[key]);
|
|
403
|
-
lines.push(`${indent}${key}/`);
|
|
404
|
-
if (children.length > 0 && indent.length < maxDepth * 2) {
|
|
405
|
-
lines.push(...render(node[key], indent + " "));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return lines;
|
|
409
|
-
}
|
|
410
|
-
return render(tree);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function generateProjectIndex(cwd) {
|
|
414
|
-
const lines = [];
|
|
415
|
-
lines.push("# Project Context");
|
|
416
|
-
lines.push(`# Auto-generated by \`aether /init\` — edit freely, this file is yours.`);
|
|
417
|
-
lines.push(`# Loaded on every agent turn to give the model deep project understanding.`);
|
|
418
|
-
lines.push("");
|
|
419
|
-
|
|
420
|
-
const { dirs, files: topFiles } = listTopLevel(cwd);
|
|
421
|
-
const frameworks = detectFrameworks(cwd, topFiles);
|
|
422
|
-
const pkg = extractPackageInfo(cwd);
|
|
423
|
-
const cargo = extractCargoInfo(cwd);
|
|
424
|
-
const goInfo = extractGoInfo(cwd);
|
|
425
|
-
const pyInfo = extractPythonInfo(cwd);
|
|
426
|
-
|
|
427
|
-
lines.push("## Overview");
|
|
428
|
-
if (pkg?.name) lines.push(`- **Package:** ${pkg.name}${pkg.version ? ` v${pkg.version}` : ""}`);
|
|
429
|
-
if (cargo?.name) lines.push(`- **Crate:** ${cargo.name}${cargo.version ? ` v${cargo.version}` : ""}`);
|
|
430
|
-
if (goInfo?.module) lines.push(`- **Module:** ${goInfo.module}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
431
|
-
if (pyInfo?.name) lines.push(`- **Python:** ${pyInfo.name || pyInfo.type}`);
|
|
432
|
-
if (frameworks.length > 0) lines.push(`- **Stack:** ${frameworks.join(", ")}`);
|
|
433
|
-
if (pkg?.frameworks?.length) lines.push(`- **Libraries:** ${pkg.frameworks.join(", ")}`);
|
|
434
|
-
if (pkg?.depCounts) lines.push(`- **Dependencies:** ${pkg.depCounts}`);
|
|
435
|
-
lines.push("");
|
|
436
|
-
|
|
437
|
-
const allFiles = collectSourceFiles(cwd);
|
|
438
|
-
const conventions = detectConventions(allFiles, cwd);
|
|
439
|
-
if (conventions.length > 0) {
|
|
440
|
-
lines.push("## Conventions");
|
|
441
|
-
for (const c of conventions) lines.push(`- ${c}`);
|
|
442
|
-
lines.push("");
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (pkg?.scripts?.length) {
|
|
446
|
-
lines.push("## Scripts");
|
|
447
|
-
const pkgFull = readJsonSafe(path.join(cwd, "package.json"));
|
|
448
|
-
if (pkgFull?.scripts) {
|
|
449
|
-
for (const [name, cmd] of Object.entries(pkgFull.scripts)) {
|
|
450
|
-
lines.push(`- \`npm run ${name}\` → \`${cmd}\``);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
lines.push("");
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
lines.push("## Structure");
|
|
457
|
-
const tree = buildDirectoryTree(allFiles);
|
|
458
|
-
for (const t of tree.slice(0, 60)) lines.push(t);
|
|
459
|
-
if (tree.length > 60) lines.push(`... (${tree.length - 60} more directories)`);
|
|
460
|
-
lines.push("");
|
|
461
|
-
|
|
462
|
-
const keyFiles = identifyKeyFiles(allFiles, cwd);
|
|
463
|
-
if (keyFiles.length > 0) {
|
|
464
|
-
lines.push("## Key Files");
|
|
465
|
-
for (const kf of keyFiles.slice(0, 25)) {
|
|
466
|
-
const abs = path.join(cwd, kf.replace(/\//g, path.sep));
|
|
467
|
-
const summary = summarizeFile(abs);
|
|
468
|
-
if (summary) {
|
|
469
|
-
const desc = summary.description ? ` — ${summary.description}` : "";
|
|
470
|
-
const exports = summary.exports?.length ? ` (exports: ${summary.exports.join(", ")})` : "";
|
|
471
|
-
lines.push(`- \`${kf}\` [${summary.lines} lines]${desc}${exports}`);
|
|
472
|
-
} else {
|
|
473
|
-
lines.push(`- \`${kf}\``);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
lines.push("");
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const sourceByDir = {};
|
|
480
|
-
for (const f of allFiles) {
|
|
481
|
-
const dir = f.path.includes("/") ? f.path.split("/").slice(0, -1).join("/") : ".";
|
|
482
|
-
if (!sourceByDir[dir]) sourceByDir[dir] = [];
|
|
483
|
-
sourceByDir[dir].push(f);
|
|
484
|
-
}
|
|
485
|
-
const topDirs = Object.entries(sourceByDir)
|
|
486
|
-
.sort((a, b) => b[1].length - a[1].length)
|
|
487
|
-
.slice(0, 10);
|
|
488
|
-
if (topDirs.length > 0) {
|
|
489
|
-
lines.push("## Module Map");
|
|
490
|
-
for (const [dir, dirFiles] of topDirs) {
|
|
491
|
-
const exts = {};
|
|
492
|
-
for (const f of dirFiles) {
|
|
493
|
-
const ext = path.extname(f.path) || "(no ext)";
|
|
494
|
-
exts[ext] = (exts[ext] || 0) + 1;
|
|
495
|
-
}
|
|
496
|
-
const extStr = Object.entries(exts).map(([e, c]) => `${c}${e}`).join(", ");
|
|
497
|
-
lines.push(`- \`${dir}/\` — ${dirFiles.length} files (${extStr})`);
|
|
498
|
-
}
|
|
499
|
-
lines.push("");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
lines.push("## Notes");
|
|
503
|
-
lines.push("<!-- Add project-specific instructions, patterns, or gotchas below. -->");
|
|
504
|
-
lines.push("<!-- The agent reads this file on every turn. -->");
|
|
505
|
-
lines.push("");
|
|
506
|
-
|
|
507
|
-
return lines.join("\n");
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// ─────────── Auto-verify: detect project check commands ───────────
|
|
511
|
-
|
|
512
|
-
export function detectVerifyCommand(cwd) {
|
|
513
|
-
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
514
|
-
if (pkg?.scripts) {
|
|
515
|
-
if (pkg.scripts.typecheck) return { cmd: "npm run typecheck", label: "typecheck" };
|
|
516
|
-
if (pkg.scripts["type-check"]) return { cmd: "npm run type-check", label: "typecheck" };
|
|
517
|
-
}
|
|
518
|
-
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
519
|
-
const hasTsc = pkg?.devDependencies?.typescript || pkg?.dependencies?.typescript;
|
|
520
|
-
if (hasTsc) return { cmd: "npx tsc --noEmit", label: "typecheck" };
|
|
521
|
-
}
|
|
522
|
-
if (pkg?.scripts) {
|
|
523
|
-
if (pkg.scripts.lint) return { cmd: "npm run lint", label: "lint" };
|
|
524
|
-
if (pkg.scripts.check) return { cmd: "npm run check", label: "check" };
|
|
525
|
-
}
|
|
526
|
-
const cargo = readTextSafe(path.join(cwd, "Cargo.toml"), 500);
|
|
527
|
-
if (cargo) return { cmd: "cargo check", label: "cargo check" };
|
|
528
|
-
const goMod = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
529
|
-
if (goMod) return { cmd: "go vet ./...", label: "go vet" };
|
|
530
|
-
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) {
|
|
531
|
-
if (pkg?.scripts?.mypy || fs.existsSync(path.join(cwd, "mypy.ini"))) {
|
|
532
|
-
return { cmd: "mypy .", label: "mypy" };
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ─────────── Auto-build: detect compilation / build commands ───────────
|
|
539
|
-
|
|
540
|
-
export function detectBuildCommand(cwd, writtenFiles = []) {
|
|
541
|
-
const exts = new Set(writtenFiles.map((f) => path.extname(f).toLowerCase()));
|
|
542
|
-
const basenames = new Set(writtenFiles.map((f) => path.basename(f).toLowerCase()));
|
|
543
|
-
const win = process.platform === "win32";
|
|
544
|
-
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return { cmd:
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return { cmd:
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1
|
+
// Project context scanner. Reads the cwd on startup to give the model
|
|
2
|
+
// immediate understanding of what project it's working in — tech stack,
|
|
3
|
+
// file structure, available scripts, dependencies, and any project-specific
|
|
4
|
+
// instructions. This is the single biggest quality-of-life improvement for
|
|
5
|
+
// a weaker model: instead of guessing, it KNOWS.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const SKIP_DIRS = new Set([
|
|
11
|
+
"node_modules", ".git", ".next", "dist", "build", "__pycache__",
|
|
12
|
+
".venv", "venv", "env", ".env", ".idea", ".vscode", "coverage",
|
|
13
|
+
".cache", ".turbo", "target", "out", ".output", ".nuxt", ".svelte-kit",
|
|
14
|
+
"vendor", "bower_components", ".parcel-cache", ".webpack",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const FRAMEWORK_SIGNATURES = {
|
|
18
|
+
"next.config.js": "Next.js",
|
|
19
|
+
"next.config.mjs": "Next.js",
|
|
20
|
+
"next.config.ts": "Next.js",
|
|
21
|
+
"nuxt.config.ts": "Nuxt",
|
|
22
|
+
"svelte.config.js": "SvelteKit",
|
|
23
|
+
"astro.config.mjs": "Astro",
|
|
24
|
+
"remix.config.js": "Remix",
|
|
25
|
+
"angular.json": "Angular",
|
|
26
|
+
"vue.config.js": "Vue CLI",
|
|
27
|
+
"vite.config.ts": "Vite",
|
|
28
|
+
"vite.config.js": "Vite",
|
|
29
|
+
"webpack.config.js": "Webpack",
|
|
30
|
+
"tsconfig.json": "TypeScript",
|
|
31
|
+
"Cargo.toml": "Rust (Cargo)",
|
|
32
|
+
"go.mod": "Go",
|
|
33
|
+
"pyproject.toml": "Python",
|
|
34
|
+
"setup.py": "Python (setuptools)",
|
|
35
|
+
"requirements.txt": "Python",
|
|
36
|
+
"Gemfile": "Ruby (Bundler)",
|
|
37
|
+
"pom.xml": "Java (Maven)",
|
|
38
|
+
"build.gradle": "Java/Kotlin (Gradle)",
|
|
39
|
+
"CMakeLists.txt": "C/C++ (CMake)",
|
|
40
|
+
"Makefile": "Make",
|
|
41
|
+
"Dockerfile": "Docker",
|
|
42
|
+
"docker-compose.yml": "Docker Compose",
|
|
43
|
+
"docker-compose.yaml": "Docker Compose",
|
|
44
|
+
".env": "dotenv config",
|
|
45
|
+
"prisma/schema.prisma": "Prisma ORM",
|
|
46
|
+
"drizzle.config.ts": "Drizzle ORM",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEP_FRAMEWORKS = {
|
|
50
|
+
next: "Next.js",
|
|
51
|
+
react: "React",
|
|
52
|
+
vue: "Vue",
|
|
53
|
+
svelte: "Svelte",
|
|
54
|
+
angular: "Angular",
|
|
55
|
+
express: "Express",
|
|
56
|
+
fastify: "Fastify",
|
|
57
|
+
koa: "Koa",
|
|
58
|
+
hono: "Hono",
|
|
59
|
+
"socket.io": "Socket.IO",
|
|
60
|
+
prisma: "Prisma",
|
|
61
|
+
drizzle: "Drizzle",
|
|
62
|
+
mongoose: "Mongoose",
|
|
63
|
+
sequelize: "Sequelize",
|
|
64
|
+
tailwindcss: "Tailwind CSS",
|
|
65
|
+
"styled-components": "styled-components",
|
|
66
|
+
electron: "Electron",
|
|
67
|
+
jest: "Jest",
|
|
68
|
+
vitest: "Vitest",
|
|
69
|
+
mocha: "Mocha",
|
|
70
|
+
playwright: "Playwright",
|
|
71
|
+
cypress: "Cypress",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function readJsonSafe(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readTextSafe(filePath, maxBytes = 2000) {
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(filePath);
|
|
85
|
+
if (!stat.isFile() || stat.size > 500_000) return null;
|
|
86
|
+
const buf = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
87
|
+
const fd = fs.openSync(filePath, "r");
|
|
88
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
return buf.toString("utf8", 0, bytesRead);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listTopLevel(cwd) {
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
100
|
+
} catch {
|
|
101
|
+
return { dirs: [], files: [] };
|
|
102
|
+
}
|
|
103
|
+
const dirs = [];
|
|
104
|
+
const files = [];
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (e.name.startsWith(".") && e.name !== ".env" && e.name !== ".gitignore") continue;
|
|
107
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
108
|
+
if (e.isDirectory()) {
|
|
109
|
+
dirs.push(e.name + "/");
|
|
110
|
+
} else if (e.isFile()) {
|
|
111
|
+
files.push(e.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
dirs.sort();
|
|
115
|
+
files.sort();
|
|
116
|
+
return { dirs, files };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function listSecondLevel(cwd, dirName) {
|
|
120
|
+
const full = path.join(cwd, dirName);
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const items = [];
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
if (e.name.startsWith(".")) continue;
|
|
130
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
131
|
+
items.push(e.isDirectory() ? e.name + "/" : e.name);
|
|
132
|
+
}
|
|
133
|
+
items.sort();
|
|
134
|
+
return items.slice(0, 30);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function detectFrameworks(cwd, topFiles) {
|
|
138
|
+
const found = new Set();
|
|
139
|
+
for (const file of topFiles) {
|
|
140
|
+
if (FRAMEWORK_SIGNATURES[file]) found.add(FRAMEWORK_SIGNATURES[file]);
|
|
141
|
+
}
|
|
142
|
+
if (fs.existsSync(path.join(cwd, "prisma", "schema.prisma"))) {
|
|
143
|
+
found.add("Prisma ORM");
|
|
144
|
+
}
|
|
145
|
+
return [...found];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractPackageInfo(cwd) {
|
|
149
|
+
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
150
|
+
if (!pkg) return null;
|
|
151
|
+
const info = { name: pkg.name, version: pkg.version };
|
|
152
|
+
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
153
|
+
info.scripts = Object.keys(pkg.scripts);
|
|
154
|
+
}
|
|
155
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
156
|
+
const detectedLibs = [];
|
|
157
|
+
for (const [dep, label] of Object.entries(DEP_FRAMEWORKS)) {
|
|
158
|
+
if (allDeps[dep]) detectedLibs.push(label);
|
|
159
|
+
}
|
|
160
|
+
if (detectedLibs.length > 0) info.frameworks = detectedLibs;
|
|
161
|
+
const depCount = Object.keys(pkg.dependencies || {}).length;
|
|
162
|
+
const devCount = Object.keys(pkg.devDependencies || {}).length;
|
|
163
|
+
if (depCount || devCount) {
|
|
164
|
+
info.depCounts = `${depCount} deps, ${devCount} devDeps`;
|
|
165
|
+
}
|
|
166
|
+
return info;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function extractCargoInfo(cwd) {
|
|
170
|
+
const raw = readTextSafe(path.join(cwd, "Cargo.toml"), 1500);
|
|
171
|
+
if (!raw) return null;
|
|
172
|
+
const name = raw.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
173
|
+
const version = raw.match(/^version\s*=\s*"(.+?)"/m)?.[1];
|
|
174
|
+
return { name, version, type: "Rust" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractGoInfo(cwd) {
|
|
178
|
+
const raw = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
179
|
+
if (!raw) return null;
|
|
180
|
+
const mod = raw.match(/^module\s+(\S+)/m)?.[1];
|
|
181
|
+
const goVer = raw.match(/^go\s+(\S+)/m)?.[1];
|
|
182
|
+
return { module: mod, goVersion: goVer, type: "Go" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractPythonInfo(cwd) {
|
|
186
|
+
const pyproject = readTextSafe(path.join(cwd, "pyproject.toml"), 1500);
|
|
187
|
+
if (pyproject) {
|
|
188
|
+
const name = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1];
|
|
189
|
+
return { name, type: "Python" };
|
|
190
|
+
}
|
|
191
|
+
if (fs.existsSync(path.join(cwd, "requirements.txt"))) {
|
|
192
|
+
return { type: "Python (requirements.txt)" };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function scanProject(cwd) {
|
|
198
|
+
const { dirs, files } = listTopLevel(cwd);
|
|
199
|
+
if (dirs.length === 0 && files.length === 0) {
|
|
200
|
+
return "[project]\nEmpty directory — no files detected.\n[/project]\n";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lines = ["[project]"];
|
|
204
|
+
|
|
205
|
+
const frameworks = detectFrameworks(cwd, files);
|
|
206
|
+
if (frameworks.length > 0) {
|
|
207
|
+
lines.push(`stack: ${frameworks.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pkg = extractPackageInfo(cwd);
|
|
211
|
+
const cargo = extractCargoInfo(cwd);
|
|
212
|
+
const goInfo = extractGoInfo(cwd);
|
|
213
|
+
const pyInfo = extractPythonInfo(cwd);
|
|
214
|
+
|
|
215
|
+
if (pkg) {
|
|
216
|
+
if (pkg.name) lines.push(`package: ${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`);
|
|
217
|
+
if (pkg.frameworks) lines.push(`libraries: ${pkg.frameworks.join(", ")}`);
|
|
218
|
+
if (pkg.depCounts) lines.push(`dependencies: ${pkg.depCounts}`);
|
|
219
|
+
if (pkg.scripts) lines.push(`scripts: ${pkg.scripts.join(", ")}`);
|
|
220
|
+
}
|
|
221
|
+
if (cargo) {
|
|
222
|
+
lines.push(`crate: ${cargo.name || "unknown"}${cargo.version ? `@${cargo.version}` : ""}`);
|
|
223
|
+
}
|
|
224
|
+
if (goInfo) {
|
|
225
|
+
lines.push(`module: ${goInfo.module || "unknown"}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
226
|
+
}
|
|
227
|
+
if (pyInfo) {
|
|
228
|
+
lines.push(`python project: ${pyInfo.name || pyInfo.type}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("structure:");
|
|
233
|
+
for (const d of dirs) {
|
|
234
|
+
lines.push(` ${d}`);
|
|
235
|
+
const children = listSecondLevel(cwd, d.replace(/\/$/, ""));
|
|
236
|
+
for (const child of children.slice(0, 15)) {
|
|
237
|
+
lines.push(` ${child}`);
|
|
238
|
+
}
|
|
239
|
+
if (children.length > 15) {
|
|
240
|
+
lines.push(` ... (${children.length - 15} more)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const f of files) {
|
|
244
|
+
lines.push(` ${f}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const contextFile = path.join(cwd, ".aether", "context.md");
|
|
248
|
+
const context = readTextSafe(contextFile, 3000);
|
|
249
|
+
if (context) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("project-instructions:");
|
|
252
|
+
lines.push(context.trim());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const readme = readTextSafe(path.join(cwd, "README.md"), 1500);
|
|
256
|
+
if (readme) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("readme-excerpt:");
|
|
259
|
+
lines.push(readme.trim());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
lines.push("[/project]");
|
|
263
|
+
return lines.join("\n") + "\n";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─────────── /init — deep project indexing ───────────
|
|
267
|
+
|
|
268
|
+
const KEY_FILES = [
|
|
269
|
+
"README.md", "CLAUDE.md", ".env.example", "Dockerfile",
|
|
270
|
+
"docker-compose.yml", "docker-compose.yaml",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const ENTRY_PATTERNS = [
|
|
274
|
+
/^(src\/)?index\.[jt]sx?$/,
|
|
275
|
+
/^(src\/)?main\.[jt]sx?$/,
|
|
276
|
+
/^(src\/)?app\.[jt]sx?$/,
|
|
277
|
+
/^(src\/)?server\.[jt]sx?$/,
|
|
278
|
+
/^(bin|cli|cmd)\/[^/]+\.[jt]sx?$/,
|
|
279
|
+
/^(pages|app)\/_app\.[jt]sx?$/,
|
|
280
|
+
/^(pages|app)\/layout\.[jt]sx?$/,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const CONFIG_FILES = [
|
|
284
|
+
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
285
|
+
"vite.config.ts", "vite.config.js", "webpack.config.js",
|
|
286
|
+
"tsconfig.json", "tailwind.config.js", "tailwind.config.ts",
|
|
287
|
+
"eslint.config.js", ".eslintrc.js", ".eslintrc.json",
|
|
288
|
+
"jest.config.js", "jest.config.ts", "vitest.config.ts",
|
|
289
|
+
"prisma/schema.prisma", "drizzle.config.ts",
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
function collectSourceFiles(dir, base, maxFiles = 500) {
|
|
293
|
+
const results = [];
|
|
294
|
+
function walk(current, rel) {
|
|
295
|
+
if (results.length >= maxFiles) return;
|
|
296
|
+
let entries;
|
|
297
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
298
|
+
for (const e of entries) {
|
|
299
|
+
if (results.length >= maxFiles) return;
|
|
300
|
+
if (e.name.startsWith(".") && e.name !== ".env.example") continue;
|
|
301
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
302
|
+
const fullPath = path.join(current, e.name);
|
|
303
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
|
304
|
+
if (e.isDirectory()) {
|
|
305
|
+
walk(fullPath, relPath);
|
|
306
|
+
} else if (e.isFile()) {
|
|
307
|
+
let size = 0;
|
|
308
|
+
try { size = fs.statSync(fullPath).size; } catch { /* skip */ }
|
|
309
|
+
results.push({ path: relPath, size });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
walk(dir, "");
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function identifyKeyFiles(allFiles, cwd) {
|
|
318
|
+
const key = [];
|
|
319
|
+
for (const f of allFiles) {
|
|
320
|
+
const name = f.path.split("/").pop();
|
|
321
|
+
if (KEY_FILES.includes(name)) { key.push(f.path); continue; }
|
|
322
|
+
if (CONFIG_FILES.includes(f.path)) { key.push(f.path); continue; }
|
|
323
|
+
for (const pat of ENTRY_PATTERNS) {
|
|
324
|
+
if (pat.test(f.path)) { key.push(f.path); break; }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return key;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function summarizeFile(filePath, maxLines = 30) {
|
|
331
|
+
try {
|
|
332
|
+
const stat = fs.statSync(filePath);
|
|
333
|
+
if (!stat.isFile() || stat.size > 200_000) return null;
|
|
334
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
335
|
+
const lines = content.split("\n");
|
|
336
|
+
const exports = [];
|
|
337
|
+
const imports = [];
|
|
338
|
+
for (const line of lines.slice(0, 100)) {
|
|
339
|
+
if (/^export\s+(default\s+)?(function|class|const|let|async)\s+(\w+)/m.test(line)) {
|
|
340
|
+
const match = line.match(/^export\s+(?:default\s+)?(?:function|class|const|let|async\s+function)\s+(\w+)/);
|
|
341
|
+
if (match) exports.push(match[1]);
|
|
342
|
+
}
|
|
343
|
+
if (/^import\s/.test(line)) {
|
|
344
|
+
const match = line.match(/from\s+["']([^"']+)["']/);
|
|
345
|
+
if (match && !match[1].startsWith(".")) imports.push(match[1]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const summary = { lines: lines.length };
|
|
349
|
+
if (exports.length > 0) summary.exports = exports.slice(0, 15);
|
|
350
|
+
if (imports.length > 0) summary.externalImports = [...new Set(imports)].slice(0, 10);
|
|
351
|
+
const firstComment = content.match(/^\/\/\s*(.+)/m);
|
|
352
|
+
if (firstComment) summary.description = firstComment[1].trim().slice(0, 120);
|
|
353
|
+
return summary;
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function detectConventions(allFiles, cwd) {
|
|
360
|
+
const conventions = [];
|
|
361
|
+
const extensions = new Set();
|
|
362
|
+
for (const f of allFiles) {
|
|
363
|
+
const ext = path.extname(f.path);
|
|
364
|
+
if (ext) extensions.add(ext);
|
|
365
|
+
}
|
|
366
|
+
if (extensions.has(".ts") || extensions.has(".tsx")) conventions.push("TypeScript");
|
|
367
|
+
if (extensions.has(".jsx") || extensions.has(".tsx")) conventions.push("JSX/TSX (React-style)");
|
|
368
|
+
const hasTest = allFiles.some((f) =>
|
|
369
|
+
/\.(test|spec)\.[jt]sx?$/.test(f.path) || f.path.startsWith("test/") || f.path.startsWith("__tests__/"),
|
|
370
|
+
);
|
|
371
|
+
if (hasTest) {
|
|
372
|
+
const testDirs = [];
|
|
373
|
+
if (allFiles.some((f) => f.path.startsWith("test/"))) testDirs.push("test/");
|
|
374
|
+
if (allFiles.some((f) => f.path.startsWith("__tests__/"))) testDirs.push("__tests__/");
|
|
375
|
+
const colocated = allFiles.some((f) => /src\/.*\.(test|spec)\.[jt]sx?$/.test(f.path));
|
|
376
|
+
if (colocated) testDirs.push("colocated in src/");
|
|
377
|
+
conventions.push(`Tests: ${testDirs.join(", ") || "detected"}`);
|
|
378
|
+
}
|
|
379
|
+
if (fs.existsSync(path.join(cwd, ".prettierrc")) || fs.existsSync(path.join(cwd, ".prettierrc.json"))) {
|
|
380
|
+
conventions.push("Prettier formatting");
|
|
381
|
+
}
|
|
382
|
+
if (fs.existsSync(path.join(cwd, ".eslintrc.js")) || fs.existsSync(path.join(cwd, "eslint.config.js"))) {
|
|
383
|
+
conventions.push("ESLint");
|
|
384
|
+
}
|
|
385
|
+
return conventions;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildDirectoryTree(allFiles, maxDepth = 3) {
|
|
389
|
+
const tree = {};
|
|
390
|
+
for (const f of allFiles) {
|
|
391
|
+
const parts = f.path.split("/");
|
|
392
|
+
let node = tree;
|
|
393
|
+
for (let i = 0; i < Math.min(parts.length - 1, maxDepth); i++) {
|
|
394
|
+
if (!node[parts[i]]) node[parts[i]] = {};
|
|
395
|
+
node = node[parts[i]];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function render(node, indent = "") {
|
|
399
|
+
const lines = [];
|
|
400
|
+
const entries = Object.keys(node).sort();
|
|
401
|
+
for (const key of entries) {
|
|
402
|
+
const children = Object.keys(node[key]);
|
|
403
|
+
lines.push(`${indent}${key}/`);
|
|
404
|
+
if (children.length > 0 && indent.length < maxDepth * 2) {
|
|
405
|
+
lines.push(...render(node[key], indent + " "));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
410
|
+
return render(tree);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function generateProjectIndex(cwd) {
|
|
414
|
+
const lines = [];
|
|
415
|
+
lines.push("# Project Context");
|
|
416
|
+
lines.push(`# Auto-generated by \`aether /init\` — edit freely, this file is yours.`);
|
|
417
|
+
lines.push(`# Loaded on every agent turn to give the model deep project understanding.`);
|
|
418
|
+
lines.push("");
|
|
419
|
+
|
|
420
|
+
const { dirs, files: topFiles } = listTopLevel(cwd);
|
|
421
|
+
const frameworks = detectFrameworks(cwd, topFiles);
|
|
422
|
+
const pkg = extractPackageInfo(cwd);
|
|
423
|
+
const cargo = extractCargoInfo(cwd);
|
|
424
|
+
const goInfo = extractGoInfo(cwd);
|
|
425
|
+
const pyInfo = extractPythonInfo(cwd);
|
|
426
|
+
|
|
427
|
+
lines.push("## Overview");
|
|
428
|
+
if (pkg?.name) lines.push(`- **Package:** ${pkg.name}${pkg.version ? ` v${pkg.version}` : ""}`);
|
|
429
|
+
if (cargo?.name) lines.push(`- **Crate:** ${cargo.name}${cargo.version ? ` v${cargo.version}` : ""}`);
|
|
430
|
+
if (goInfo?.module) lines.push(`- **Module:** ${goInfo.module}${goInfo.goVersion ? ` (go ${goInfo.goVersion})` : ""}`);
|
|
431
|
+
if (pyInfo?.name) lines.push(`- **Python:** ${pyInfo.name || pyInfo.type}`);
|
|
432
|
+
if (frameworks.length > 0) lines.push(`- **Stack:** ${frameworks.join(", ")}`);
|
|
433
|
+
if (pkg?.frameworks?.length) lines.push(`- **Libraries:** ${pkg.frameworks.join(", ")}`);
|
|
434
|
+
if (pkg?.depCounts) lines.push(`- **Dependencies:** ${pkg.depCounts}`);
|
|
435
|
+
lines.push("");
|
|
436
|
+
|
|
437
|
+
const allFiles = collectSourceFiles(cwd);
|
|
438
|
+
const conventions = detectConventions(allFiles, cwd);
|
|
439
|
+
if (conventions.length > 0) {
|
|
440
|
+
lines.push("## Conventions");
|
|
441
|
+
for (const c of conventions) lines.push(`- ${c}`);
|
|
442
|
+
lines.push("");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (pkg?.scripts?.length) {
|
|
446
|
+
lines.push("## Scripts");
|
|
447
|
+
const pkgFull = readJsonSafe(path.join(cwd, "package.json"));
|
|
448
|
+
if (pkgFull?.scripts) {
|
|
449
|
+
for (const [name, cmd] of Object.entries(pkgFull.scripts)) {
|
|
450
|
+
lines.push(`- \`npm run ${name}\` → \`${cmd}\``);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
lines.push("");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
lines.push("## Structure");
|
|
457
|
+
const tree = buildDirectoryTree(allFiles);
|
|
458
|
+
for (const t of tree.slice(0, 60)) lines.push(t);
|
|
459
|
+
if (tree.length > 60) lines.push(`... (${tree.length - 60} more directories)`);
|
|
460
|
+
lines.push("");
|
|
461
|
+
|
|
462
|
+
const keyFiles = identifyKeyFiles(allFiles, cwd);
|
|
463
|
+
if (keyFiles.length > 0) {
|
|
464
|
+
lines.push("## Key Files");
|
|
465
|
+
for (const kf of keyFiles.slice(0, 25)) {
|
|
466
|
+
const abs = path.join(cwd, kf.replace(/\//g, path.sep));
|
|
467
|
+
const summary = summarizeFile(abs);
|
|
468
|
+
if (summary) {
|
|
469
|
+
const desc = summary.description ? ` — ${summary.description}` : "";
|
|
470
|
+
const exports = summary.exports?.length ? ` (exports: ${summary.exports.join(", ")})` : "";
|
|
471
|
+
lines.push(`- \`${kf}\` [${summary.lines} lines]${desc}${exports}`);
|
|
472
|
+
} else {
|
|
473
|
+
lines.push(`- \`${kf}\``);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
lines.push("");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const sourceByDir = {};
|
|
480
|
+
for (const f of allFiles) {
|
|
481
|
+
const dir = f.path.includes("/") ? f.path.split("/").slice(0, -1).join("/") : ".";
|
|
482
|
+
if (!sourceByDir[dir]) sourceByDir[dir] = [];
|
|
483
|
+
sourceByDir[dir].push(f);
|
|
484
|
+
}
|
|
485
|
+
const topDirs = Object.entries(sourceByDir)
|
|
486
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
487
|
+
.slice(0, 10);
|
|
488
|
+
if (topDirs.length > 0) {
|
|
489
|
+
lines.push("## Module Map");
|
|
490
|
+
for (const [dir, dirFiles] of topDirs) {
|
|
491
|
+
const exts = {};
|
|
492
|
+
for (const f of dirFiles) {
|
|
493
|
+
const ext = path.extname(f.path) || "(no ext)";
|
|
494
|
+
exts[ext] = (exts[ext] || 0) + 1;
|
|
495
|
+
}
|
|
496
|
+
const extStr = Object.entries(exts).map(([e, c]) => `${c}${e}`).join(", ");
|
|
497
|
+
lines.push(`- \`${dir}/\` — ${dirFiles.length} files (${extStr})`);
|
|
498
|
+
}
|
|
499
|
+
lines.push("");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
lines.push("## Notes");
|
|
503
|
+
lines.push("<!-- Add project-specific instructions, patterns, or gotchas below. -->");
|
|
504
|
+
lines.push("<!-- The agent reads this file on every turn. -->");
|
|
505
|
+
lines.push("");
|
|
506
|
+
|
|
507
|
+
return lines.join("\n");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ─────────── Auto-verify: detect project check commands ───────────
|
|
511
|
+
|
|
512
|
+
export function detectVerifyCommand(cwd) {
|
|
513
|
+
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
514
|
+
if (pkg?.scripts) {
|
|
515
|
+
if (pkg.scripts.typecheck) return { cmd: "npm run typecheck", label: "typecheck" };
|
|
516
|
+
if (pkg.scripts["type-check"]) return { cmd: "npm run type-check", label: "typecheck" };
|
|
517
|
+
}
|
|
518
|
+
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
519
|
+
const hasTsc = pkg?.devDependencies?.typescript || pkg?.dependencies?.typescript;
|
|
520
|
+
if (hasTsc) return { cmd: "npx tsc --noEmit", label: "typecheck" };
|
|
521
|
+
}
|
|
522
|
+
if (pkg?.scripts) {
|
|
523
|
+
if (pkg.scripts.lint) return { cmd: "npm run lint", label: "lint" };
|
|
524
|
+
if (pkg.scripts.check) return { cmd: "npm run check", label: "check" };
|
|
525
|
+
}
|
|
526
|
+
const cargo = readTextSafe(path.join(cwd, "Cargo.toml"), 500);
|
|
527
|
+
if (cargo) return { cmd: "cargo check", label: "cargo check" };
|
|
528
|
+
const goMod = readTextSafe(path.join(cwd, "go.mod"), 500);
|
|
529
|
+
if (goMod) return { cmd: "go vet ./...", label: "go vet" };
|
|
530
|
+
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) {
|
|
531
|
+
if (pkg?.scripts?.mypy || fs.existsSync(path.join(cwd, "mypy.ini"))) {
|
|
532
|
+
return { cmd: "mypy .", label: "mypy" };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ─────────── Auto-build: detect compilation / build commands ───────────
|
|
539
|
+
|
|
540
|
+
export function detectBuildCommand(cwd, writtenFiles = []) {
|
|
541
|
+
const exts = new Set(writtenFiles.map((f) => path.extname(f).toLowerCase()));
|
|
542
|
+
const basenames = new Set(writtenFiles.map((f) => path.basename(f).toLowerCase()));
|
|
543
|
+
const win = process.platform === "win32";
|
|
544
|
+
// Quote a path for a shell:true command so filenames/dirs with spaces don't
|
|
545
|
+
// split into multiple args (a bare `src/my game/main.cpp` made g++ fail with a
|
|
546
|
+
// "no such file" that looked like a build error). Double quotes work in both
|
|
547
|
+
// cmd.exe and POSIX sh.
|
|
548
|
+
const q = (p) => `"${p}"`;
|
|
549
|
+
|
|
550
|
+
// Assembly (NASM/FASM/MASM)
|
|
551
|
+
if (exts.has(".asm") || exts.has(".s")) {
|
|
552
|
+
const asmFiles = writtenFiles.filter((f) => /\.(asm|s)$/i.test(f));
|
|
553
|
+
if (asmFiles.length > 0) {
|
|
554
|
+
const outName = win ? "output.exe" : "output";
|
|
555
|
+
const objName = asmFiles[0].replace(/\.(asm|s)$/i, ".o");
|
|
556
|
+
return {
|
|
557
|
+
cmd: `nasm -f ${win ? "win64" : "elf64"} ${q(asmFiles[0])} -o ${q(objName)} && ${win ? "link" : "ld"} ${q(objName)} -o ${outName}`,
|
|
558
|
+
label: "nasm",
|
|
559
|
+
type: "compiled",
|
|
560
|
+
runCmd: win ? `.\\${outName}` : `./${outName}`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// C/C++
|
|
566
|
+
if (exts.has(".c") || exts.has(".cpp") || exts.has(".cc") || exts.has(".cxx") || exts.has(".h") || exts.has(".hpp")) {
|
|
567
|
+
if (fs.existsSync(path.join(cwd, "CMakeLists.txt"))) {
|
|
568
|
+
return { cmd: "cmake -B build && cmake --build build", label: "cmake", type: "compiled" };
|
|
569
|
+
}
|
|
570
|
+
if (fs.existsSync(path.join(cwd, "Makefile")) || fs.existsSync(path.join(cwd, "makefile"))) {
|
|
571
|
+
return { cmd: "make", label: "make", type: "compiled" };
|
|
572
|
+
}
|
|
573
|
+
const cFiles = writtenFiles.filter((f) => /\.(c|cpp|cc|cxx)$/i.test(f));
|
|
574
|
+
if (cFiles.length > 0) {
|
|
575
|
+
const hasC = cFiles.some((f) => /\.c$/i.test(f) && !/\.(cpp|cc|cxx)$/i.test(f));
|
|
576
|
+
const hasCpp = cFiles.some((f) => /\.(cpp|cc|cxx)$/i.test(f));
|
|
577
|
+
const compiler = hasCpp ? "g++" : "gcc";
|
|
578
|
+
const outName = win ? "output.exe" : "output";
|
|
579
|
+
const flags = [];
|
|
580
|
+
if (hasCpp) flags.push("-std=c++17");
|
|
581
|
+
else if (hasC) flags.push("-std=c11");
|
|
582
|
+
flags.push("-Wall", "-O2");
|
|
583
|
+
// Check if they need threading, math, etc. from the source
|
|
584
|
+
const allSrc = cFiles.map((f) => {
|
|
585
|
+
try { return fs.readFileSync(path.join(cwd, f), "utf8"); } catch { return ""; }
|
|
586
|
+
}).join("\n");
|
|
587
|
+
if (allSrc.includes("pthread") || allSrc.includes("<thread>")) flags.push("-pthread");
|
|
588
|
+
if (allSrc.includes("<math.h>") || allSrc.includes("<cmath>")) flags.push("-lm");
|
|
589
|
+
if (win && (allSrc.includes("winsock") || allSrc.includes("WinSock") || allSrc.includes("ws2_32"))) flags.push("-lws2_32");
|
|
590
|
+
if (win && allSrc.includes("windows.h")) flags.push("-lkernel32", "-luser32");
|
|
591
|
+
return {
|
|
592
|
+
cmd: `${compiler} ${flags.join(" ")} ${cFiles.map(q).join(" ")} -o ${outName}`,
|
|
593
|
+
label: compiler,
|
|
594
|
+
type: "compiled",
|
|
595
|
+
runCmd: win ? `.\\${outName}` : `./${outName}`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Rust
|
|
601
|
+
if (exts.has(".rs") || fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
602
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
603
|
+
return { cmd: "cargo build --release", label: "cargo", type: "compiled" };
|
|
604
|
+
}
|
|
605
|
+
const rsFiles = writtenFiles.filter((f) => /\.rs$/i.test(f));
|
|
606
|
+
if (rsFiles.length === 1) {
|
|
607
|
+
const outName = win ? "output.exe" : "output";
|
|
608
|
+
return { cmd: `rustc -o ${outName} ${q(rsFiles[0])}`, label: "rustc", type: "compiled", runCmd: win ? `.\\${outName}` : `./${outName}` };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Go
|
|
613
|
+
if (exts.has(".go") || fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
614
|
+
const outName = win ? "output.exe" : "output";
|
|
615
|
+
return { cmd: `go build -o ${outName} ./...`, label: "go build", type: "compiled", runCmd: win ? `.\\${outName}` : `./${outName}` };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// C# / .NET
|
|
619
|
+
if (exts.has(".cs")) {
|
|
620
|
+
const csproj = writtenFiles.find((f) => f.endsWith(".csproj"));
|
|
621
|
+
if (csproj || fs.existsSync(path.join(cwd, "*.csproj"))) {
|
|
622
|
+
return { cmd: "dotnet build", label: "dotnet", type: "compiled" };
|
|
623
|
+
}
|
|
624
|
+
const csFiles = writtenFiles.filter((f) => /\.cs$/i.test(f));
|
|
625
|
+
if (csFiles.length > 0) {
|
|
626
|
+
const outName = win ? "output.exe" : "output";
|
|
627
|
+
return { cmd: `dotnet-script ${csFiles[0]}`, label: "dotnet-script", type: "interpreted", runCmd: `dotnet-script ${csFiles[0]}` };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Java
|
|
632
|
+
if (exts.has(".java")) {
|
|
633
|
+
const javaFiles = writtenFiles.filter((f) => /\.java$/i.test(f));
|
|
634
|
+
if (javaFiles.length > 0) {
|
|
635
|
+
// Find the file with main()
|
|
636
|
+
let mainClass = null;
|
|
637
|
+
for (const jf of javaFiles) {
|
|
638
|
+
try {
|
|
639
|
+
const src = fs.readFileSync(path.join(cwd, jf), "utf8");
|
|
640
|
+
if (src.includes("public static void main")) {
|
|
641
|
+
mainClass = path.basename(jf, ".java");
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
} catch { /* skip */ }
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
cmd: `javac ${javaFiles.map(q).join(" ")}`,
|
|
648
|
+
label: "javac",
|
|
649
|
+
type: "compiled",
|
|
650
|
+
runCmd: mainClass ? `java ${mainClass}` : null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Node/TypeScript — npm build script
|
|
656
|
+
if (fs.existsSync(path.join(cwd, "package.json"))) {
|
|
657
|
+
const pkg = readJsonSafe(path.join(cwd, "package.json"));
|
|
658
|
+
if (pkg?.scripts?.build) {
|
|
659
|
+
const startCmd = pkg.scripts.start ? "npm start" : null;
|
|
660
|
+
return { cmd: "npm install && npm run build", label: "npm build", type: "compiled", runCmd: startCmd };
|
|
661
|
+
}
|
|
662
|
+
if (pkg?.scripts?.start) {
|
|
663
|
+
return { cmd: "npm install", label: "npm install", type: "interpreted", runCmd: "npm start" };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Python
|
|
668
|
+
if (exts.has(".py")) {
|
|
669
|
+
// Check for requirements.txt first
|
|
670
|
+
const hasReqs = fs.existsSync(path.join(cwd, "requirements.txt"));
|
|
671
|
+
const installCmd = hasReqs ? "pip install -r requirements.txt && " : "";
|
|
672
|
+
// Priority: main.py > __main__.py > app.py > single file
|
|
673
|
+
const pyFiles = writtenFiles.filter((f) => /\.py$/i.test(f));
|
|
674
|
+
const mainCandidates = ["main.py", "__main__.py", "app.py", "run.py"];
|
|
675
|
+
let entrypoint = null;
|
|
676
|
+
for (const mc of mainCandidates) {
|
|
677
|
+
const match = pyFiles.find((f) => path.basename(f).toLowerCase() === mc);
|
|
678
|
+
if (match) { entrypoint = match; break; }
|
|
679
|
+
}
|
|
680
|
+
if (!entrypoint && pyFiles.length === 1) entrypoint = pyFiles[0];
|
|
681
|
+
if (entrypoint) {
|
|
682
|
+
return {
|
|
683
|
+
cmd: installCmd ? installCmd.slice(0, -4) : `python ${entrypoint}`,
|
|
684
|
+
label: hasReqs ? "pip install" : "python",
|
|
685
|
+
type: "interpreted",
|
|
686
|
+
runCmd: `python ${entrypoint}`,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Makefile fallback (no source files matched above but a Makefile exists)
|
|
692
|
+
if (fs.existsSync(path.join(cwd, "Makefile")) || fs.existsSync(path.join(cwd, "makefile"))) {
|
|
693
|
+
return { cmd: "make", label: "make", type: "compiled" };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function scanProjectQuick(cwd) {
|
|
700
|
+
const { dirs, files } = listTopLevel(cwd);
|
|
701
|
+
const total = dirs.length + files.length;
|
|
702
|
+
if (total === 0) return "empty directory";
|
|
703
|
+
const frameworks = detectFrameworks(cwd, files);
|
|
704
|
+
const pkg = extractPackageInfo(cwd);
|
|
705
|
+
const parts = [];
|
|
706
|
+
if (pkg?.name) parts.push(pkg.name);
|
|
707
|
+
if (frameworks.length > 0) parts.push(frameworks.join(", "));
|
|
708
|
+
else if (pkg?.frameworks?.length) parts.push(pkg.frameworks[0]);
|
|
709
|
+
parts.push(`${total} items`);
|
|
710
|
+
if (pkg?.scripts) parts.push(`${pkg.scripts.length} scripts`);
|
|
711
|
+
return parts.join(" · ");
|
|
712
|
+
}
|