claude-think 0.4.0 → 0.4.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/package.json +1 -1
- package/src/cli/commands/project-learn.ts +69 -172
- package/src/core/project-detect.ts +325 -50
- package/CLAUDE.md +0 -111
package/package.json
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { writeFile
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { writeFile } from "fs/promises";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { detectProject
|
|
6
|
-
|
|
7
|
-
const IGNORE = new Set([
|
|
8
|
-
"node_modules", ".git", "dist", "build", ".next", "__pycache__",
|
|
9
|
-
".venv", "venv", "target", ".cache", "coverage", ".turbo", ".DS_Store",
|
|
10
|
-
".idea", ".vscode", "vendor", "tmp", "temp", "logs",
|
|
11
|
-
]);
|
|
5
|
+
import { detectProject } from "../../core/project-detect";
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
|
-
* Generate a
|
|
15
|
-
* Efficient: just structure summary, no AI, minimal context usage
|
|
8
|
+
* Generate a project CLAUDE.md with detected info
|
|
16
9
|
*/
|
|
17
10
|
export async function projectLearnCommand(options: {
|
|
18
11
|
force?: boolean;
|
|
@@ -26,191 +19,95 @@ export async function projectLearnCommand(options: {
|
|
|
26
19
|
return;
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
console.log(chalk.blue("
|
|
22
|
+
console.log(chalk.blue("Analyzing project..."));
|
|
30
23
|
|
|
31
24
|
const project = await detectProject(cwd);
|
|
32
|
-
const structure = await scanStructure(cwd);
|
|
33
|
-
const entryPoints = findEntryPoints(cwd, project.type);
|
|
34
|
-
|
|
35
|
-
// Build compact CLAUDE.md
|
|
36
25
|
const lines: string[] = [];
|
|
37
26
|
|
|
27
|
+
// Header
|
|
38
28
|
lines.push(`# ${project.name}`);
|
|
39
29
|
lines.push("");
|
|
40
|
-
lines.push(`${project.type} project${structure.description ? `: ${structure.description}` : ""}`);
|
|
41
|
-
lines.push("");
|
|
42
30
|
|
|
43
|
-
//
|
|
44
|
-
if (
|
|
45
|
-
lines.push(
|
|
31
|
+
// Description
|
|
32
|
+
if (project.description) {
|
|
33
|
+
lines.push(project.description);
|
|
46
34
|
lines.push("");
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
// Runtime & Stack summary
|
|
38
|
+
const stackParts: string[] = [];
|
|
39
|
+
stackParts.push(project.runtime.charAt(0).toUpperCase() + project.runtime.slice(1));
|
|
40
|
+
if (project.monorepo) {
|
|
41
|
+
stackParts.push(project.monorepo.tool);
|
|
53
42
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (structure.rootFiles.length > 0) {
|
|
57
|
-
lines.push(`- root: ${structure.rootFiles.join(", ")}`);
|
|
43
|
+
if (project.frameworks.length > 0) {
|
|
44
|
+
stackParts.push(...project.frameworks.slice(0, 3));
|
|
58
45
|
}
|
|
46
|
+
lines.push(`**Stack:** ${stackParts.join(", ")}`);
|
|
59
47
|
lines.push("");
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface DirSummary {
|
|
68
|
-
name: string;
|
|
69
|
-
count: number;
|
|
70
|
-
hint?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface StructureSummary {
|
|
74
|
-
dirs: DirSummary[];
|
|
75
|
-
rootFiles: string[];
|
|
76
|
-
description?: string;
|
|
77
|
-
}
|
|
49
|
+
// Monorepo structure
|
|
50
|
+
if (project.monorepo) {
|
|
51
|
+
lines.push("## Workspaces");
|
|
52
|
+
lines.push("");
|
|
78
53
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const pkgPath = join(dir, "package.json");
|
|
87
|
-
if (existsSync(pkgPath)) {
|
|
88
|
-
try {
|
|
89
|
-
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
90
|
-
description = pkg.description;
|
|
91
|
-
} catch {}
|
|
92
|
-
}
|
|
54
|
+
// Group by type
|
|
55
|
+
const grouped = new Map<string, typeof project.monorepo.workspaces>();
|
|
56
|
+
for (const ws of project.monorepo.workspaces) {
|
|
57
|
+
const type = ws.type || "other";
|
|
58
|
+
if (!grouped.has(type)) grouped.set(type, []);
|
|
59
|
+
grouped.get(type)!.push(ws);
|
|
60
|
+
}
|
|
93
61
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
62
|
+
// Output grouped
|
|
63
|
+
const typeOrder = ["app", "server", "service", "package", "cli", "tool", "other"];
|
|
64
|
+
for (const type of typeOrder) {
|
|
65
|
+
const workspaces = grouped.get(type);
|
|
66
|
+
if (!workspaces || workspaces.length === 0) continue;
|
|
67
|
+
|
|
68
|
+
const typeLabel = type === "other" ? "Other" : type.charAt(0).toUpperCase() + type.slice(1) + "s";
|
|
69
|
+
lines.push(`### ${typeLabel}`);
|
|
70
|
+
for (const ws of workspaces) {
|
|
71
|
+
const desc = ws.description ? ` - ${ws.description}` : "";
|
|
72
|
+
lines.push(`- \`${ws.path}\` (${ws.name})${desc}`);
|
|
105
73
|
}
|
|
106
|
-
|
|
107
|
-
rootFiles.push(entry.name);
|
|
74
|
+
lines.push("");
|
|
108
75
|
}
|
|
109
76
|
}
|
|
110
77
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (bIdx !== -1) return 1;
|
|
119
|
-
return a.name.localeCompare(b.name);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return { dirs, rootFiles, description };
|
|
123
|
-
}
|
|
78
|
+
// Tooling
|
|
79
|
+
if (project.tooling.length > 0) {
|
|
80
|
+
lines.push("## Tooling");
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(project.tooling.join(", "));
|
|
83
|
+
lines.push("");
|
|
84
|
+
}
|
|
124
85
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
count += await countFiles(join(dir, entry.name));
|
|
133
|
-
} else {
|
|
134
|
-
count++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
} catch {}
|
|
138
|
-
return count;
|
|
139
|
-
}
|
|
86
|
+
// Frameworks (if not already shown in stack or there are more)
|
|
87
|
+
if (project.frameworks.length > 3) {
|
|
88
|
+
lines.push("## Frameworks");
|
|
89
|
+
lines.push("");
|
|
90
|
+
lines.push(project.frameworks.join(", "));
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
140
93
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml",
|
|
144
|
-
"Gemfile", "Makefile", "docker-compose.yml", "Dockerfile",
|
|
145
|
-
"README.md", "CHANGELOG.md",
|
|
146
|
-
];
|
|
147
|
-
return relevant.includes(name);
|
|
148
|
-
}
|
|
94
|
+
const content = lines.join("\n");
|
|
95
|
+
await writeFile(claudeMdPath, content);
|
|
149
96
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
src: "source code",
|
|
153
|
-
app: "application",
|
|
154
|
-
lib: "library",
|
|
155
|
-
pages: "routes",
|
|
156
|
-
components: "UI",
|
|
157
|
-
api: "endpoints",
|
|
158
|
-
test: "tests",
|
|
159
|
-
tests: "tests",
|
|
160
|
-
scripts: "tooling",
|
|
161
|
-
docs: "documentation",
|
|
162
|
-
public: "static assets",
|
|
163
|
-
assets: "resources",
|
|
164
|
-
config: "configuration",
|
|
165
|
-
utils: "utilities",
|
|
166
|
-
hooks: "React hooks",
|
|
167
|
-
services: "business logic",
|
|
168
|
-
models: "data models",
|
|
169
|
-
controllers: "request handlers",
|
|
170
|
-
middleware: "middleware",
|
|
171
|
-
types: "TypeScript types",
|
|
172
|
-
};
|
|
173
|
-
return hints[name];
|
|
174
|
-
}
|
|
97
|
+
console.log(chalk.green("Created CLAUDE.md"));
|
|
98
|
+
console.log(chalk.dim(`${lines.length} lines, ${content.length} bytes`));
|
|
175
99
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
184
|
-
if (pkg.bin) {
|
|
185
|
-
const bins = typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin);
|
|
186
|
-
for (const bin of bins as string[]) {
|
|
187
|
-
if (bin && !entries.includes(bin)) entries.push(bin);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (pkg.main && !entries.includes(pkg.main)) entries.push(pkg.main);
|
|
191
|
-
if (pkg.module && !entries.includes(pkg.module)) entries.push(pkg.module);
|
|
192
|
-
} catch {}
|
|
100
|
+
// Show summary
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(chalk.cyan("Detected:"));
|
|
103
|
+
console.log(` Runtime: ${project.runtime}`);
|
|
104
|
+
if (project.monorepo) {
|
|
105
|
+
console.log(` Monorepo: ${project.monorepo.tool} (${project.monorepo.workspaces.length} workspaces)`);
|
|
193
106
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"src/app.ts", "src/app.tsx", "src/server.ts",
|
|
200
|
-
"app/page.tsx", "pages/index.tsx",
|
|
201
|
-
"index.ts", "index.tsx", "main.ts", "main.py",
|
|
202
|
-
"src/lib.rs", "src/main.rs",
|
|
203
|
-
"cmd/main.go", "main.go",
|
|
204
|
-
"app.rb", "config.ru",
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
for (const candidate of candidates) {
|
|
208
|
-
if (existsSync(join(dir, candidate))) {
|
|
209
|
-
entries.push(candidate);
|
|
210
|
-
if (entries.length >= 2) break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
107
|
+
if (project.frameworks.length > 0) {
|
|
108
|
+
console.log(` Frameworks: ${project.frameworks.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
if (project.tooling.length > 0) {
|
|
111
|
+
console.log(` Tooling: ${project.tooling.join(", ")}`);
|
|
213
112
|
}
|
|
214
|
-
|
|
215
|
-
return entries.slice(0, 3); // Max 3
|
|
216
113
|
}
|
|
@@ -1,76 +1,351 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
4
|
export interface ProjectInfo {
|
|
5
|
-
type: ProjectType;
|
|
6
5
|
name: string;
|
|
6
|
+
description?: string;
|
|
7
7
|
root: string;
|
|
8
|
+
runtime: Runtime;
|
|
9
|
+
monorepo?: MonorepoInfo;
|
|
10
|
+
frameworks: string[];
|
|
11
|
+
tooling: string[];
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
node: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
24
|
-
deno: ["deno.json", "deno.jsonc"],
|
|
25
|
-
rust: ["Cargo.toml"],
|
|
26
|
-
python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
|
|
27
|
-
go: ["go.mod"],
|
|
28
|
-
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
29
|
-
ruby: ["Gemfile"],
|
|
30
|
-
unknown: [],
|
|
31
|
-
};
|
|
14
|
+
export interface MonorepoInfo {
|
|
15
|
+
tool: string;
|
|
16
|
+
workspaces: WorkspaceInfo[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkspaceInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
type?: string; // "app" | "package" | "service" | etc.
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Runtime = "bun" | "node" | "deno" | "rust" | "python" | "go" | "java" | "ruby" | "unknown";
|
|
32
27
|
|
|
33
28
|
/**
|
|
34
|
-
* Detect project
|
|
29
|
+
* Detect project info from the given directory
|
|
35
30
|
*/
|
|
36
31
|
export async function detectProject(dir: string): Promise<ProjectInfo> {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
const pkg = readPackageJson(dir);
|
|
33
|
+
|
|
34
|
+
// Check for monorepo first
|
|
35
|
+
const monorepo = detectMonorepo(dir, pkg);
|
|
36
|
+
|
|
37
|
+
// Collect all frameworks and tooling (including from workspaces)
|
|
38
|
+
let frameworks = detectFrameworks(dir, pkg);
|
|
39
|
+
let tooling = detectTooling(dir, pkg);
|
|
40
|
+
|
|
41
|
+
// If monorepo, also scan workspaces for frameworks
|
|
42
|
+
if (monorepo) {
|
|
43
|
+
for (const ws of monorepo.workspaces) {
|
|
44
|
+
const wsPath = join(dir, ws.path);
|
|
45
|
+
const wsPkg = readPackageJson(wsPath);
|
|
46
|
+
if (wsPkg) {
|
|
47
|
+
frameworks = [...frameworks, ...detectFrameworks(wsPath, wsPkg)];
|
|
48
|
+
// Only add key tooling from workspaces, not everything
|
|
49
|
+
const wsTooling = detectFrameworks(wsPath, wsPkg);
|
|
50
|
+
tooling = [...tooling, ...wsTooling.filter(t =>
|
|
51
|
+
["Tauri", "Electron", "React Native", "Expo"].includes(t)
|
|
52
|
+
)];
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
root,
|
|
57
|
+
const info: ProjectInfo = {
|
|
58
|
+
name: pkg?.name || detectProjectName(dir),
|
|
59
|
+
description: pkg?.description || extractReadmeDescription(dir),
|
|
60
|
+
root: dir,
|
|
61
|
+
runtime: detectRuntime(dir),
|
|
62
|
+
frameworks: [...new Set(frameworks)],
|
|
63
|
+
tooling: [...new Set(tooling)],
|
|
57
64
|
};
|
|
65
|
+
|
|
66
|
+
if (monorepo) {
|
|
67
|
+
info.monorepo = monorepo;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return info;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
function readPackageJson(dir: string): any | null {
|
|
74
|
+
const pkgPath = join(dir, "package.json");
|
|
75
|
+
if (!existsSync(pkgPath)) return null;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectRuntime(dir: string): Runtime {
|
|
84
|
+
// Order matters - more specific first
|
|
85
|
+
if (existsSync(join(dir, "bun.lock")) || existsSync(join(dir, "bunfig.toml"))) return "bun";
|
|
86
|
+
if (existsSync(join(dir, "deno.json")) || existsSync(join(dir, "deno.jsonc"))) return "deno";
|
|
87
|
+
if (existsSync(join(dir, "Cargo.toml"))) return "rust";
|
|
88
|
+
if (existsSync(join(dir, "go.mod"))) return "go";
|
|
89
|
+
if (existsSync(join(dir, "pyproject.toml")) || existsSync(join(dir, "requirements.txt"))) return "python";
|
|
90
|
+
if (existsSync(join(dir, "Gemfile"))) return "ruby";
|
|
91
|
+
if (existsSync(join(dir, "pom.xml")) || existsSync(join(dir, "build.gradle"))) return "java";
|
|
92
|
+
if (existsSync(join(dir, "package.json"))) return "node";
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectMonorepo(dir: string, pkg: any): MonorepoInfo | undefined {
|
|
97
|
+
let tool: string | undefined;
|
|
98
|
+
let workspacePatterns: string[] = [];
|
|
99
|
+
|
|
100
|
+
// Detect monorepo tool
|
|
101
|
+
if (existsSync(join(dir, "turbo.json"))) {
|
|
102
|
+
tool = "Turborepo";
|
|
103
|
+
} else if (existsSync(join(dir, "nx.json"))) {
|
|
104
|
+
tool = "Nx";
|
|
105
|
+
} else if (existsSync(join(dir, "lerna.json"))) {
|
|
106
|
+
tool = "Lerna";
|
|
107
|
+
} else if (existsSync(join(dir, "pnpm-workspace.yaml"))) {
|
|
108
|
+
tool = "pnpm workspaces";
|
|
109
|
+
workspacePatterns = parsePnpmWorkspaces(dir);
|
|
68
110
|
}
|
|
69
111
|
|
|
112
|
+
// Get workspace patterns from package.json
|
|
113
|
+
if (pkg?.workspaces) {
|
|
114
|
+
const ws = pkg.workspaces;
|
|
115
|
+
workspacePatterns = Array.isArray(ws) ? ws : ws.packages || [];
|
|
116
|
+
|
|
117
|
+
if (!tool) {
|
|
118
|
+
// Detect workspace tool from lockfile
|
|
119
|
+
if (existsSync(join(dir, "bun.lock"))) tool = "Bun workspaces";
|
|
120
|
+
else if (existsSync(join(dir, "pnpm-lock.yaml"))) tool = "pnpm workspaces";
|
|
121
|
+
else if (existsSync(join(dir, "yarn.lock"))) tool = "Yarn workspaces";
|
|
122
|
+
else tool = "npm workspaces";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!tool || workspacePatterns.length === 0) return undefined;
|
|
127
|
+
|
|
128
|
+
// Resolve workspaces
|
|
129
|
+
const workspaces = resolveWorkspaces(dir, workspacePatterns);
|
|
130
|
+
if (workspaces.length === 0) return undefined;
|
|
131
|
+
|
|
132
|
+
return { tool, workspaces };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parsePnpmWorkspaces(dir: string): string[] {
|
|
136
|
+
const wsPath = join(dir, "pnpm-workspace.yaml");
|
|
137
|
+
if (!existsSync(wsPath)) return [];
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(wsPath, "utf-8");
|
|
140
|
+
const match = content.match(/packages:\s*\n((?:\s*-\s*.+\n?)+)/);
|
|
141
|
+
if (match) {
|
|
142
|
+
return match[1].split("\n")
|
|
143
|
+
.map(line => line.replace(/^\s*-\s*['"]?|['"]?\s*$/g, ""))
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveWorkspaces(dir: string, patterns: string[]): WorkspaceInfo[] {
|
|
151
|
+
const workspaces: WorkspaceInfo[] = [];
|
|
152
|
+
|
|
153
|
+
for (const pattern of patterns) {
|
|
154
|
+
// Handle glob patterns like "apps/*", "packages/*"
|
|
155
|
+
if (pattern.endsWith("/*")) {
|
|
156
|
+
const baseDir = pattern.slice(0, -2);
|
|
157
|
+
const fullPath = join(dir, baseDir);
|
|
158
|
+
if (existsSync(fullPath)) {
|
|
159
|
+
try {
|
|
160
|
+
const entries = readdirSync(fullPath, { withFileTypes: true });
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
163
|
+
const wsPath = join(fullPath, entry.name);
|
|
164
|
+
const wsPkg = readPackageJson(wsPath);
|
|
165
|
+
workspaces.push({
|
|
166
|
+
name: wsPkg?.name || entry.name,
|
|
167
|
+
path: `${baseDir}/${entry.name}`,
|
|
168
|
+
description: wsPkg?.description,
|
|
169
|
+
type: inferWorkspaceType(baseDir, entry.name, wsPkg),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// Direct path
|
|
177
|
+
const wsPath = join(dir, pattern);
|
|
178
|
+
if (existsSync(wsPath)) {
|
|
179
|
+
const wsPkg = readPackageJson(wsPath);
|
|
180
|
+
workspaces.push({
|
|
181
|
+
name: wsPkg?.name || pattern.split("/").pop() || pattern,
|
|
182
|
+
path: pattern,
|
|
183
|
+
description: wsPkg?.description,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return workspaces;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function inferWorkspaceType(baseDir: string, name: string, pkg: any): string | undefined {
|
|
193
|
+
// From directory name
|
|
194
|
+
if (baseDir === "apps" || baseDir === "applications") return "app";
|
|
195
|
+
if (baseDir === "packages" || baseDir === "libs") return "package";
|
|
196
|
+
if (baseDir === "services") return "service";
|
|
197
|
+
if (baseDir === "tools" || baseDir === "tooling") return "tool";
|
|
198
|
+
|
|
199
|
+
// From package.json hints
|
|
200
|
+
if (pkg?.bin) return "cli";
|
|
201
|
+
if (pkg?.main?.includes("server") || pkg?.name?.includes("server")) return "server";
|
|
202
|
+
if (pkg?.name?.includes("client") || pkg?.dependencies?.react) return "app";
|
|
203
|
+
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function detectFrameworks(dir: string, pkg: any): string[] {
|
|
208
|
+
const frameworks: string[] = [];
|
|
209
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
210
|
+
|
|
211
|
+
// Frontend
|
|
212
|
+
if (deps?.react) frameworks.push("React");
|
|
213
|
+
if (deps?.vue) frameworks.push("Vue");
|
|
214
|
+
if (deps?.svelte) frameworks.push("Svelte");
|
|
215
|
+
if (deps?.angular || deps?.["@angular/core"]) frameworks.push("Angular");
|
|
216
|
+
if (deps?.solid || deps?.["solid-js"]) frameworks.push("Solid");
|
|
217
|
+
|
|
218
|
+
// Meta-frameworks
|
|
219
|
+
if (deps?.next) frameworks.push("Next.js");
|
|
220
|
+
if (deps?.nuxt) frameworks.push("Nuxt");
|
|
221
|
+
if (deps?.astro) frameworks.push("Astro");
|
|
222
|
+
if (deps?.remix || deps?.["@remix-run/node"]) frameworks.push("Remix");
|
|
223
|
+
|
|
224
|
+
// Backend
|
|
225
|
+
if (deps?.express) frameworks.push("Express");
|
|
226
|
+
if (deps?.fastify) frameworks.push("Fastify");
|
|
227
|
+
if (deps?.hono) frameworks.push("Hono");
|
|
228
|
+
if (deps?.elysia) frameworks.push("Elysia");
|
|
229
|
+
if (deps?.["@nestjs/core"]) frameworks.push("NestJS");
|
|
230
|
+
|
|
231
|
+
// Desktop/Mobile
|
|
232
|
+
if (existsSync(join(dir, "tauri.conf.json")) || existsSync(join(dir, "src-tauri"))) {
|
|
233
|
+
frameworks.push("Tauri");
|
|
234
|
+
}
|
|
235
|
+
if (deps?.electron) frameworks.push("Electron");
|
|
236
|
+
if (deps?.["react-native"]) frameworks.push("React Native");
|
|
237
|
+
if (deps?.expo) frameworks.push("Expo");
|
|
238
|
+
|
|
239
|
+
// AI/ML
|
|
240
|
+
if (deps?.["@anthropic-ai/sdk"] || deps?.["@anthropic-ai/claude-agent-sdk"]) {
|
|
241
|
+
frameworks.push("Claude SDK");
|
|
242
|
+
}
|
|
243
|
+
if (deps?.openai) frameworks.push("OpenAI");
|
|
244
|
+
if (deps?.langchain || deps?.["@langchain/core"]) frameworks.push("LangChain");
|
|
245
|
+
|
|
246
|
+
// Rust detection
|
|
247
|
+
const cargoPath = join(dir, "Cargo.toml");
|
|
248
|
+
if (existsSync(cargoPath)) {
|
|
249
|
+
const cargo = readFileSync(cargoPath, "utf-8");
|
|
250
|
+
if (cargo.includes("tauri")) frameworks.push("Tauri");
|
|
251
|
+
if (cargo.includes("actix")) frameworks.push("Actix");
|
|
252
|
+
if (cargo.includes("axum")) frameworks.push("Axum");
|
|
253
|
+
if (cargo.includes("rocket")) frameworks.push("Rocket");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [...new Set(frameworks)]; // dedupe
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function detectTooling(dir: string, pkg: any): string[] {
|
|
260
|
+
const tools: string[] = [];
|
|
261
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
262
|
+
|
|
263
|
+
// Build tools
|
|
264
|
+
if (existsSync(join(dir, "turbo.json"))) tools.push("Turborepo");
|
|
265
|
+
if (existsSync(join(dir, "nx.json"))) tools.push("Nx");
|
|
266
|
+
if (deps?.vite) tools.push("Vite");
|
|
267
|
+
if (deps?.webpack) tools.push("Webpack");
|
|
268
|
+
if (deps?.esbuild) tools.push("esbuild");
|
|
269
|
+
if (deps?.rollup) tools.push("Rollup");
|
|
270
|
+
|
|
271
|
+
// Linting/Formatting
|
|
272
|
+
if (deps?.["@biomejs/biome"] || existsSync(join(dir, "biome.json"))) tools.push("Biome");
|
|
273
|
+
if (deps?.eslint || existsSync(join(dir, ".eslintrc.json"))) tools.push("ESLint");
|
|
274
|
+
if (deps?.prettier || existsSync(join(dir, ".prettierrc"))) tools.push("Prettier");
|
|
275
|
+
|
|
276
|
+
// Testing
|
|
277
|
+
if (deps?.vitest) tools.push("Vitest");
|
|
278
|
+
if (deps?.jest) tools.push("Jest");
|
|
279
|
+
if (deps?.playwright || deps?.["@playwright/test"]) tools.push("Playwright");
|
|
280
|
+
if (deps?.cypress) tools.push("Cypress");
|
|
281
|
+
|
|
282
|
+
// Database/ORM
|
|
283
|
+
if (deps?.prisma || deps?.["@prisma/client"]) tools.push("Prisma");
|
|
284
|
+
if (deps?.drizzle || deps?.["drizzle-orm"]) tools.push("Drizzle");
|
|
285
|
+
if (deps?.typeorm) tools.push("TypeORM");
|
|
286
|
+
if (deps?.mongoose) tools.push("Mongoose");
|
|
287
|
+
|
|
288
|
+
// Other
|
|
289
|
+
if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "Dockerfile"))) {
|
|
290
|
+
tools.push("Docker");
|
|
291
|
+
}
|
|
292
|
+
if (deps?.tailwindcss || existsSync(join(dir, "tailwind.config.js"))) tools.push("Tailwind");
|
|
293
|
+
if (deps?.typescript || existsSync(join(dir, "tsconfig.json"))) tools.push("TypeScript");
|
|
294
|
+
|
|
295
|
+
return [...new Set(tools)];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function extractReadmeDescription(dir: string): string | undefined {
|
|
299
|
+
const readmePath = join(dir, "README.md");
|
|
300
|
+
if (!existsSync(readmePath)) return undefined;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const content = readFileSync(readmePath, "utf-8");
|
|
304
|
+
|
|
305
|
+
// Try to find a tagline/description pattern (often in bold after title)
|
|
306
|
+
// Match patterns like "**The Agentic Development Environment**"
|
|
307
|
+
const taglineMatch = content.match(/\*\*([^*]{10,100})\*\*/);
|
|
308
|
+
if (taglineMatch && !taglineMatch[1].includes("http") && !taglineMatch[1].includes("badge")) {
|
|
309
|
+
return taglineMatch[1].trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Try Overview/About section
|
|
313
|
+
const overviewMatch = content.match(/##\s*(?:Overview|About|Description)\s*\n+([^\n#]+)/i);
|
|
314
|
+
if (overviewMatch) {
|
|
315
|
+
return cleanMarkdown(overviewMatch[1]).slice(0, 200);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fall back to first paragraph
|
|
319
|
+
const lines = content.split("\n");
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
const trimmed = line.trim();
|
|
322
|
+
// Skip headings, badges, empty lines, HTML
|
|
323
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[") ||
|
|
324
|
+
trimmed.startsWith("!") || trimmed.startsWith("<") ||
|
|
325
|
+
trimmed.startsWith("*") || trimmed.startsWith("-") ||
|
|
326
|
+
trimmed.startsWith("|") || trimmed.includes("badge")) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
return cleanMarkdown(trimmed).slice(0, 200);
|
|
330
|
+
}
|
|
331
|
+
} catch {}
|
|
332
|
+
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cleanMarkdown(text: string): string {
|
|
337
|
+
return text
|
|
338
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1") // bold
|
|
339
|
+
.replace(/\*([^*]+)\*/g, "$1") // italic
|
|
340
|
+
.replace(/`([^`]+)`/g, "$1") // code
|
|
341
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links
|
|
342
|
+
.trim();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function detectProjectName(root: string): string {
|
|
70
346
|
// Try Cargo.toml
|
|
71
347
|
const cargoPath = join(root, "Cargo.toml");
|
|
72
348
|
if (existsSync(cargoPath)) {
|
|
73
|
-
// Simple parse - just look for name =
|
|
74
349
|
try {
|
|
75
350
|
const content = readFileSync(cargoPath, "utf-8");
|
|
76
351
|
const match = content.match(/name\s*=\s*"([^"]+)"/);
|
package/CLAUDE.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
-
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Default to using Bun instead of Node.js.
|
|
8
|
-
|
|
9
|
-
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
-
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
15
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
16
|
-
|
|
17
|
-
## APIs
|
|
18
|
-
|
|
19
|
-
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
20
|
-
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
21
|
-
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
22
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
23
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
24
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
25
|
-
- Bun.$`ls` instead of execa.
|
|
26
|
-
|
|
27
|
-
## Testing
|
|
28
|
-
|
|
29
|
-
Use `bun test` to run tests.
|
|
30
|
-
|
|
31
|
-
```ts#index.test.ts
|
|
32
|
-
import { test, expect } from "bun:test";
|
|
33
|
-
|
|
34
|
-
test("hello world", () => {
|
|
35
|
-
expect(1).toBe(1);
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Frontend
|
|
40
|
-
|
|
41
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
42
|
-
|
|
43
|
-
Server:
|
|
44
|
-
|
|
45
|
-
```ts#index.ts
|
|
46
|
-
import index from "./index.html"
|
|
47
|
-
|
|
48
|
-
Bun.serve({
|
|
49
|
-
routes: {
|
|
50
|
-
"/": index,
|
|
51
|
-
"/api/users/:id": {
|
|
52
|
-
GET: (req) => {
|
|
53
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
// optional websocket support
|
|
58
|
-
websocket: {
|
|
59
|
-
open: (ws) => {
|
|
60
|
-
ws.send("Hello, world!");
|
|
61
|
-
},
|
|
62
|
-
message: (ws, message) => {
|
|
63
|
-
ws.send(message);
|
|
64
|
-
},
|
|
65
|
-
close: (ws) => {
|
|
66
|
-
// handle close
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
development: {
|
|
70
|
-
hmr: true,
|
|
71
|
-
console: true,
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
77
|
-
|
|
78
|
-
```html#index.html
|
|
79
|
-
<html>
|
|
80
|
-
<body>
|
|
81
|
-
<h1>Hello, world!</h1>
|
|
82
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
83
|
-
</body>
|
|
84
|
-
</html>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
With the following `frontend.tsx`:
|
|
88
|
-
|
|
89
|
-
```tsx#frontend.tsx
|
|
90
|
-
import React from "react";
|
|
91
|
-
import { createRoot } from "react-dom/client";
|
|
92
|
-
|
|
93
|
-
// import .css files directly and it works
|
|
94
|
-
import './index.css';
|
|
95
|
-
|
|
96
|
-
const root = createRoot(document.body);
|
|
97
|
-
|
|
98
|
-
export default function Frontend() {
|
|
99
|
-
return <h1>Hello, world!</h1>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
root.render(<Frontend />);
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Then, run index.ts
|
|
106
|
-
|
|
107
|
-
```sh
|
|
108
|
-
bun --hot ./index.ts
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|