claude-launchpad 0.2.2 → 0.3.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 +22 -1
- package/dist/cli.js +132 -78
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,10 +133,31 @@ claude-launchpad eval --suite common
|
|
|
133
133
|
Config Eval Score ━━━━━━━━━━━━━━━━━━── 89%
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
Each scenario
|
|
136
|
+
Each scenario is a YAML file. [Write your own](scenarios/CONTRIBUTING.md).
|
|
137
137
|
|
|
138
138
|
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
139
|
|
|
140
|
+
## How It Works Under the Hood
|
|
141
|
+
|
|
142
|
+
### doctor
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
### init
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
### enhance
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
### eval
|
|
152
|
+
1. Creates a temp directory (`/tmp/claude-eval-<uuid>/`)
|
|
153
|
+
2. Writes seed files from the scenario YAML (e.g., a `src/db.ts` with a TODO)
|
|
154
|
+
3. Writes a `CLAUDE.md` with the scenario's instructions
|
|
155
|
+
4. Initializes a git repo (Claude Code expects one)
|
|
156
|
+
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
|
|
157
|
+
6. After Claude finishes, runs grep/file assertions against the modified files
|
|
158
|
+
7. Scores: each check has points, total determines pass/fail
|
|
159
|
+
8. Cleans up the temp directory (or preserves it with `--debug`)
|
|
160
|
+
|
|
140
161
|
## Use in CI
|
|
141
162
|
|
|
142
163
|
Add this workflow to block PRs that degrade Claude Code config quality:
|
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;
|
|
@@ -1028,10 +1028,20 @@ async function analyzeHooks(config) {
|
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
1030
|
// src/commands/doctor/analyzers/rules.ts
|
|
1031
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1032
|
-
import { basename as basename2 } from "path";
|
|
1031
|
+
import { readFile as readFile4, access as access3 } from "fs/promises";
|
|
1032
|
+
import { basename as basename2, join as join4, dirname } from "path";
|
|
1033
1033
|
async function analyzeRules(config) {
|
|
1034
1034
|
const issues = [];
|
|
1035
|
+
const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
|
|
1036
|
+
const hasClaudeignore = await fileExists3(join4(projectRoot, ".claudeignore"));
|
|
1037
|
+
if (!hasClaudeignore) {
|
|
1038
|
+
issues.push({
|
|
1039
|
+
analyzer: "Rules",
|
|
1040
|
+
severity: "low",
|
|
1041
|
+
message: "No .claudeignore found \u2014 Claude may read noise files (node_modules, dist, lockfiles)",
|
|
1042
|
+
fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1035
1045
|
if (config.rules.length === 0) {
|
|
1036
1046
|
issues.push({
|
|
1037
1047
|
analyzer: "Rules",
|
|
@@ -1070,6 +1080,14 @@ async function analyzeRules(config) {
|
|
|
1070
1080
|
const score = Math.max(0, 100 - issues.length * 10);
|
|
1071
1081
|
return { name: "Rules", issues, score };
|
|
1072
1082
|
}
|
|
1083
|
+
async function fileExists3(path) {
|
|
1084
|
+
try {
|
|
1085
|
+
await access3(path);
|
|
1086
|
+
return true;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1073
1091
|
|
|
1074
1092
|
// src/commands/doctor/analyzers/permissions.ts
|
|
1075
1093
|
async function analyzePermissions(config) {
|
|
@@ -1116,7 +1134,7 @@ async function analyzePermissions(config) {
|
|
|
1116
1134
|
}
|
|
1117
1135
|
|
|
1118
1136
|
// src/commands/doctor/analyzers/mcp.ts
|
|
1119
|
-
import { access as
|
|
1137
|
+
import { access as access4 } from "fs/promises";
|
|
1120
1138
|
async function analyzeMcp(config) {
|
|
1121
1139
|
const issues = [];
|
|
1122
1140
|
const servers = config.mcpServers;
|
|
@@ -1124,8 +1142,7 @@ async function analyzeMcp(config) {
|
|
|
1124
1142
|
issues.push({
|
|
1125
1143
|
analyzer: "MCP",
|
|
1126
1144
|
severity: "info",
|
|
1127
|
-
message: "No MCP servers configured
|
|
1128
|
-
fix: "Add MCP servers in .claude/settings.json for GitHub, database, docs, etc."
|
|
1145
|
+
message: "No MCP servers configured. Run `claude-launchpad enhance` to get stack-specific recommendations."
|
|
1129
1146
|
});
|
|
1130
1147
|
return { name: "MCP Servers", issues, score: 50 };
|
|
1131
1148
|
}
|
|
@@ -1150,13 +1167,13 @@ async function analyzeMcp(config) {
|
|
|
1150
1167
|
const executable = server.command.split(" ")[0];
|
|
1151
1168
|
if (executable.startsWith("/") || executable.startsWith("./")) {
|
|
1152
1169
|
try {
|
|
1153
|
-
await
|
|
1170
|
+
await access4(executable);
|
|
1154
1171
|
} catch {
|
|
1155
1172
|
issues.push({
|
|
1156
1173
|
analyzer: "MCP",
|
|
1157
1174
|
severity: "medium",
|
|
1158
1175
|
message: `MCP server "${server.name}" command not found: ${executable}`,
|
|
1159
|
-
fix:
|
|
1176
|
+
fix: "Verify the path exists or install the required package"
|
|
1160
1177
|
});
|
|
1161
1178
|
}
|
|
1162
1179
|
}
|
|
@@ -1168,7 +1185,7 @@ async function analyzeMcp(config) {
|
|
|
1168
1185
|
|
|
1169
1186
|
// src/commands/doctor/analyzers/quality.ts
|
|
1170
1187
|
var ESSENTIAL_SECTIONS = [
|
|
1171
|
-
{ pattern: /^##\s+Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
|
|
1188
|
+
{ pattern: /^##\s+(Tech )?Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
|
|
1172
1189
|
{ pattern: /^##\s+Commands/m, name: "Commands", why: "Claude guesses wrong without explicit dev/build/test commands" },
|
|
1173
1190
|
{ pattern: /^##\s+Session Start/m, name: "Session Start", why: "Without this, Claude won't read TASKS.md or maintain continuity" },
|
|
1174
1191
|
{ pattern: /^##\s+Off.?Limits/m, name: "Off-Limits", why: "Without guardrails, Claude has no boundaries beyond defaults" },
|
|
@@ -1249,8 +1266,8 @@ async function analyzeQuality(config) {
|
|
|
1249
1266
|
}
|
|
1250
1267
|
|
|
1251
1268
|
// src/commands/doctor/fixer.ts
|
|
1252
|
-
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as
|
|
1253
|
-
import { join as
|
|
1269
|
+
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access5 } from "fs/promises";
|
|
1270
|
+
import { join as join5 } from "path";
|
|
1254
1271
|
async function applyFixes(issues, projectRoot) {
|
|
1255
1272
|
const detected = await detectProject(projectRoot);
|
|
1256
1273
|
let fixed = 0;
|
|
@@ -1299,6 +1316,9 @@ async function tryFix(issue, root, detected) {
|
|
|
1299
1316
|
if (issue.analyzer === "Quality" && issue.message.includes("Session Start")) {
|
|
1300
1317
|
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
1318
|
}
|
|
1319
|
+
if (issue.analyzer === "Rules" && issue.message.includes("No .claudeignore")) {
|
|
1320
|
+
return createClaudeignore(root, detected);
|
|
1321
|
+
}
|
|
1302
1322
|
if (issue.analyzer === "Rules" && issue.message.includes("No .claude/rules/")) {
|
|
1303
1323
|
return createStarterRules(root);
|
|
1304
1324
|
}
|
|
@@ -1384,7 +1404,7 @@ async function addForcePushProtection(root) {
|
|
|
1384
1404
|
return true;
|
|
1385
1405
|
}
|
|
1386
1406
|
async function addClaudeMdSection(root, heading, content) {
|
|
1387
|
-
const claudeMdPath =
|
|
1407
|
+
const claudeMdPath = join5(root, "CLAUDE.md");
|
|
1388
1408
|
let existing;
|
|
1389
1409
|
try {
|
|
1390
1410
|
existing = await readFile5(claudeMdPath, "utf-8");
|
|
@@ -1404,16 +1424,28 @@ ${content}
|
|
|
1404
1424
|
log.success(`Added "${heading}" section to CLAUDE.md`);
|
|
1405
1425
|
return true;
|
|
1406
1426
|
}
|
|
1427
|
+
async function createClaudeignore(root, detected) {
|
|
1428
|
+
const ignorePath = join5(root, ".claudeignore");
|
|
1429
|
+
try {
|
|
1430
|
+
await access5(ignorePath);
|
|
1431
|
+
return false;
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
const content = generateClaudeignore(detected);
|
|
1435
|
+
await writeFile2(ignorePath, content);
|
|
1436
|
+
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1407
1439
|
async function createStarterRules(root) {
|
|
1408
|
-
const rulesDir =
|
|
1440
|
+
const rulesDir = join5(root, ".claude", "rules");
|
|
1409
1441
|
try {
|
|
1410
|
-
await
|
|
1442
|
+
await access5(rulesDir);
|
|
1411
1443
|
return false;
|
|
1412
1444
|
} catch {
|
|
1413
1445
|
}
|
|
1414
1446
|
await mkdir2(rulesDir, { recursive: true });
|
|
1415
1447
|
await writeFile2(
|
|
1416
|
-
|
|
1448
|
+
join5(rulesDir, "conventions.md"),
|
|
1417
1449
|
`# Project Conventions
|
|
1418
1450
|
|
|
1419
1451
|
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
@@ -1426,7 +1458,7 @@ async function createStarterRules(root) {
|
|
|
1426
1458
|
return true;
|
|
1427
1459
|
}
|
|
1428
1460
|
async function readSettingsJson(root) {
|
|
1429
|
-
const path =
|
|
1461
|
+
const path = join5(root, ".claude", "settings.json");
|
|
1430
1462
|
try {
|
|
1431
1463
|
const content = await readFile5(path, "utf-8");
|
|
1432
1464
|
return JSON.parse(content);
|
|
@@ -1435,47 +1467,60 @@ async function readSettingsJson(root) {
|
|
|
1435
1467
|
}
|
|
1436
1468
|
}
|
|
1437
1469
|
async function writeSettingsJson(root, settings) {
|
|
1438
|
-
const dir =
|
|
1470
|
+
const dir = join5(root, ".claude");
|
|
1439
1471
|
await mkdir2(dir, { recursive: true });
|
|
1440
|
-
await writeFile2(
|
|
1472
|
+
await writeFile2(join5(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1441
1473
|
}
|
|
1442
1474
|
|
|
1443
1475
|
// src/commands/doctor/watcher.ts
|
|
1444
|
-
import {
|
|
1445
|
-
import { join as
|
|
1476
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
1477
|
+
import { join as join6 } from "path";
|
|
1446
1478
|
async function watchConfig(projectRoot) {
|
|
1447
|
-
const claudeDir = join5(projectRoot, ".claude");
|
|
1448
|
-
const claudeMd = join5(projectRoot, "CLAUDE.md");
|
|
1449
|
-
const claudeignore = join5(projectRoot, ".claudeignore");
|
|
1450
1479
|
await runAndDisplay(projectRoot);
|
|
1451
1480
|
log.blank();
|
|
1452
1481
|
log.info("Watching for changes... (Ctrl+C to stop)");
|
|
1453
1482
|
log.blank();
|
|
1454
|
-
let
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1483
|
+
let lastSnapshot = await getFileSnapshot(projectRoot);
|
|
1484
|
+
setInterval(async () => {
|
|
1485
|
+
const currentSnapshot = await getFileSnapshot(projectRoot);
|
|
1486
|
+
if (currentSnapshot !== lastSnapshot) {
|
|
1487
|
+
lastSnapshot = currentSnapshot;
|
|
1458
1488
|
console.clear();
|
|
1459
1489
|
await runAndDisplay(projectRoot);
|
|
1460
1490
|
log.blank();
|
|
1461
1491
|
log.info("Watching for changes... (Ctrl+C to stop)");
|
|
1462
1492
|
log.blank();
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1493
|
+
}
|
|
1494
|
+
}, 1e3);
|
|
1495
|
+
await new Promise(() => {
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
async function getFileSnapshot(projectRoot) {
|
|
1499
|
+
const files = [
|
|
1500
|
+
join6(projectRoot, "CLAUDE.md"),
|
|
1501
|
+
join6(projectRoot, ".claudeignore")
|
|
1502
|
+
];
|
|
1503
|
+
const claudeDir = join6(projectRoot, ".claude");
|
|
1469
1504
|
try {
|
|
1470
|
-
|
|
1505
|
+
const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
|
|
1506
|
+
for (const entry of entries) {
|
|
1507
|
+
if (entry.isFile()) {
|
|
1508
|
+
const parentPath = entry.parentPath ?? claudeDir;
|
|
1509
|
+
files.push(join6(parentPath, entry.name));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1471
1512
|
} catch {
|
|
1472
1513
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1514
|
+
const mtimes = [];
|
|
1515
|
+
for (const file of files) {
|
|
1516
|
+
try {
|
|
1517
|
+
const s = await stat(file);
|
|
1518
|
+
mtimes.push(`${file}:${s.mtimeMs}`);
|
|
1519
|
+
} catch {
|
|
1520
|
+
mtimes.push(`${file}:missing`);
|
|
1521
|
+
}
|
|
1476
1522
|
}
|
|
1477
|
-
|
|
1478
|
-
});
|
|
1523
|
+
return mtimes.join("|");
|
|
1479
1524
|
}
|
|
1480
1525
|
async function runAndDisplay(projectRoot) {
|
|
1481
1526
|
console.log("\x1B[36m\x1B[1m Claude Launchpad\x1B[0m");
|
|
@@ -1613,8 +1658,8 @@ import ora from "ora";
|
|
|
1613
1658
|
import chalk2 from "chalk";
|
|
1614
1659
|
|
|
1615
1660
|
// src/commands/eval/loader.ts
|
|
1616
|
-
import { readFile as readFile6, readdir as
|
|
1617
|
-
import { join as
|
|
1661
|
+
import { readFile as readFile6, readdir as readdir3, access as access6 } from "fs/promises";
|
|
1662
|
+
import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
|
|
1618
1663
|
import { fileURLToPath } from "url";
|
|
1619
1664
|
import { parse as parseYaml } from "yaml";
|
|
1620
1665
|
|
|
@@ -1717,7 +1762,7 @@ var ScenarioError = class extends Error {
|
|
|
1717
1762
|
|
|
1718
1763
|
// src/commands/eval/loader.ts
|
|
1719
1764
|
async function findScenariosDir() {
|
|
1720
|
-
const thisDir =
|
|
1765
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
1721
1766
|
const devPath = resolve2(thisDir, "../../../scenarios");
|
|
1722
1767
|
if (await dirExists(devPath)) return devPath;
|
|
1723
1768
|
const bundledPath = resolve2(thisDir, "../scenarios");
|
|
@@ -1728,7 +1773,7 @@ async function findScenariosDir() {
|
|
|
1728
1773
|
}
|
|
1729
1774
|
async function dirExists(path) {
|
|
1730
1775
|
try {
|
|
1731
|
-
await
|
|
1776
|
+
await access6(path);
|
|
1732
1777
|
return true;
|
|
1733
1778
|
} catch {
|
|
1734
1779
|
return false;
|
|
@@ -1737,7 +1782,7 @@ async function dirExists(path) {
|
|
|
1737
1782
|
async function loadScenarios(options) {
|
|
1738
1783
|
const { suite, customPath } = options;
|
|
1739
1784
|
const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
|
|
1740
|
-
const dirs = suite ? [
|
|
1785
|
+
const dirs = suite ? [join7(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
|
|
1741
1786
|
const allDirs = [scenarioDir, ...dirs];
|
|
1742
1787
|
const scenarios = [];
|
|
1743
1788
|
for (const dir of allDirs) {
|
|
@@ -1758,31 +1803,31 @@ async function loadScenarios(options) {
|
|
|
1758
1803
|
}
|
|
1759
1804
|
async function getSubdirectories(dir) {
|
|
1760
1805
|
try {
|
|
1761
|
-
const entries = await
|
|
1762
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1806
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1807
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join7(dir, e.name));
|
|
1763
1808
|
} catch {
|
|
1764
1809
|
return [];
|
|
1765
1810
|
}
|
|
1766
1811
|
}
|
|
1767
1812
|
async function listYamlFiles(dir) {
|
|
1768
1813
|
try {
|
|
1769
|
-
const entries = await
|
|
1770
|
-
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) =>
|
|
1814
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1815
|
+
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join7(dir, e.name));
|
|
1771
1816
|
} catch {
|
|
1772
1817
|
return [];
|
|
1773
1818
|
}
|
|
1774
1819
|
}
|
|
1775
1820
|
|
|
1776
1821
|
// src/commands/eval/runner.ts
|
|
1777
|
-
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as
|
|
1778
|
-
import { join as
|
|
1822
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as readdir4, rm } from "fs/promises";
|
|
1823
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
1779
1824
|
import { tmpdir } from "os";
|
|
1780
1825
|
import { randomUUID } from "crypto";
|
|
1781
1826
|
import { execFile } from "child_process";
|
|
1782
1827
|
import { promisify } from "util";
|
|
1783
1828
|
var exec = promisify(execFile);
|
|
1784
1829
|
async function runScenario(scenario, options) {
|
|
1785
|
-
const sandboxDir =
|
|
1830
|
+
const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
|
|
1786
1831
|
try {
|
|
1787
1832
|
await setupSandbox(sandboxDir, scenario);
|
|
1788
1833
|
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout);
|
|
@@ -1808,13 +1853,13 @@ async function runScenarioWithRetries(scenario, options) {
|
|
|
1808
1853
|
async function setupSandbox(sandboxDir, scenario) {
|
|
1809
1854
|
await mkdir3(sandboxDir, { recursive: true });
|
|
1810
1855
|
for (const file of scenario.setup.files) {
|
|
1811
|
-
const filePath =
|
|
1812
|
-
await mkdir3(
|
|
1856
|
+
const filePath = join8(sandboxDir, file.path);
|
|
1857
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
1813
1858
|
await writeFile3(filePath, file.content);
|
|
1814
1859
|
}
|
|
1815
1860
|
if (scenario.setup.instructions) {
|
|
1816
1861
|
await writeFile3(
|
|
1817
|
-
|
|
1862
|
+
join8(sandboxDir, "CLAUDE.md"),
|
|
1818
1863
|
`# Eval Scenario
|
|
1819
1864
|
|
|
1820
1865
|
${scenario.setup.instructions}
|
|
@@ -1927,7 +1972,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
|
|
|
1927
1972
|
async function checkGrep(check, sandboxDir) {
|
|
1928
1973
|
if (!check.pattern) return false;
|
|
1929
1974
|
try {
|
|
1930
|
-
const content = await readFile7(
|
|
1975
|
+
const content = await readFile7(join8(sandboxDir, check.target), "utf-8");
|
|
1931
1976
|
let found;
|
|
1932
1977
|
try {
|
|
1933
1978
|
found = new RegExp(check.pattern).test(content);
|
|
@@ -1941,7 +1986,7 @@ async function checkGrep(check, sandboxDir) {
|
|
|
1941
1986
|
}
|
|
1942
1987
|
async function checkFileExists(check, sandboxDir) {
|
|
1943
1988
|
try {
|
|
1944
|
-
await readFile7(
|
|
1989
|
+
await readFile7(join8(sandboxDir, check.target));
|
|
1945
1990
|
return check.expect === "present";
|
|
1946
1991
|
} catch {
|
|
1947
1992
|
return check.expect === "absent";
|
|
@@ -1949,7 +1994,7 @@ async function checkFileExists(check, sandboxDir) {
|
|
|
1949
1994
|
}
|
|
1950
1995
|
async function checkFileAbsent(check, sandboxDir) {
|
|
1951
1996
|
try {
|
|
1952
|
-
await readFile7(
|
|
1997
|
+
await readFile7(join8(sandboxDir, check.target));
|
|
1953
1998
|
return check.expect === "absent";
|
|
1954
1999
|
} catch {
|
|
1955
2000
|
return check.expect === "present";
|
|
@@ -1958,7 +2003,7 @@ async function checkFileAbsent(check, sandboxDir) {
|
|
|
1958
2003
|
async function checkMaxLines(check, sandboxDir) {
|
|
1959
2004
|
const maxLines = parseInt(check.pattern ?? "800", 10);
|
|
1960
2005
|
try {
|
|
1961
|
-
const files = await listAllFiles(
|
|
2006
|
+
const files = await listAllFiles(join8(sandboxDir, check.target));
|
|
1962
2007
|
for (const file of files) {
|
|
1963
2008
|
const content = await readFile7(file, "utf-8");
|
|
1964
2009
|
if (content.split("\n").length > maxLines) {
|
|
@@ -1973,9 +2018,9 @@ async function checkMaxLines(check, sandboxDir) {
|
|
|
1973
2018
|
async function listAllFiles(dir) {
|
|
1974
2019
|
const results = [];
|
|
1975
2020
|
try {
|
|
1976
|
-
const entries = await
|
|
2021
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1977
2022
|
for (const entry of entries) {
|
|
1978
|
-
const fullPath =
|
|
2023
|
+
const fullPath = join8(dir, entry.name);
|
|
1979
2024
|
if (entry.isDirectory()) {
|
|
1980
2025
|
results.push(...await listAllFiles(fullPath));
|
|
1981
2026
|
} else {
|
|
@@ -2100,28 +2145,37 @@ async function checkClaudeCli() {
|
|
|
2100
2145
|
import { Command as Command4 } from "commander";
|
|
2101
2146
|
import { spawn, execFile as execFile2 } from "child_process";
|
|
2102
2147
|
import { promisify as promisify2 } from "util";
|
|
2103
|
-
import { access as
|
|
2104
|
-
import { join as
|
|
2148
|
+
import { access as access7 } from "fs/promises";
|
|
2149
|
+
import { join as join9 } from "path";
|
|
2105
2150
|
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
|
|
2151
|
+
var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
|
|
2152
|
+
|
|
2153
|
+
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:
|
|
2154
|
+
- Create .claude/rules/conventions.md for detailed coding patterns
|
|
2155
|
+
- Create .claude/rules/architecture.md for detailed structure docs
|
|
2156
|
+
- Keep CLAUDE.md to HIGH-LEVEL summaries only (3-5 bullets per section max)
|
|
2107
2157
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2158
|
+
Sections to fill in or preserve (DO NOT remove any existing section):
|
|
2159
|
+
1. **## Stack** \u2014 if missing or incomplete, detect and add language, framework, package manager
|
|
2160
|
+
2. **## Architecture** \u2014 3-5 bullet points describing the codebase shape (not a full directory tree)
|
|
2161
|
+
3. **## Conventions** \u2014 max 8 key patterns. Move detailed rules to .claude/rules/conventions.md
|
|
2162
|
+
4. **## Off-Limits** \u2014 max 8 guardrails specific to this project
|
|
2163
|
+
5. **## Key Decisions** \u2014 only decisions that affect how Claude should work in this codebase
|
|
2164
|
+
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.
|
|
2112
2165
|
|
|
2113
2166
|
Rules:
|
|
2114
|
-
- Keep CLAUDE.md under 150 instructions (lines of actionable content)
|
|
2115
2167
|
- Don't remove existing content \u2014 only add or improve
|
|
2116
2168
|
- Be specific to THIS project, not generic advice
|
|
2117
|
-
- Use bullet points, not paragraphs
|
|
2169
|
+
- Use bullet points, not paragraphs
|
|
2170
|
+
- If a section would exceed 8 bullets, split into a .claude/rules/ file and reference it
|
|
2171
|
+
- After editing, count the actionable lines. If over 120, move content to rules files until under`;
|
|
2118
2172
|
function createEnhanceCommand() {
|
|
2119
2173
|
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
2174
|
printBanner();
|
|
2121
2175
|
const root = opts.path;
|
|
2122
|
-
const claudeMdPath =
|
|
2176
|
+
const claudeMdPath = join9(root, "CLAUDE.md");
|
|
2123
2177
|
try {
|
|
2124
|
-
await
|
|
2178
|
+
await access7(claudeMdPath);
|
|
2125
2179
|
} catch {
|
|
2126
2180
|
log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
|
|
2127
2181
|
process.exit(1);
|
|
@@ -2148,8 +2202,8 @@ function createEnhanceCommand() {
|
|
|
2148
2202
|
}
|
|
2149
2203
|
|
|
2150
2204
|
// 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
|
|
2205
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.0", "-v, --version").action(async () => {
|
|
2206
|
+
const hasConfig = await fileExists4(join10(process.cwd(), "CLAUDE.md")) || await fileExists4(join10(process.cwd(), ".claude", "settings.json"));
|
|
2153
2207
|
if (hasConfig) {
|
|
2154
2208
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
2155
2209
|
} else {
|
|
@@ -2167,9 +2221,9 @@ program.addCommand(createDoctorCommand());
|
|
|
2167
2221
|
program.addCommand(createEnhanceCommand());
|
|
2168
2222
|
program.addCommand(createEvalCommand());
|
|
2169
2223
|
program.parse();
|
|
2170
|
-
async function
|
|
2224
|
+
async function fileExists4(path) {
|
|
2171
2225
|
try {
|
|
2172
|
-
await
|
|
2226
|
+
await access8(path);
|
|
2173
2227
|
return true;
|
|
2174
2228
|
} catch {
|
|
2175
2229
|
return false;
|