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/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 join10 } from "path";
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 \u2014 use .env.example for documentation' && exit 1; exit 0`
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 Object.keys(hooks).length > 0 ? { hooks } : {};
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 (merged with existing)");
669
- if (!hasClaudeignore) {
670
- log.success("Generated .claudeignore");
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
- await exec(
1827
- "claude",
1828
- [
1829
- "-p",
1830
- prompt,
1831
- "--output-format",
1832
- "text",
1833
- "--max-turns",
1834
- "20",
1835
- "--dangerously-skip-permissions",
1836
- "--allowedTools",
1837
- "Bash",
1838
- "Read",
1839
- "Write",
1840
- "Edit",
1841
- "Glob",
1842
- "Grep"
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.dim("\u2500")} ${chalk2.dim(check.label)}`);
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 join9 } from "path";
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 = join9(root, "CLAUDE.md");
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.3.3", "-v, --version").action(async () => {
2129
- const hasConfig = await fileExists(join10(process.cwd(), "CLAUDE.md")) || await fileExists(join10(process.cwd(), ".claude", "settings.json"));
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 {