claude-launchpad 0.3.3 → 0.4.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/LICENSE +21 -0
- package/README.md +161 -105
- package/dist/cli.js +161 -40
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/scenarios/{common → conventions}/error-handling.yaml +3 -3
- package/scenarios/{common → conventions}/file-size.yaml +3 -3
- package/scenarios/{common → conventions}/immutability.yaml +3 -3
- package/scenarios/{common → conventions}/naming-conventions.yaml +5 -5
- package/scenarios/{common → conventions}/no-hardcoded-values.yaml +3 -3
- package/scenarios/{common → security}/env-protection.yaml +3 -3
- package/scenarios/{common → security}/input-validation.yaml +3 -3
- package/scenarios/{common → security}/secret-exposure.yaml +2 -2
- package/scenarios/{common → security}/sql-injection.yaml +2 -2
- package/scenarios/{common → workflow}/git-conventions.yaml +3 -3
- package/scenarios/{common → workflow}/session-continuity.yaml +3 -3
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command as Command5 } from "commander";
|
|
5
|
-
import { join as
|
|
5
|
+
import { join as join11 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/commands/init/index.ts
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -440,7 +440,14 @@ function generateSettings(detected) {
|
|
|
440
440
|
matcher: "Read|Write|Edit",
|
|
441
441
|
hooks: [{
|
|
442
442
|
type: "command",
|
|
443
|
-
command: `echo "$TOOL_INPUT_FILE_PATH" | grep -qE '\\.(env|env\\..*)$' && ! echo "$TOOL_INPUT_FILE_PATH" | grep -q '.env.example' && echo 'BLOCKED: .env files contain secrets
|
|
443
|
+
command: `echo "$TOOL_INPUT_FILE_PATH" | grep -qE '\\.(env|env\\..*)$' && ! echo "$TOOL_INPUT_FILE_PATH" | grep -q '.env.example' && echo 'BLOCKED: .env files contain secrets' && exit 1; exit 0`
|
|
444
|
+
}]
|
|
445
|
+
});
|
|
446
|
+
preToolUse.push({
|
|
447
|
+
matcher: "Bash",
|
|
448
|
+
hooks: [{
|
|
449
|
+
type: "command",
|
|
450
|
+
command: `echo "$TOOL_INPUT_COMMAND" | grep -qE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|push.*--force|push.*-f' && echo 'BLOCKED: Destructive command detected' && exit 1; exit 0`
|
|
444
451
|
}]
|
|
445
452
|
});
|
|
446
453
|
const formatHook = buildFormatHook(detected);
|
|
@@ -450,7 +457,19 @@ function generateSettings(detected) {
|
|
|
450
457
|
const hooks = {};
|
|
451
458
|
if (preToolUse.length > 0) hooks.PreToolUse = preToolUse;
|
|
452
459
|
if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
|
|
453
|
-
return
|
|
460
|
+
return {
|
|
461
|
+
$schema: "https://json.schemastore.org/claude-code-settings.json",
|
|
462
|
+
permissions: {
|
|
463
|
+
deny: [
|
|
464
|
+
"Bash(rm -rf /)",
|
|
465
|
+
"Bash(rm -rf ~)",
|
|
466
|
+
"Read(.env)",
|
|
467
|
+
"Read(.env.*)",
|
|
468
|
+
"Read(secrets/**)"
|
|
469
|
+
]
|
|
470
|
+
},
|
|
471
|
+
hooks
|
|
472
|
+
};
|
|
454
473
|
}
|
|
455
474
|
var SAFE_FORMATTERS = {
|
|
456
475
|
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
@@ -649,11 +668,15 @@ async function scaffold(root, options, detected) {
|
|
|
649
668
|
const tasksMd = generateTasksMd(options);
|
|
650
669
|
const settings = generateSettings(detected);
|
|
651
670
|
const claudeignore = generateClaudeignore(detected);
|
|
652
|
-
await mkdir(join2(root, ".claude"), { recursive: true });
|
|
671
|
+
await mkdir(join2(root, ".claude", "rules"), { recursive: true });
|
|
653
672
|
const settingsPath = join2(root, ".claude", "settings.json");
|
|
654
673
|
const mergedSettings = await mergeSettings(settingsPath, settings);
|
|
655
674
|
const claudeignorePath = join2(root, ".claudeignore");
|
|
656
675
|
const hasClaudeignore = await fileExists(claudeignorePath);
|
|
676
|
+
const claudeGitignorePath = join2(root, ".claude", ".gitignore");
|
|
677
|
+
const hasClaudeGitignore = await fileExists(claudeGitignorePath);
|
|
678
|
+
const rulesPath = join2(root, ".claude", "rules", "conventions.md");
|
|
679
|
+
const hasRules = await fileExists(rulesPath);
|
|
657
680
|
const writes = [
|
|
658
681
|
writeFile(join2(root, "CLAUDE.md"), claudeMd),
|
|
659
682
|
writeFile(join2(root, "TASKS.md"), tasksMd),
|
|
@@ -662,18 +685,61 @@ async function scaffold(root, options, detected) {
|
|
|
662
685
|
if (!hasClaudeignore) {
|
|
663
686
|
writes.push(writeFile(claudeignorePath, claudeignore));
|
|
664
687
|
}
|
|
688
|
+
if (!hasClaudeGitignore) {
|
|
689
|
+
writes.push(writeFile(claudeGitignorePath, [
|
|
690
|
+
"# Local-only Claude Code files (never commit these)",
|
|
691
|
+
"settings.local.json",
|
|
692
|
+
"plans/",
|
|
693
|
+
"memory/",
|
|
694
|
+
"sessions/",
|
|
695
|
+
"tmp/",
|
|
696
|
+
""
|
|
697
|
+
].join("\n")));
|
|
698
|
+
}
|
|
699
|
+
if (!hasRules) {
|
|
700
|
+
const rulesContent = generateStarterRules(detected);
|
|
701
|
+
writes.push(writeFile(rulesPath, rulesContent));
|
|
702
|
+
}
|
|
665
703
|
await Promise.all(writes);
|
|
666
704
|
log.success("Generated CLAUDE.md");
|
|
667
705
|
log.success("Generated TASKS.md");
|
|
668
|
-
log.success("Generated .claude/settings.json (
|
|
669
|
-
if (!
|
|
670
|
-
|
|
671
|
-
|
|
706
|
+
log.success("Generated .claude/settings.json (schema, permissions, hooks)");
|
|
707
|
+
if (!hasClaudeGitignore) log.success("Generated .claude/.gitignore");
|
|
708
|
+
if (!hasClaudeignore) log.success("Generated .claudeignore");
|
|
709
|
+
if (!hasRules) log.success("Generated .claude/rules/conventions.md");
|
|
672
710
|
log.blank();
|
|
673
711
|
log.success("Done! Run `claude` to start.");
|
|
674
712
|
log.info("Run `claude-launchpad doctor` to check your config quality.");
|
|
675
713
|
log.blank();
|
|
676
714
|
}
|
|
715
|
+
function generateStarterRules(detected) {
|
|
716
|
+
const lines = [
|
|
717
|
+
"# Project Conventions",
|
|
718
|
+
"",
|
|
719
|
+
"- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)",
|
|
720
|
+
"- Keep files under 400 lines, functions under 50 lines",
|
|
721
|
+
"- Handle errors explicitly - no empty catch blocks",
|
|
722
|
+
"- Validate input at system boundaries"
|
|
723
|
+
];
|
|
724
|
+
if (detected.language === "TypeScript" || detected.language === "JavaScript") {
|
|
725
|
+
lines.push("- Use named exports, no default exports except Next.js pages");
|
|
726
|
+
lines.push("- No `any` types in TypeScript");
|
|
727
|
+
}
|
|
728
|
+
if (detected.language === "Python") {
|
|
729
|
+
lines.push("- Type hints on all function signatures");
|
|
730
|
+
lines.push("- Async everywhere for I/O operations");
|
|
731
|
+
}
|
|
732
|
+
if (detected.language === "Go") {
|
|
733
|
+
lines.push("- Table-driven tests");
|
|
734
|
+
lines.push("- Errors are values - handle them, don't ignore them");
|
|
735
|
+
}
|
|
736
|
+
if (detected.language === "Rust") {
|
|
737
|
+
lines.push("- Prefer Result over unwrap/expect in library code");
|
|
738
|
+
lines.push("- No unsafe blocks without a safety comment");
|
|
739
|
+
}
|
|
740
|
+
lines.push("");
|
|
741
|
+
return lines.join("\n");
|
|
742
|
+
}
|
|
677
743
|
async function mergeSettings(existingPath, generated) {
|
|
678
744
|
try {
|
|
679
745
|
const existing = JSON.parse(await readFile2(existingPath, "utf-8"));
|
|
@@ -1573,6 +1639,8 @@ function createDoctorCommand() {
|
|
|
1573
1639
|
import { Command as Command3 } from "commander";
|
|
1574
1640
|
import ora from "ora";
|
|
1575
1641
|
import chalk2 from "chalk";
|
|
1642
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1643
|
+
import { join as join9 } from "path";
|
|
1576
1644
|
|
|
1577
1645
|
// src/commands/eval/loader.ts
|
|
1578
1646
|
import { readFile as readFile5, readdir as readdir3, access as access5 } from "fs/promises";
|
|
@@ -1747,7 +1815,7 @@ async function runScenario(scenario, options) {
|
|
|
1747
1815
|
const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
|
|
1748
1816
|
try {
|
|
1749
1817
|
await setupSandbox(sandboxDir, scenario);
|
|
1750
|
-
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout);
|
|
1818
|
+
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
|
|
1751
1819
|
return await scoreResults(scenario, sandboxDir);
|
|
1752
1820
|
} finally {
|
|
1753
1821
|
if (options.debug) {
|
|
@@ -1796,7 +1864,7 @@ ${scenario.setup.instructions}
|
|
|
1796
1864
|
"eval setup"
|
|
1797
1865
|
], { cwd: sandboxDir });
|
|
1798
1866
|
}
|
|
1799
|
-
async function runClaudeInSandbox(cwd, prompt, timeout) {
|
|
1867
|
+
async function runClaudeInSandbox(cwd, prompt, timeout, model) {
|
|
1800
1868
|
try {
|
|
1801
1869
|
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
1802
1870
|
const controller = new AbortController();
|
|
@@ -1810,7 +1878,8 @@ async function runClaudeInSandbox(cwd, prompt, timeout) {
|
|
|
1810
1878
|
permissionMode: "dontAsk",
|
|
1811
1879
|
settingSources: [],
|
|
1812
1880
|
maxTurns: 20,
|
|
1813
|
-
abortController: controller
|
|
1881
|
+
abortController: controller,
|
|
1882
|
+
...model ? { model } : {}
|
|
1814
1883
|
}
|
|
1815
1884
|
})) {
|
|
1816
1885
|
}
|
|
@@ -1818,31 +1887,29 @@ async function runClaudeInSandbox(cwd, prompt, timeout) {
|
|
|
1818
1887
|
clearTimeout(timeoutId);
|
|
1819
1888
|
}
|
|
1820
1889
|
} catch {
|
|
1821
|
-
await runClaudeCli(cwd, prompt, timeout);
|
|
1890
|
+
await runClaudeCli(cwd, prompt, timeout, model);
|
|
1822
1891
|
}
|
|
1823
1892
|
}
|
|
1824
|
-
async function runClaudeCli(cwd, prompt, timeout) {
|
|
1893
|
+
async function runClaudeCli(cwd, prompt, timeout, model) {
|
|
1825
1894
|
try {
|
|
1826
|
-
|
|
1827
|
-
"
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
{ cwd, timeout, maxBuffer: 10 * 1024 * 1024 }
|
|
1845
|
-
);
|
|
1895
|
+
const args = [
|
|
1896
|
+
"-p",
|
|
1897
|
+
prompt,
|
|
1898
|
+
"--output-format",
|
|
1899
|
+
"text",
|
|
1900
|
+
"--max-turns",
|
|
1901
|
+
"20",
|
|
1902
|
+
"--dangerously-skip-permissions",
|
|
1903
|
+
"--allowedTools",
|
|
1904
|
+
"Bash",
|
|
1905
|
+
"Read",
|
|
1906
|
+
"Write",
|
|
1907
|
+
"Edit",
|
|
1908
|
+
"Glob",
|
|
1909
|
+
"Grep"
|
|
1910
|
+
];
|
|
1911
|
+
if (model) args.push("--model", model);
|
|
1912
|
+
await exec("claude", args, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 });
|
|
1846
1913
|
} catch (error) {
|
|
1847
1914
|
if (error && typeof error === "object" && "stdout" in error) {
|
|
1848
1915
|
return;
|
|
@@ -1951,7 +2018,7 @@ async function listAllFiles(dir) {
|
|
|
1951
2018
|
|
|
1952
2019
|
// src/commands/eval/index.ts
|
|
1953
2020
|
function createEvalCommand() {
|
|
1954
|
-
return new Command3("eval").description("Test your Claude Code config against eval scenarios").option("-s, --suite <suite>", "Eval suite to run (e.g., security, conventions, workflow)").option("-p, --path <path>", "Project root path", process.cwd()).option("--scenarios <path>", "Custom scenarios directory").option("--runs <n>", "Runs per scenario (default: 3)", "3").option("--timeout <ms>", "Timeout per run in ms (default: 120000)", "120000").option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").action(async (opts) => {
|
|
2021
|
+
return new Command3("eval").description("Test your Claude Code config against eval scenarios").option("-s, --suite <suite>", "Eval suite to run (e.g., security, conventions, workflow)").option("-p, --path <path>", "Project root path", process.cwd()).option("--scenarios <path>", "Custom scenarios directory").option("--runs <n>", "Runs per scenario (default: 3)", "3").option("--timeout <ms>", "Timeout per run in ms (default: 120000)", "120000").option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").option("--model <model>", "Model to use for eval (e.g., sonnet, haiku, opus)").action(async (opts) => {
|
|
1955
2022
|
printBanner();
|
|
1956
2023
|
const claudeAvailable = await checkClaudeCli();
|
|
1957
2024
|
if (!claudeAvailable) {
|
|
@@ -1972,6 +2039,9 @@ function createEvalCommand() {
|
|
|
1972
2039
|
return;
|
|
1973
2040
|
}
|
|
1974
2041
|
log.success(`Loaded ${scenarios.length} scenario(s)`);
|
|
2042
|
+
if (opts.model) {
|
|
2043
|
+
log.info(`Model: ${opts.model}`);
|
|
2044
|
+
}
|
|
1975
2045
|
log.blank();
|
|
1976
2046
|
const runs = parseInt(opts.runs, 10);
|
|
1977
2047
|
const timeout = parseInt(opts.timeout, 10);
|
|
@@ -1984,7 +2054,7 @@ function createEvalCommand() {
|
|
|
1984
2054
|
try {
|
|
1985
2055
|
const result = await runScenarioWithRetries(
|
|
1986
2056
|
{ ...scenario, runs },
|
|
1987
|
-
{ projectRoot: opts.path, timeout, debug: opts.debug }
|
|
2057
|
+
{ projectRoot: opts.path, timeout, debug: opts.debug, model: opts.model }
|
|
1988
2058
|
);
|
|
1989
2059
|
results.push(result);
|
|
1990
2060
|
if (result.passed) {
|
|
@@ -2019,6 +2089,7 @@ function createEvalCommand() {
|
|
|
2019
2089
|
return;
|
|
2020
2090
|
}
|
|
2021
2091
|
renderEvalReport(results);
|
|
2092
|
+
await saveEvalReport(results, opts.path, opts.suite, opts.model);
|
|
2022
2093
|
});
|
|
2023
2094
|
}
|
|
2024
2095
|
function renderEvalReport(results) {
|
|
@@ -2029,7 +2100,7 @@ function renderEvalReport(results) {
|
|
|
2029
2100
|
console.log(` ${icon} ${chalk2.bold(result.scenario)} ${score} ${status}`);
|
|
2030
2101
|
const failedChecks = result.checks.filter((c) => !c.passed);
|
|
2031
2102
|
for (const check of failedChecks) {
|
|
2032
|
-
console.log(` ${chalk2.
|
|
2103
|
+
console.log(` ${chalk2.red("\u2717")} ${chalk2.dim(check.label)}`);
|
|
2033
2104
|
}
|
|
2034
2105
|
}
|
|
2035
2106
|
log.blank();
|
|
@@ -2046,6 +2117,56 @@ function renderEvalReport(results) {
|
|
|
2046
2117
|
log.warn(`${passed} passed, ${failed} failed out of ${results.length} scenario(s).`);
|
|
2047
2118
|
}
|
|
2048
2119
|
}
|
|
2120
|
+
async function saveEvalReport(results, projectRoot, suite, model) {
|
|
2121
|
+
const totalScore = results.reduce((s, r) => s + r.score, 0);
|
|
2122
|
+
const totalMax = results.reduce((s, r) => s + r.maxScore, 0);
|
|
2123
|
+
const pct = totalMax > 0 ? Math.round(totalScore / totalMax * 100) : 0;
|
|
2124
|
+
const passed = results.filter((r) => r.passed).length;
|
|
2125
|
+
const failed = results.length - passed;
|
|
2126
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
2127
|
+
const lines = [
|
|
2128
|
+
`# Eval Report \u2014 ${timestamp}`,
|
|
2129
|
+
"",
|
|
2130
|
+
`**Score: ${pct}%** (${passed} passed, ${failed} failed out of ${results.length} scenarios)`,
|
|
2131
|
+
"",
|
|
2132
|
+
`- Suite: ${suite ?? "all"}`,
|
|
2133
|
+
`- Model: ${model ?? "default"}`,
|
|
2134
|
+
`- Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2135
|
+
"",
|
|
2136
|
+
"## Results",
|
|
2137
|
+
""
|
|
2138
|
+
];
|
|
2139
|
+
for (const result of results) {
|
|
2140
|
+
const status = result.passed ? "PASS" : "FAIL";
|
|
2141
|
+
lines.push(`### ${result.scenario} \u2014 ${result.score}/${result.maxScore} ${status}`);
|
|
2142
|
+
const failedChecks = result.checks.filter((c) => !c.passed);
|
|
2143
|
+
const passedChecks = result.checks.filter((c) => c.passed);
|
|
2144
|
+
for (const check of passedChecks) {
|
|
2145
|
+
lines.push(`- PASSED: ${check.label} (${check.points} pts)`);
|
|
2146
|
+
}
|
|
2147
|
+
for (const check of failedChecks) {
|
|
2148
|
+
lines.push(`- FAILED: ${check.label} (${check.points} pts)`);
|
|
2149
|
+
}
|
|
2150
|
+
lines.push("");
|
|
2151
|
+
}
|
|
2152
|
+
if (failed > 0) {
|
|
2153
|
+
lines.push("## Recommendations");
|
|
2154
|
+
lines.push("");
|
|
2155
|
+
for (const result of results.filter((r) => !r.passed)) {
|
|
2156
|
+
lines.push(`### Fix: ${result.scenario}`);
|
|
2157
|
+
const failedChecks = result.checks.filter((c) => !c.passed);
|
|
2158
|
+
for (const check of failedChecks) {
|
|
2159
|
+
lines.push(`- ${check.label} \u2014 update CLAUDE.md instructions or add hooks to enforce this behavior`);
|
|
2160
|
+
}
|
|
2161
|
+
lines.push("");
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const evalDir = join9(projectRoot, ".claude", "eval");
|
|
2165
|
+
await mkdir4(evalDir, { recursive: true });
|
|
2166
|
+
const filename = `eval-${suite ?? "all"}-${timestamp}.md`;
|
|
2167
|
+
await writeFile4(join9(evalDir, filename), lines.join("\n"));
|
|
2168
|
+
log.success(`Report saved to .claude/eval/${filename}`);
|
|
2169
|
+
}
|
|
2049
2170
|
async function checkClaudeCli() {
|
|
2050
2171
|
const { execFile: execFile3 } = await import("child_process");
|
|
2051
2172
|
const { promisify: promisify3 } = await import("util");
|
|
@@ -2063,7 +2184,7 @@ import { Command as Command4 } from "commander";
|
|
|
2063
2184
|
import { spawn, execFile as execFile2 } from "child_process";
|
|
2064
2185
|
import { promisify as promisify2 } from "util";
|
|
2065
2186
|
import { access as access6 } from "fs/promises";
|
|
2066
|
-
import { join as
|
|
2187
|
+
import { join as join10 } from "path";
|
|
2067
2188
|
var execAsync = promisify2(execFile2);
|
|
2068
2189
|
var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
|
|
2069
2190
|
|
|
@@ -2096,7 +2217,7 @@ function createEnhanceCommand() {
|
|
|
2096
2217
|
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) => {
|
|
2097
2218
|
printBanner();
|
|
2098
2219
|
const root = opts.path;
|
|
2099
|
-
const claudeMdPath =
|
|
2220
|
+
const claudeMdPath = join10(root, "CLAUDE.md");
|
|
2100
2221
|
try {
|
|
2101
2222
|
await access6(claudeMdPath);
|
|
2102
2223
|
} catch {
|
|
@@ -2125,8 +2246,8 @@ function createEnhanceCommand() {
|
|
|
2125
2246
|
}
|
|
2126
2247
|
|
|
2127
2248
|
// src/cli.ts
|
|
2128
|
-
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.
|
|
2129
|
-
const hasConfig = await fileExists(
|
|
2249
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.4.0", "-v, --version").action(async () => {
|
|
2250
|
+
const hasConfig = await fileExists(join11(process.cwd(), "CLAUDE.md")) || await fileExists(join11(process.cwd(), ".claude", "settings.json"));
|
|
2130
2251
|
if (hasConfig) {
|
|
2131
2252
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
2132
2253
|
} else {
|