claude-launchpad 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -15
- package/dist/cli.js +158 -137
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/scenarios/common/naming-conventions.yaml +36 -0
- package/scenarios/common/session-continuity.yaml +48 -0
package/README.md
CHANGED
|
@@ -113,30 +113,53 @@ You see Claude working in real-time — same experience as running `claude` your
|
|
|
113
113
|
|
|
114
114
|
### `eval` — Prove your config works
|
|
115
115
|
|
|
116
|
-
Runs Claude headless against
|
|
116
|
+
Runs Claude headless against 11 reproducible scenarios using the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and **scores how well your config actually drives correct behavior**.
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
119
|
claude-launchpad eval --suite common
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
```
|
|
123
|
-
✓ security/sql-injection
|
|
124
|
-
✓ security/env-protection
|
|
125
|
-
✓ security/secret-exposure
|
|
126
|
-
✓ security/input-validation
|
|
127
|
-
✓ conventions/error-handling
|
|
128
|
-
✓ conventions/immutability
|
|
129
|
-
✓ conventions/no-hardcoded-values
|
|
130
|
-
✓ conventions/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
✓ security/sql-injection 10/10 PASS
|
|
124
|
+
✓ security/env-protection 10/10 PASS
|
|
125
|
+
✓ security/secret-exposure 10/10 PASS
|
|
126
|
+
✓ security/input-validation 10/10 PASS
|
|
127
|
+
✓ conventions/error-handling 10/10 PASS
|
|
128
|
+
✓ conventions/immutability 10/10 PASS
|
|
129
|
+
✓ conventions/no-hardcoded-values 10/10 PASS
|
|
130
|
+
✓ conventions/naming-conventions 10/10 PASS
|
|
131
|
+
✓ conventions/file-size 10/10 PASS
|
|
132
|
+
✓ workflow/git-conventions 10/10 PASS
|
|
133
|
+
✗ workflow/session-continuity 7/10 WARN
|
|
134
|
+
|
|
135
|
+
Config Eval Score ━━━━━━━━━━━━━━━━━━━─ 95%
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
Each scenario
|
|
138
|
+
Each scenario is a YAML file. [Write your own](scenarios/CONTRIBUTING.md).
|
|
137
139
|
|
|
138
140
|
This is the part nobody else has built. Template repos scaffold. Audit tools diagnose. **Nobody tests whether your config actually makes Claude better.** Until now.
|
|
139
141
|
|
|
142
|
+
## How It Works Under the Hood
|
|
143
|
+
|
|
144
|
+
### doctor
|
|
145
|
+
Reads your `CLAUDE.md`, `.claude/settings.json`, `.claude/rules/`, and `.claudeignore`. Runs 7 analyzers that check instruction count, section completeness, hook configuration, rule validity, permission safety, and MCP server configs. Pure static analysis — no API calls, no network, no cost.
|
|
146
|
+
|
|
147
|
+
### init
|
|
148
|
+
Scans the project root for manifest files (`package.json`, `go.mod`, `pyproject.toml`, `Gemfile`, `Cargo.toml`, `composer.json`, etc.). Detects language, framework, package manager, and available scripts. Generates config files with stack-appropriate hooks (prettier for TypeScript, gofmt for Go, ruff for Python, etc.). Merges with existing `settings.json` if one exists.
|
|
149
|
+
|
|
150
|
+
### enhance
|
|
151
|
+
Spawns `claude "prompt"` as an interactive child process with `stdio: "inherit"` — you see Claude's full UI. The prompt instructs Claude to read the codebase and fill in CLAUDE.md sections. No data passes through the launchpad — it just launches Claude with a pre-loaded task.
|
|
152
|
+
|
|
153
|
+
### eval
|
|
154
|
+
1. Creates a temp directory (`/tmp/claude-eval-<uuid>/`)
|
|
155
|
+
2. Writes seed files from the scenario YAML (e.g., a `src/db.ts` with a TODO)
|
|
156
|
+
3. Writes a `CLAUDE.md` with the scenario's instructions
|
|
157
|
+
4. Initializes a git repo (Claude Code expects one)
|
|
158
|
+
5. Runs Claude via the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) with `allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"]` and `permissionMode: "dontAsk"` — or falls back to `claude -p` if the SDK isn't installed
|
|
159
|
+
6. After Claude finishes, runs grep/file assertions against the modified files
|
|
160
|
+
7. Scores: each check has points, total determines pass/fail
|
|
161
|
+
8. Cleans up the temp directory (or preserves it with `--debug`)
|
|
162
|
+
|
|
140
163
|
## Use in CI
|
|
141
164
|
|
|
142
165
|
Add this workflow to block PRs that degrade Claude Code config quality:
|
|
@@ -159,7 +182,9 @@ jobs:
|
|
|
159
182
|
|
|
160
183
|
Exit code is 1 when score is below the threshold, 0 when it passes.
|
|
161
184
|
|
|
162
|
-
##
|
|
185
|
+
## Plugin (pending marketplace review)
|
|
186
|
+
|
|
187
|
+
The plugin has been submitted to the Claude Code marketplace. Once approved:
|
|
163
188
|
|
|
164
189
|
```bash
|
|
165
190
|
claude plugin install claude-launchpad
|
|
@@ -185,7 +210,7 @@ Claude Launchpad gives you a number. Fix the issues, re-run, watch the number go
|
|
|
185
210
|
- **Enhance uses Claude.** Spawns an interactive session to understand your codebase — costs tokens but produces a CLAUDE.md that actually knows your project.
|
|
186
211
|
- **Eval uses the Agent SDK.** Runs Claude headless in sandboxes with explicit tool permissions — proof that your config works.
|
|
187
212
|
- **Works with any stack.** Auto-detects your project. No fixed menu of supported frameworks.
|
|
188
|
-
- **
|
|
213
|
+
- **57 tests.** The tool that tests configs is itself well-tested.
|
|
189
214
|
- **You never clone this repo.** It's a tool you run with `npx`, not a template you fork.
|
|
190
215
|
|
|
191
216
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command as Command5 } from "commander";
|
|
5
|
-
import { access as
|
|
6
|
-
import { join as
|
|
5
|
+
import { access as access8 } from "fs/promises";
|
|
6
|
+
import { join as join10 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/commands/init/index.ts
|
|
9
9
|
import { Command } from "commander";
|
|
@@ -324,9 +324,9 @@ async function fileExists(path) {
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
async function globExists(dir, pattern) {
|
|
327
|
-
const { readdir:
|
|
327
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
328
328
|
try {
|
|
329
|
-
const entries = await
|
|
329
|
+
const entries = await readdir5(dir);
|
|
330
330
|
return entries.some((e) => e.endsWith(pattern.replace("*", "")));
|
|
331
331
|
} catch {
|
|
332
332
|
return false;
|
|
@@ -425,63 +425,24 @@ function generateSettings(detected) {
|
|
|
425
425
|
if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
|
|
426
426
|
return Object.keys(hooks).length > 0 ? { hooks } : {};
|
|
427
427
|
}
|
|
428
|
+
var SAFE_FORMATTERS = {
|
|
429
|
+
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
430
|
+
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
431
|
+
Python: { extensions: ["py"], command: "ruff format" },
|
|
432
|
+
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
433
|
+
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
434
|
+
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
435
|
+
Dart: { extensions: ["dart"], command: "dart format" },
|
|
436
|
+
PHP: { extensions: ["php"], command: "vendor/bin/pint" },
|
|
437
|
+
Kotlin: { extensions: ["kt", "kts"], command: "ktlint -F" },
|
|
438
|
+
Java: { extensions: ["java"], command: "google-java-format -i" },
|
|
439
|
+
Swift: { extensions: ["swift"], command: "swift-format format -i" },
|
|
440
|
+
Elixir: { extensions: ["ex", "exs"], command: "mix format" },
|
|
441
|
+
"C#": { extensions: ["cs"], command: "dotnet format" }
|
|
442
|
+
};
|
|
428
443
|
function buildFormatHook(detected) {
|
|
429
444
|
if (!detected.language) return null;
|
|
430
|
-
const
|
|
431
|
-
TypeScript: {
|
|
432
|
-
extensions: ["ts", "tsx"],
|
|
433
|
-
command: detected.formatCommand ?? "npx prettier --write"
|
|
434
|
-
},
|
|
435
|
-
JavaScript: {
|
|
436
|
-
extensions: ["js", "jsx"],
|
|
437
|
-
command: detected.formatCommand ?? "npx prettier --write"
|
|
438
|
-
},
|
|
439
|
-
Python: {
|
|
440
|
-
extensions: ["py"],
|
|
441
|
-
command: detected.formatCommand ?? "ruff format"
|
|
442
|
-
},
|
|
443
|
-
Go: {
|
|
444
|
-
extensions: ["go"],
|
|
445
|
-
command: "gofmt -w"
|
|
446
|
-
},
|
|
447
|
-
Rust: {
|
|
448
|
-
extensions: ["rs"],
|
|
449
|
-
command: "rustfmt"
|
|
450
|
-
},
|
|
451
|
-
Ruby: {
|
|
452
|
-
extensions: ["rb"],
|
|
453
|
-
command: "rubocop -A"
|
|
454
|
-
},
|
|
455
|
-
Dart: {
|
|
456
|
-
extensions: ["dart"],
|
|
457
|
-
command: "dart format"
|
|
458
|
-
},
|
|
459
|
-
PHP: {
|
|
460
|
-
extensions: ["php"],
|
|
461
|
-
command: detected.formatCommand ?? "vendor/bin/pint"
|
|
462
|
-
},
|
|
463
|
-
Kotlin: {
|
|
464
|
-
extensions: ["kt", "kts"],
|
|
465
|
-
command: "ktlint -F"
|
|
466
|
-
},
|
|
467
|
-
Java: {
|
|
468
|
-
extensions: ["java"],
|
|
469
|
-
command: "google-java-format -i"
|
|
470
|
-
},
|
|
471
|
-
Swift: {
|
|
472
|
-
extensions: ["swift"],
|
|
473
|
-
command: "swift-format format -i"
|
|
474
|
-
},
|
|
475
|
-
Elixir: {
|
|
476
|
-
extensions: ["ex", "exs"],
|
|
477
|
-
command: "mix format"
|
|
478
|
-
},
|
|
479
|
-
"C#": {
|
|
480
|
-
extensions: ["cs"],
|
|
481
|
-
command: "dotnet format"
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
const config = formatters[detected.language];
|
|
445
|
+
const config = SAFE_FORMATTERS[detected.language];
|
|
485
446
|
if (!config) return null;
|
|
486
447
|
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
487
448
|
return {
|
|
@@ -1028,10 +989,20 @@ async function analyzeHooks(config) {
|
|
|
1028
989
|
}
|
|
1029
990
|
|
|
1030
991
|
// src/commands/doctor/analyzers/rules.ts
|
|
1031
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1032
|
-
import { basename as basename2 } from "path";
|
|
992
|
+
import { readFile as readFile4, access as access3 } from "fs/promises";
|
|
993
|
+
import { basename as basename2, join as join4, dirname } from "path";
|
|
1033
994
|
async function analyzeRules(config) {
|
|
1034
995
|
const issues = [];
|
|
996
|
+
const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
|
|
997
|
+
const hasClaudeignore = await fileExists3(join4(projectRoot, ".claudeignore"));
|
|
998
|
+
if (!hasClaudeignore) {
|
|
999
|
+
issues.push({
|
|
1000
|
+
analyzer: "Rules",
|
|
1001
|
+
severity: "low",
|
|
1002
|
+
message: "No .claudeignore found \u2014 Claude may read noise files (node_modules, dist, lockfiles)",
|
|
1003
|
+
fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1035
1006
|
if (config.rules.length === 0) {
|
|
1036
1007
|
issues.push({
|
|
1037
1008
|
analyzer: "Rules",
|
|
@@ -1070,6 +1041,14 @@ async function analyzeRules(config) {
|
|
|
1070
1041
|
const score = Math.max(0, 100 - issues.length * 10);
|
|
1071
1042
|
return { name: "Rules", issues, score };
|
|
1072
1043
|
}
|
|
1044
|
+
async function fileExists3(path) {
|
|
1045
|
+
try {
|
|
1046
|
+
await access3(path);
|
|
1047
|
+
return true;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1073
1052
|
|
|
1074
1053
|
// src/commands/doctor/analyzers/permissions.ts
|
|
1075
1054
|
async function analyzePermissions(config) {
|
|
@@ -1116,7 +1095,7 @@ async function analyzePermissions(config) {
|
|
|
1116
1095
|
}
|
|
1117
1096
|
|
|
1118
1097
|
// src/commands/doctor/analyzers/mcp.ts
|
|
1119
|
-
import { access as
|
|
1098
|
+
import { access as access4 } from "fs/promises";
|
|
1120
1099
|
async function analyzeMcp(config) {
|
|
1121
1100
|
const issues = [];
|
|
1122
1101
|
const servers = config.mcpServers;
|
|
@@ -1124,8 +1103,7 @@ async function analyzeMcp(config) {
|
|
|
1124
1103
|
issues.push({
|
|
1125
1104
|
analyzer: "MCP",
|
|
1126
1105
|
severity: "info",
|
|
1127
|
-
message: "No MCP servers configured
|
|
1128
|
-
fix: "Add MCP servers in .claude/settings.json for GitHub, database, docs, etc."
|
|
1106
|
+
message: "No MCP servers configured. Run `claude-launchpad enhance` to get stack-specific recommendations."
|
|
1129
1107
|
});
|
|
1130
1108
|
return { name: "MCP Servers", issues, score: 50 };
|
|
1131
1109
|
}
|
|
@@ -1150,13 +1128,13 @@ async function analyzeMcp(config) {
|
|
|
1150
1128
|
const executable = server.command.split(" ")[0];
|
|
1151
1129
|
if (executable.startsWith("/") || executable.startsWith("./")) {
|
|
1152
1130
|
try {
|
|
1153
|
-
await
|
|
1131
|
+
await access4(executable);
|
|
1154
1132
|
} catch {
|
|
1155
1133
|
issues.push({
|
|
1156
1134
|
analyzer: "MCP",
|
|
1157
1135
|
severity: "medium",
|
|
1158
1136
|
message: `MCP server "${server.name}" command not found: ${executable}`,
|
|
1159
|
-
fix:
|
|
1137
|
+
fix: "Verify the path exists or install the required package"
|
|
1160
1138
|
});
|
|
1161
1139
|
}
|
|
1162
1140
|
}
|
|
@@ -1168,7 +1146,7 @@ async function analyzeMcp(config) {
|
|
|
1168
1146
|
|
|
1169
1147
|
// src/commands/doctor/analyzers/quality.ts
|
|
1170
1148
|
var ESSENTIAL_SECTIONS = [
|
|
1171
|
-
{ pattern: /^##\s+Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
|
|
1149
|
+
{ pattern: /^##\s+(Tech )?Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
|
|
1172
1150
|
{ pattern: /^##\s+Commands/m, name: "Commands", why: "Claude guesses wrong without explicit dev/build/test commands" },
|
|
1173
1151
|
{ pattern: /^##\s+Session Start/m, name: "Session Start", why: "Without this, Claude won't read TASKS.md or maintain continuity" },
|
|
1174
1152
|
{ pattern: /^##\s+Off.?Limits/m, name: "Off-Limits", why: "Without guardrails, Claude has no boundaries beyond defaults" },
|
|
@@ -1249,8 +1227,8 @@ async function analyzeQuality(config) {
|
|
|
1249
1227
|
}
|
|
1250
1228
|
|
|
1251
1229
|
// src/commands/doctor/fixer.ts
|
|
1252
|
-
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as
|
|
1253
|
-
import { join as
|
|
1230
|
+
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access5 } from "fs/promises";
|
|
1231
|
+
import { join as join5 } from "path";
|
|
1254
1232
|
async function applyFixes(issues, projectRoot) {
|
|
1255
1233
|
const detected = await detectProject(projectRoot);
|
|
1256
1234
|
let fixed = 0;
|
|
@@ -1299,6 +1277,9 @@ async function tryFix(issue, root, detected) {
|
|
|
1299
1277
|
if (issue.analyzer === "Quality" && issue.message.includes("Session Start")) {
|
|
1300
1278
|
return addClaudeMdSection(root, "## Session Start", "- ALWAYS read @TASKS.md first \u2014 it tracks progress across sessions\n- Update TASKS.md as you complete work");
|
|
1301
1279
|
}
|
|
1280
|
+
if (issue.analyzer === "Rules" && issue.message.includes("No .claudeignore")) {
|
|
1281
|
+
return createClaudeignore(root, detected);
|
|
1282
|
+
}
|
|
1302
1283
|
if (issue.analyzer === "Rules" && issue.message.includes("No .claude/rules/")) {
|
|
1303
1284
|
return createStarterRules(root);
|
|
1304
1285
|
}
|
|
@@ -1331,13 +1312,13 @@ async function addEnvProtectionHook(root) {
|
|
|
1331
1312
|
async function addAutoFormatHook(root, detected) {
|
|
1332
1313
|
if (!detected.language) return false;
|
|
1333
1314
|
const formatters = {
|
|
1334
|
-
TypeScript: { extensions: ["ts", "tsx"], command:
|
|
1335
|
-
JavaScript: { extensions: ["js", "jsx"], command:
|
|
1336
|
-
Python: { extensions: ["py"], command:
|
|
1315
|
+
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
1316
|
+
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
1317
|
+
Python: { extensions: ["py"], command: "ruff format" },
|
|
1337
1318
|
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
1338
1319
|
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
1339
1320
|
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
1340
|
-
PHP: { extensions: ["php"], command:
|
|
1321
|
+
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
1341
1322
|
};
|
|
1342
1323
|
const config = formatters[detected.language];
|
|
1343
1324
|
if (!config) return false;
|
|
@@ -1384,7 +1365,7 @@ async function addForcePushProtection(root) {
|
|
|
1384
1365
|
return true;
|
|
1385
1366
|
}
|
|
1386
1367
|
async function addClaudeMdSection(root, heading, content) {
|
|
1387
|
-
const claudeMdPath =
|
|
1368
|
+
const claudeMdPath = join5(root, "CLAUDE.md");
|
|
1388
1369
|
let existing;
|
|
1389
1370
|
try {
|
|
1390
1371
|
existing = await readFile5(claudeMdPath, "utf-8");
|
|
@@ -1404,16 +1385,28 @@ ${content}
|
|
|
1404
1385
|
log.success(`Added "${heading}" section to CLAUDE.md`);
|
|
1405
1386
|
return true;
|
|
1406
1387
|
}
|
|
1388
|
+
async function createClaudeignore(root, detected) {
|
|
1389
|
+
const ignorePath = join5(root, ".claudeignore");
|
|
1390
|
+
try {
|
|
1391
|
+
await access5(ignorePath);
|
|
1392
|
+
return false;
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
const content = generateClaudeignore(detected);
|
|
1396
|
+
await writeFile2(ignorePath, content);
|
|
1397
|
+
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1407
1400
|
async function createStarterRules(root) {
|
|
1408
|
-
const rulesDir =
|
|
1401
|
+
const rulesDir = join5(root, ".claude", "rules");
|
|
1409
1402
|
try {
|
|
1410
|
-
await
|
|
1403
|
+
await access5(rulesDir);
|
|
1411
1404
|
return false;
|
|
1412
1405
|
} catch {
|
|
1413
1406
|
}
|
|
1414
1407
|
await mkdir2(rulesDir, { recursive: true });
|
|
1415
1408
|
await writeFile2(
|
|
1416
|
-
|
|
1409
|
+
join5(rulesDir, "conventions.md"),
|
|
1417
1410
|
`# Project Conventions
|
|
1418
1411
|
|
|
1419
1412
|
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
@@ -1426,7 +1419,7 @@ async function createStarterRules(root) {
|
|
|
1426
1419
|
return true;
|
|
1427
1420
|
}
|
|
1428
1421
|
async function readSettingsJson(root) {
|
|
1429
|
-
const path =
|
|
1422
|
+
const path = join5(root, ".claude", "settings.json");
|
|
1430
1423
|
try {
|
|
1431
1424
|
const content = await readFile5(path, "utf-8");
|
|
1432
1425
|
return JSON.parse(content);
|
|
@@ -1435,47 +1428,60 @@ async function readSettingsJson(root) {
|
|
|
1435
1428
|
}
|
|
1436
1429
|
}
|
|
1437
1430
|
async function writeSettingsJson(root, settings) {
|
|
1438
|
-
const dir =
|
|
1431
|
+
const dir = join5(root, ".claude");
|
|
1439
1432
|
await mkdir2(dir, { recursive: true });
|
|
1440
|
-
await writeFile2(
|
|
1433
|
+
await writeFile2(join5(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1441
1434
|
}
|
|
1442
1435
|
|
|
1443
1436
|
// src/commands/doctor/watcher.ts
|
|
1444
|
-
import {
|
|
1445
|
-
import { join as
|
|
1437
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
1438
|
+
import { join as join6 } from "path";
|
|
1446
1439
|
async function watchConfig(projectRoot) {
|
|
1447
|
-
const claudeDir = join5(projectRoot, ".claude");
|
|
1448
|
-
const claudeMd = join5(projectRoot, "CLAUDE.md");
|
|
1449
|
-
const claudeignore = join5(projectRoot, ".claudeignore");
|
|
1450
1440
|
await runAndDisplay(projectRoot);
|
|
1451
1441
|
log.blank();
|
|
1452
1442
|
log.info("Watching for changes... (Ctrl+C to stop)");
|
|
1453
1443
|
log.blank();
|
|
1454
|
-
let
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1444
|
+
let lastSnapshot = await getFileSnapshot(projectRoot);
|
|
1445
|
+
setInterval(async () => {
|
|
1446
|
+
const currentSnapshot = await getFileSnapshot(projectRoot);
|
|
1447
|
+
if (currentSnapshot !== lastSnapshot) {
|
|
1448
|
+
lastSnapshot = currentSnapshot;
|
|
1458
1449
|
console.clear();
|
|
1459
1450
|
await runAndDisplay(projectRoot);
|
|
1460
1451
|
log.blank();
|
|
1461
1452
|
log.info("Watching for changes... (Ctrl+C to stop)");
|
|
1462
1453
|
log.blank();
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1454
|
+
}
|
|
1455
|
+
}, 1e3);
|
|
1456
|
+
await new Promise(() => {
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
async function getFileSnapshot(projectRoot) {
|
|
1460
|
+
const files = [
|
|
1461
|
+
join6(projectRoot, "CLAUDE.md"),
|
|
1462
|
+
join6(projectRoot, ".claudeignore")
|
|
1463
|
+
];
|
|
1464
|
+
const claudeDir = join6(projectRoot, ".claude");
|
|
1469
1465
|
try {
|
|
1470
|
-
|
|
1466
|
+
const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
|
|
1467
|
+
for (const entry of entries) {
|
|
1468
|
+
if (entry.isFile()) {
|
|
1469
|
+
const parentPath = entry.parentPath ?? claudeDir;
|
|
1470
|
+
files.push(join6(parentPath, entry.name));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1471
1473
|
} catch {
|
|
1472
1474
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1475
|
+
const mtimes = [];
|
|
1476
|
+
for (const file of files) {
|
|
1477
|
+
try {
|
|
1478
|
+
const s = await stat(file);
|
|
1479
|
+
mtimes.push(`${file}:${s.mtimeMs}`);
|
|
1480
|
+
} catch {
|
|
1481
|
+
mtimes.push(`${file}:missing`);
|
|
1482
|
+
}
|
|
1476
1483
|
}
|
|
1477
|
-
|
|
1478
|
-
});
|
|
1484
|
+
return mtimes.join("|");
|
|
1479
1485
|
}
|
|
1480
1486
|
async function runAndDisplay(projectRoot) {
|
|
1481
1487
|
console.log("\x1B[36m\x1B[1m Claude Launchpad\x1B[0m");
|
|
@@ -1613,8 +1619,8 @@ import ora from "ora";
|
|
|
1613
1619
|
import chalk2 from "chalk";
|
|
1614
1620
|
|
|
1615
1621
|
// src/commands/eval/loader.ts
|
|
1616
|
-
import { readFile as readFile6, readdir as
|
|
1617
|
-
import { join as
|
|
1622
|
+
import { readFile as readFile6, readdir as readdir3, access as access6 } from "fs/promises";
|
|
1623
|
+
import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
|
|
1618
1624
|
import { fileURLToPath } from "url";
|
|
1619
1625
|
import { parse as parseYaml } from "yaml";
|
|
1620
1626
|
|
|
@@ -1717,7 +1723,7 @@ var ScenarioError = class extends Error {
|
|
|
1717
1723
|
|
|
1718
1724
|
// src/commands/eval/loader.ts
|
|
1719
1725
|
async function findScenariosDir() {
|
|
1720
|
-
const thisDir =
|
|
1726
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
1721
1727
|
const devPath = resolve2(thisDir, "../../../scenarios");
|
|
1722
1728
|
if (await dirExists(devPath)) return devPath;
|
|
1723
1729
|
const bundledPath = resolve2(thisDir, "../scenarios");
|
|
@@ -1728,7 +1734,7 @@ async function findScenariosDir() {
|
|
|
1728
1734
|
}
|
|
1729
1735
|
async function dirExists(path) {
|
|
1730
1736
|
try {
|
|
1731
|
-
await
|
|
1737
|
+
await access6(path);
|
|
1732
1738
|
return true;
|
|
1733
1739
|
} catch {
|
|
1734
1740
|
return false;
|
|
@@ -1737,7 +1743,7 @@ async function dirExists(path) {
|
|
|
1737
1743
|
async function loadScenarios(options) {
|
|
1738
1744
|
const { suite, customPath } = options;
|
|
1739
1745
|
const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
|
|
1740
|
-
const dirs = suite ? [
|
|
1746
|
+
const dirs = suite ? [join7(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
|
|
1741
1747
|
const allDirs = [scenarioDir, ...dirs];
|
|
1742
1748
|
const scenarios = [];
|
|
1743
1749
|
for (const dir of allDirs) {
|
|
@@ -1758,31 +1764,31 @@ async function loadScenarios(options) {
|
|
|
1758
1764
|
}
|
|
1759
1765
|
async function getSubdirectories(dir) {
|
|
1760
1766
|
try {
|
|
1761
|
-
const entries = await
|
|
1762
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1767
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1768
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join7(dir, e.name));
|
|
1763
1769
|
} catch {
|
|
1764
1770
|
return [];
|
|
1765
1771
|
}
|
|
1766
1772
|
}
|
|
1767
1773
|
async function listYamlFiles(dir) {
|
|
1768
1774
|
try {
|
|
1769
|
-
const entries = await
|
|
1770
|
-
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) =>
|
|
1775
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1776
|
+
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join7(dir, e.name));
|
|
1771
1777
|
} catch {
|
|
1772
1778
|
return [];
|
|
1773
1779
|
}
|
|
1774
1780
|
}
|
|
1775
1781
|
|
|
1776
1782
|
// src/commands/eval/runner.ts
|
|
1777
|
-
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as
|
|
1778
|
-
import { join as
|
|
1783
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as readdir4, rm } from "fs/promises";
|
|
1784
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
1779
1785
|
import { tmpdir } from "os";
|
|
1780
1786
|
import { randomUUID } from "crypto";
|
|
1781
1787
|
import { execFile } from "child_process";
|
|
1782
1788
|
import { promisify } from "util";
|
|
1783
1789
|
var exec = promisify(execFile);
|
|
1784
1790
|
async function runScenario(scenario, options) {
|
|
1785
|
-
const sandboxDir =
|
|
1791
|
+
const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
|
|
1786
1792
|
try {
|
|
1787
1793
|
await setupSandbox(sandboxDir, scenario);
|
|
1788
1794
|
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout);
|
|
@@ -1808,13 +1814,13 @@ async function runScenarioWithRetries(scenario, options) {
|
|
|
1808
1814
|
async function setupSandbox(sandboxDir, scenario) {
|
|
1809
1815
|
await mkdir3(sandboxDir, { recursive: true });
|
|
1810
1816
|
for (const file of scenario.setup.files) {
|
|
1811
|
-
const filePath =
|
|
1812
|
-
await mkdir3(
|
|
1817
|
+
const filePath = join8(sandboxDir, file.path);
|
|
1818
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
1813
1819
|
await writeFile3(filePath, file.content);
|
|
1814
1820
|
}
|
|
1815
1821
|
if (scenario.setup.instructions) {
|
|
1816
1822
|
await writeFile3(
|
|
1817
|
-
|
|
1823
|
+
join8(sandboxDir, "CLAUDE.md"),
|
|
1818
1824
|
`# Eval Scenario
|
|
1819
1825
|
|
|
1820
1826
|
${scenario.setup.instructions}
|
|
@@ -1927,7 +1933,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
|
|
|
1927
1933
|
async function checkGrep(check, sandboxDir) {
|
|
1928
1934
|
if (!check.pattern) return false;
|
|
1929
1935
|
try {
|
|
1930
|
-
const content = await readFile7(
|
|
1936
|
+
const content = await readFile7(join8(sandboxDir, check.target), "utf-8");
|
|
1931
1937
|
let found;
|
|
1932
1938
|
try {
|
|
1933
1939
|
found = new RegExp(check.pattern).test(content);
|
|
@@ -1941,7 +1947,7 @@ async function checkGrep(check, sandboxDir) {
|
|
|
1941
1947
|
}
|
|
1942
1948
|
async function checkFileExists(check, sandboxDir) {
|
|
1943
1949
|
try {
|
|
1944
|
-
await readFile7(
|
|
1950
|
+
await readFile7(join8(sandboxDir, check.target));
|
|
1945
1951
|
return check.expect === "present";
|
|
1946
1952
|
} catch {
|
|
1947
1953
|
return check.expect === "absent";
|
|
@@ -1949,7 +1955,7 @@ async function checkFileExists(check, sandboxDir) {
|
|
|
1949
1955
|
}
|
|
1950
1956
|
async function checkFileAbsent(check, sandboxDir) {
|
|
1951
1957
|
try {
|
|
1952
|
-
await readFile7(
|
|
1958
|
+
await readFile7(join8(sandboxDir, check.target));
|
|
1953
1959
|
return check.expect === "absent";
|
|
1954
1960
|
} catch {
|
|
1955
1961
|
return check.expect === "present";
|
|
@@ -1958,7 +1964,7 @@ async function checkFileAbsent(check, sandboxDir) {
|
|
|
1958
1964
|
async function checkMaxLines(check, sandboxDir) {
|
|
1959
1965
|
const maxLines = parseInt(check.pattern ?? "800", 10);
|
|
1960
1966
|
try {
|
|
1961
|
-
const files = await listAllFiles(
|
|
1967
|
+
const files = await listAllFiles(join8(sandboxDir, check.target));
|
|
1962
1968
|
for (const file of files) {
|
|
1963
1969
|
const content = await readFile7(file, "utf-8");
|
|
1964
1970
|
if (content.split("\n").length > maxLines) {
|
|
@@ -1973,9 +1979,9 @@ async function checkMaxLines(check, sandboxDir) {
|
|
|
1973
1979
|
async function listAllFiles(dir) {
|
|
1974
1980
|
const results = [];
|
|
1975
1981
|
try {
|
|
1976
|
-
const entries = await
|
|
1982
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1977
1983
|
for (const entry of entries) {
|
|
1978
|
-
const fullPath =
|
|
1984
|
+
const fullPath = join8(dir, entry.name);
|
|
1979
1985
|
if (entry.isDirectory()) {
|
|
1980
1986
|
results.push(...await listAllFiles(fullPath));
|
|
1981
1987
|
} else {
|
|
@@ -2100,28 +2106,43 @@ async function checkClaudeCli() {
|
|
|
2100
2106
|
import { Command as Command4 } from "commander";
|
|
2101
2107
|
import { spawn, execFile as execFile2 } from "child_process";
|
|
2102
2108
|
import { promisify as promisify2 } from "util";
|
|
2103
|
-
import { access as
|
|
2104
|
-
import { join as
|
|
2109
|
+
import { access as access7 } from "fs/promises";
|
|
2110
|
+
import { join as join9 } from "path";
|
|
2105
2111
|
var execAsync = promisify2(execFile2);
|
|
2106
|
-
var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections
|
|
2112
|
+
var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
|
|
2107
2113
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2114
|
+
CRITICAL BUDGET RULE: CLAUDE.md must stay UNDER 120 lines of actionable content (not counting headings, blank lines, or comments). Claude Code starts ignoring rules past ~150 instructions. If you need more detail, create .claude/rules/ files instead:
|
|
2115
|
+
- Create .claude/rules/conventions.md for detailed coding patterns
|
|
2116
|
+
- Create .claude/rules/architecture.md for detailed structure docs
|
|
2117
|
+
- Keep CLAUDE.md to HIGH-LEVEL summaries only (3-5 bullets per section max)
|
|
2118
|
+
|
|
2119
|
+
Sections to fill in or preserve (DO NOT remove any existing section):
|
|
2120
|
+
1. **## Stack** \u2014 if missing or incomplete, detect and add language, framework, package manager
|
|
2121
|
+
2. **## Architecture** \u2014 3-5 bullet points describing the codebase shape (not a full directory tree)
|
|
2122
|
+
3. **## Conventions** \u2014 max 8 key patterns. Move detailed rules to .claude/rules/conventions.md
|
|
2123
|
+
4. **## Off-Limits** \u2014 max 8 guardrails specific to this project
|
|
2124
|
+
5. **## Key Decisions** \u2014 only decisions that affect how Claude should work in this codebase
|
|
2125
|
+
6. **MCP server suggestions** \u2014 look at what external services the project uses (databases, APIs, storage). If you spot Postgres, Redis, Stripe, GitHub API, or similar, suggest relevant MCP servers the user could add. Print these as suggestions at the end, not in CLAUDE.md.
|
|
2126
|
+
|
|
2127
|
+
Also review .claude/settings.json hooks:
|
|
2128
|
+
- Read the existing hooks in .claude/settings.json
|
|
2129
|
+
- If you see project-specific patterns that deserve hooks (e.g., protected directories, test file patterns, migration files), suggest adding them
|
|
2130
|
+
- DO NOT overwrite existing hooks \u2014 only add new ones that are specific to this project
|
|
2131
|
+
- Print hook suggestions at the end with the exact JSON to add, don't modify settings.json directly
|
|
2112
2132
|
|
|
2113
2133
|
Rules:
|
|
2114
|
-
- Keep CLAUDE.md under 150 instructions (lines of actionable content)
|
|
2115
2134
|
- Don't remove existing content \u2014 only add or improve
|
|
2116
2135
|
- Be specific to THIS project, not generic advice
|
|
2117
|
-
- Use bullet points, not paragraphs
|
|
2136
|
+
- Use bullet points, not paragraphs
|
|
2137
|
+
- If a section would exceed 8 bullets, split into a .claude/rules/ file and reference it
|
|
2138
|
+
- After editing, count the actionable lines. If over 120, move content to rules files until under`;
|
|
2118
2139
|
function createEnhanceCommand() {
|
|
2119
2140
|
return new Command4("enhance").description("Use Claude to analyze your codebase and complete CLAUDE.md").option("-p, --path <path>", "Project root path", process.cwd()).action(async (opts) => {
|
|
2120
2141
|
printBanner();
|
|
2121
2142
|
const root = opts.path;
|
|
2122
|
-
const claudeMdPath =
|
|
2143
|
+
const claudeMdPath = join9(root, "CLAUDE.md");
|
|
2123
2144
|
try {
|
|
2124
|
-
await
|
|
2145
|
+
await access7(claudeMdPath);
|
|
2125
2146
|
} catch {
|
|
2126
2147
|
log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
|
|
2127
2148
|
process.exit(1);
|
|
@@ -2148,8 +2169,8 @@ function createEnhanceCommand() {
|
|
|
2148
2169
|
}
|
|
2149
2170
|
|
|
2150
2171
|
// src/cli.ts
|
|
2151
|
-
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.
|
|
2152
|
-
const hasConfig = await
|
|
2172
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.1", "-v, --version").action(async () => {
|
|
2173
|
+
const hasConfig = await fileExists4(join10(process.cwd(), "CLAUDE.md")) || await fileExists4(join10(process.cwd(), ".claude", "settings.json"));
|
|
2153
2174
|
if (hasConfig) {
|
|
2154
2175
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
2155
2176
|
} else {
|
|
@@ -2167,9 +2188,9 @@ program.addCommand(createDoctorCommand());
|
|
|
2167
2188
|
program.addCommand(createEnhanceCommand());
|
|
2168
2189
|
program.addCommand(createEvalCommand());
|
|
2169
2190
|
program.parse();
|
|
2170
|
-
async function
|
|
2191
|
+
async function fileExists4(path) {
|
|
2171
2192
|
try {
|
|
2172
|
-
await
|
|
2193
|
+
await access8(path);
|
|
2173
2194
|
return true;
|
|
2174
2195
|
} catch {
|
|
2175
2196
|
return false;
|