claude-launchpad 0.4.2 → 0.5.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 CHANGED
@@ -81,7 +81,7 @@ The core of the tool. Runs 7 analyzers against your `.claude/` directory and `CL
81
81
  | **Settings** | No hooks configured, dangerous tool access without safety nets |
82
82
  | **Hooks** | Missing auto-format on save, no .env file protection, no security gates, no PostCompact hook |
83
83
  | **Rules** | Dead rule files, stale references, empty configs |
84
- | **Permissions** | Bash auto-allowed without security hooks, no force-push protection |
84
+ | **Permissions** | Credential file exposure (~/.ssh, ~/.aws, ~/.npmrc), blanket Bash approval, bypass mode unprotected, sandbox disabled, .env gap between hooks and .claudeignore, no force-push protection |
85
85
  | **MCP Servers** | Invalid transport configs, missing commands/URLs |
86
86
 
87
87
  Output looks like this:
@@ -132,7 +132,7 @@ Detects your project and generates Claude Code config that fits. No templates, n
132
132
  **What you get (6 files):**
133
133
  - `CLAUDE.md` — your stack, commands, conventions, guardrails, memory management instructions
134
134
  - `TASKS.md` — sprint tracking, session continuity, deferred issues parking
135
- - `.claude/settings.json` — `$schema` for IDE autocomplete, `permissions.deny` for security, hooks for .env protection + destructive command blocking + auto-format + PostCompact context re-injection
135
+ - `.claude/settings.json` — `$schema` for IDE autocomplete, `permissions.deny` for credential + secret protection, sandbox enabled, bypass mode disabled, hooks for .env protection + destructive command blocking + auto-format + PostCompact context re-injection
136
136
  - `.claude/.gitignore` — prevents local settings and plans from being committed
137
137
  - `.claudeignore` — language-specific ignore patterns
138
138
  - `.claude/rules/conventions.md` — language-specific starter rules
@@ -154,26 +154,14 @@ Stays under the 120-instruction budget. Overflows detailed content to `.claude/r
154
154
  The part nobody else has built. Runs Claude against real test scenarios and scores the results.
155
155
 
156
156
  ```bash
157
- # Run only security tests (4 scenarios)
158
- claude-launchpad eval --suite security
159
-
160
- # Run only convention tests (5 scenarios)
161
- claude-launchpad eval --suite conventions
162
-
163
- # Run only workflow tests (2 scenarios)
164
- claude-launchpad eval --suite workflow
165
-
166
- # Run everything (11 scenarios)
157
+ # Interactive mode pick suite, runs, and model
167
158
  claude-launchpad eval
168
159
 
169
- # Use a cheaper model
170
- claude-launchpad eval --suite security --model haiku
171
-
172
- # One run per scenario (fastest)
173
- claude-launchpad eval --suite security --runs 1
160
+ # Or pass flags directly
161
+ claude-launchpad eval --suite security --runs 1 --model haiku
174
162
  ```
175
163
 
176
- Each scenario creates an isolated sandbox, runs Claude with a task, and checks if Claude followed the rules:
164
+ Each scenario creates an isolated sandbox with your full Claude Code config (settings.json, rules, hooks, .claudeignore) copied in, runs Claude with a task, and checks if your configuration made Claude follow the rules:
177
165
 
178
166
  ```
179
167
  ✓ security/sql-injection 10/10 PASS
@@ -192,7 +180,7 @@ Results are saved to `.claude/eval/` as structured markdown — you can feed the
192
180
 
193
181
  | Suite | Scenarios | What it tests |
194
182
  |---|---|---|
195
- | `security` | 4 | SQL injection, .env protection, secret exposure, input validation |
183
+ | `security` | 6 | SQL injection, .env protection, secret exposure, input validation, credential read, sandbox escape |
196
184
  | `conventions` | 5 | Error handling, immutability, file size, naming, no hardcoded values |
197
185
  | `workflow` | 2 | Git conventions, session continuity |
198
186
 
@@ -241,11 +229,11 @@ Then use `/launchpad:doctor`, `/launchpad:init`, `/launchpad:enhance`, `/launchp
241
229
 
242
230
  **Doctor** reads your files and runs static analysis. No API calls. No network. No cost.
243
231
 
244
- **Init** scans manifest files (package.json, go.mod, pyproject.toml, etc.), detects your stack, and generates 6 files: CLAUDE.md (with memory management instructions), TASKS.md (with deferred issues section), settings.json (with $schema, permissions.deny, hooks including PostCompact for context re-injection), .claude/.gitignore, .claudeignore, and language-specific rules. Formatter hooks use hardcoded safe commands only.
232
+ **Init** scans manifest files (package.json, go.mod, pyproject.toml, etc.), detects your stack, and generates 6 files: CLAUDE.md (with memory management instructions), TASKS.md (with deferred issues section), settings.json (with credential deny rules, sandbox enabled, bypass mode disabled, hooks including PostCompact), .claude/.gitignore, .claudeignore, and language-specific rules. Formatter hooks use hardcoded safe commands only.
245
233
 
246
234
  **Enhance** spawns `claude "prompt"` as an interactive child process. You see Claude's full UI. No data passes through the tool — it just launches Claude with a task.
247
235
 
248
- **Eval** creates a temp directory, writes seed files from the scenario YAML, initializes a git repo, runs Claude via the Agent SDK (or falls back to CLI), then checks the output with grep/file assertions. Sandbox is cleaned up after (or preserved with `--debug`).
236
+ **Eval** creates a temp directory, copies your full `.claude/` config (settings.json, rules, hooks, permissions) and `.claudeignore` into it, writes seed files from the scenario YAML, initializes a git repo, runs Claude via the Agent SDK (or falls back to CLI), then checks the output with grep/file assertions. Your code is never copied — only your Claude Code configuration. Sandbox is cleaned up after (or preserved with `--debug`).
249
237
 
250
238
  ## Why This Exists
251
239
 
package/dist/cli.js CHANGED
@@ -126,7 +126,7 @@ async function readJsonOrNull(path) {
126
126
  import { join, basename } from "path";
127
127
  async function detectProject(root) {
128
128
  const name = basename(root);
129
- const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradle, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
129
+ const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradleGroovy, buildGradleKts, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
130
130
  readJsonOrNull(join(root, "package.json")),
131
131
  fileExists(join(root, "go.mod")),
132
132
  readFileOrNull(join(root, "pyproject.toml")),
@@ -135,12 +135,14 @@ async function detectProject(root) {
135
135
  fileExists(join(root, "pubspec.yaml")),
136
136
  readJsonOrNull(join(root, "composer.json")),
137
137
  fileExists(join(root, "pom.xml")),
138
- fileExists(join(root, "build.gradle")) || fileExists(join(root, "build.gradle.kts")),
138
+ fileExists(join(root, "build.gradle")),
139
+ fileExists(join(root, "build.gradle.kts")),
139
140
  fileExists(join(root, "Package.swift")),
140
141
  fileExists(join(root, "mix.exs")),
141
142
  globExists(root, "*.csproj"),
142
143
  detectLockfiles(root)
143
144
  ]);
145
+ const buildGradle = buildGradleGroovy || buildGradleKts;
144
146
  const manifests = {
145
147
  pkgJson,
146
148
  goMod,
@@ -421,10 +423,18 @@ function generateSettings(detected) {
421
423
  "Bash(rm -rf ~)",
422
424
  "Read(.env)",
423
425
  "Read(.env.*)",
424
- "Read(secrets/**)"
426
+ "Read(secrets/**)",
427
+ "Read(~/.ssh/*)",
428
+ "Read(~/.aws/*)",
429
+ "Read(~/.npmrc)"
425
430
  ]
426
431
  },
427
- hooks
432
+ hooks,
433
+ disableBypassPermissionsMode: "disable",
434
+ sandbox: {
435
+ enabled: true,
436
+ failIfUnavailable: true
437
+ }
428
438
  };
429
439
  }
430
440
  var SAFE_FORMATTERS = {
@@ -730,13 +740,14 @@ var RULES_DIR = "rules";
730
740
  async function parseClaudeConfig(projectRoot) {
731
741
  const root = resolve(projectRoot);
732
742
  const claudeDir = join3(root, CLAUDE_DIR);
733
- const [claudeMd, settings, hooks, rules, mcpServers, skills] = await Promise.all([
743
+ const [claudeMd, settings, hooks, rules, mcpServers, skills, claudeignore] = await Promise.all([
734
744
  readClaudeMd(root),
735
745
  readSettings(claudeDir),
736
746
  readHooks(claudeDir),
737
747
  readRules(claudeDir),
738
748
  readMcpServers(claudeDir),
739
- readSkills(claudeDir)
749
+ readSkills(claudeDir),
750
+ readFileOrNull(join3(root, ".claudeignore"))
740
751
  ]);
741
752
  const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
742
753
  return {
@@ -748,7 +759,9 @@ async function parseClaudeConfig(projectRoot) {
748
759
  hooks,
749
760
  rules,
750
761
  mcpServers,
751
- skills
762
+ skills,
763
+ claudeignorePath: claudeignore !== null ? join3(root, ".claudeignore") : null,
764
+ claudeignoreContent: claudeignore
752
765
  };
753
766
  }
754
767
  async function readClaudeMd(root) {
@@ -1088,10 +1101,63 @@ async function analyzeRules(config) {
1088
1101
  // src/commands/doctor/analyzers/permissions.ts
1089
1102
  async function analyzePermissions(config) {
1090
1103
  const issues = [];
1104
+ const settings = config.settings;
1105
+ const permissions = settings?.permissions;
1106
+ const denyList = permissions?.deny ?? [];
1107
+ const allowList = permissions?.allow ?? [];
1108
+ const credentialPatterns = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1109
+ const missingCreds = credentialPatterns.filter((p) => !denyList.includes(p));
1110
+ if (missingCreds.length > 0) {
1111
+ issues.push({
1112
+ analyzer: "Permissions",
1113
+ severity: "high",
1114
+ message: `Credential files not blocked: ${missingCreds.join(", ")} \u2014 Claude can read SSH keys, AWS creds, or npm tokens`,
1115
+ fix: "Add Read(~/.ssh/*), Read(~/.aws/*), Read(~/.npmrc) to permissions.deny"
1116
+ });
1117
+ }
1118
+ const hasBlanketBash = allowList.some((a) => a === "Bash" || a.startsWith("Bash") && !a.includes("("));
1119
+ if (hasBlanketBash) {
1120
+ issues.push({
1121
+ analyzer: "Permissions",
1122
+ severity: "high",
1123
+ message: "Bash is blanket-allowed without pattern restriction \u2014 all shell commands are auto-approved",
1124
+ fix: "Replace blanket Bash with scoped patterns like Bash(npm test) or remove it"
1125
+ });
1126
+ }
1127
+ if (settings?.disableBypassPermissionsMode !== "disable") {
1128
+ issues.push({
1129
+ analyzer: "Permissions",
1130
+ severity: "high",
1131
+ message: "Bypass permissions mode not disabled \u2014 --dangerously-skip-permissions bypasses all checks",
1132
+ fix: 'Add "disableBypassPermissionsMode": "disable" to settings.json'
1133
+ });
1134
+ }
1135
+ const sandbox = settings?.sandbox;
1136
+ if (sandbox?.enabled !== true) {
1137
+ issues.push({
1138
+ analyzer: "Permissions",
1139
+ severity: "medium",
1140
+ message: "Sandbox not enabled \u2014 hooks block tool calls but not subprocess access (e.g. cat .env via Bash)",
1141
+ fix: 'Add "sandbox": { "enabled": true, "failIfUnavailable": true } to settings.json'
1142
+ });
1143
+ }
1144
+ const hasEnvHook = config.hooks.some((h) => h.command?.includes(".env"));
1145
+ if (hasEnvHook && config.claudeignoreContent !== null) {
1146
+ const lines = config.claudeignoreContent.split("\n").map((l) => l.trim());
1147
+ const hasEnvInIgnore = lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*");
1148
+ if (!hasEnvInIgnore) {
1149
+ issues.push({
1150
+ analyzer: "Permissions",
1151
+ severity: "medium",
1152
+ message: ".env is protected by hooks but not in .claudeignore \u2014 cat .env via Bash bypasses hooks",
1153
+ fix: "Add .env to .claudeignore for defense in depth"
1154
+ });
1155
+ }
1156
+ }
1091
1157
  const hasBashSecurity = config.hooks.some(
1092
1158
  (h) => h.event === "PreToolUse" && (h.matcher?.includes("Bash") || !h.matcher)
1093
1159
  );
1094
- const bashAllowed = config.settings?.allowedTools;
1160
+ const bashAllowed = settings?.allowedTools;
1095
1161
  const hasBashAutoAllow = bashAllowed?.some(
1096
1162
  (t) => typeof t === "string" && t.toLowerCase().includes("bash")
1097
1163
  );
@@ -1100,7 +1166,7 @@ async function analyzePermissions(config) {
1100
1166
  analyzer: "Permissions",
1101
1167
  severity: "high",
1102
1168
  message: "Bash is auto-allowed without a security hook \u2014 dangerous commands could run unchecked",
1103
- fix: "Add a PreToolUse hook for Bash that blocks destructive commands (rm -rf, git push --force)"
1169
+ fix: "Add a PreToolUse hook for Bash that blocks destructive commands"
1104
1170
  });
1105
1171
  }
1106
1172
  const hasForceProtection = config.hooks.some(
@@ -1125,7 +1191,7 @@ async function analyzePermissions(config) {
1125
1191
  });
1126
1192
  }
1127
1193
  }
1128
- const score = Math.max(0, 100 - issues.length * 20);
1194
+ const score = Math.max(0, 100 - issues.length * 15);
1129
1195
  return { name: "Permissions", issues, score };
1130
1196
  }
1131
1197
 
@@ -1303,7 +1369,11 @@ var FIX_TABLE = [
1303
1369
  { analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
1304
1370
  { analyzer: "Quality", match: "Memory", fix: (root) => addClaudeMdSection(root, "## Memory & Learnings", "Use the built-in memory system to persist knowledge across sessions:\n- **Save immediately** when you discover: a non-obvious fix, a gotcha, an external resource, a decision with context that would be lost, or a known issue to fix later\n- **Categories**: `decision` (why X over Y), `gotcha` (non-obvious pitfall), `deferred` (known issue, not urgent), `reference` (where to find things)\n- **Where**: project memory for this repo, global memory for cross-project learnings\n- **Format**: one fact per memory, include date and why \u2014 not just what\n- **Prune**: check if a memory on this topic exists before saving \u2014 update, don't duplicate\n- Before starting work, check memory for relevant context from previous sessions") },
1305
1371
  { analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
1306
- { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) }
1372
+ { analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
1373
+ { analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
1374
+ { analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
1375
+ { analyzer: "Permissions", match: "Sandbox not enabled", fix: (root) => addSandboxSettings(root) },
1376
+ { analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) }
1307
1377
  ];
1308
1378
  async function tryFix(issue, root, detected) {
1309
1379
  const entry = FIX_TABLE.find(
@@ -1405,6 +1475,49 @@ async function addPostCompactHook(root) {
1405
1475
  log.success("Added PostCompact hook (re-injects TASKS.md after compaction)");
1406
1476
  return true;
1407
1477
  }
1478
+ async function addCredentialDenyRules(root) {
1479
+ const settings = await readSettingsJson(root);
1480
+ const permissions = settings.permissions ?? {};
1481
+ const deny = permissions.deny ?? [];
1482
+ const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1483
+ const missing = toAdd.filter((p) => !deny.includes(p));
1484
+ if (missing.length === 0) return false;
1485
+ settings.permissions = { ...permissions, deny: [...deny, ...missing] };
1486
+ await writeSettingsJson(root, settings);
1487
+ log.success("Added credential deny rules (SSH, AWS, npm)");
1488
+ return true;
1489
+ }
1490
+ async function addBypassDisable(root) {
1491
+ const settings = await readSettingsJson(root);
1492
+ if (settings.disableBypassPermissionsMode === "disable") return false;
1493
+ settings.disableBypassPermissionsMode = "disable";
1494
+ await writeSettingsJson(root, settings);
1495
+ log.success("Added disableBypassPermissionsMode: disable");
1496
+ return true;
1497
+ }
1498
+ async function addSandboxSettings(root) {
1499
+ const settings = await readSettingsJson(root);
1500
+ const sandbox = settings.sandbox;
1501
+ if (sandbox?.enabled === true) return false;
1502
+ settings.sandbox = { enabled: true, failIfUnavailable: true };
1503
+ await writeSettingsJson(root, settings);
1504
+ log.success("Enabled sandbox with failIfUnavailable");
1505
+ return true;
1506
+ }
1507
+ async function addEnvToClaudeignore(root) {
1508
+ const ignorePath = join5(root, ".claudeignore");
1509
+ let content;
1510
+ try {
1511
+ content = await readFile4(ignorePath, "utf-8");
1512
+ } catch {
1513
+ return false;
1514
+ }
1515
+ const lines = content.split("\n").map((l) => l.trim());
1516
+ if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
1517
+ await writeFile2(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
1518
+ log.success("Added .env to .claudeignore");
1519
+ return true;
1520
+ }
1408
1521
  async function addClaudeMdSection(root, heading, content) {
1409
1522
  const claudeMdPath = join5(root, "CLAUDE.md");
1410
1523
  let existing;
@@ -1608,6 +1721,7 @@ function createDoctorCommand() {
1608
1721
 
1609
1722
  // src/commands/eval/index.ts
1610
1723
  import { Command as Command3 } from "commander";
1724
+ import { select } from "@inquirer/prompts";
1611
1725
  import ora from "ora";
1612
1726
  import chalk2 from "chalk";
1613
1727
  import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
@@ -1775,7 +1889,7 @@ async function listYamlFiles(dir) {
1775
1889
  }
1776
1890
 
1777
1891
  // src/commands/eval/runner.ts
1778
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile6, readdir as readdir4, rm } from "fs/promises";
1892
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile6, readdir as readdir4, rm, cp, access as access6 } from "fs/promises";
1779
1893
  import { join as join8, dirname as dirname3 } from "path";
1780
1894
  import { tmpdir } from "os";
1781
1895
  import { randomUUID } from "crypto";
@@ -1785,7 +1899,7 @@ var exec = promisify(execFile);
1785
1899
  async function runScenario(scenario, options) {
1786
1900
  const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
1787
1901
  try {
1788
- await setupSandbox(sandboxDir, scenario);
1902
+ await setupSandbox(sandboxDir, scenario, options.projectRoot);
1789
1903
  await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
1790
1904
  return await scoreResults(scenario, sandboxDir);
1791
1905
  } finally {
@@ -1806,13 +1920,14 @@ async function runScenarioWithRetries(scenario, options) {
1806
1920
  const sorted = [...results].sort((a, b) => a.score - b.score);
1807
1921
  return sorted[Math.floor(sorted.length / 2)];
1808
1922
  }
1809
- async function setupSandbox(sandboxDir, scenario) {
1923
+ async function setupSandbox(sandboxDir, scenario, projectRoot) {
1810
1924
  await mkdir3(sandboxDir, { recursive: true });
1811
1925
  for (const file of scenario.setup.files) {
1812
1926
  const filePath = join8(sandboxDir, file.path);
1813
1927
  await mkdir3(dirname3(filePath), { recursive: true });
1814
1928
  await writeFile3(filePath, file.content);
1815
1929
  }
1930
+ await copyProjectConfig(sandboxDir, projectRoot);
1816
1931
  if (scenario.setup.instructions) {
1817
1932
  await writeFile3(
1818
1933
  join8(sandboxDir, "CLAUDE.md"),
@@ -1835,6 +1950,31 @@ ${scenario.setup.instructions}
1835
1950
  "eval setup"
1836
1951
  ], { cwd: sandboxDir });
1837
1952
  }
1953
+ async function copyProjectConfig(sandboxDir, projectRoot) {
1954
+ const claudeDir = join8(projectRoot, ".claude");
1955
+ const sandboxClaudeDir = join8(sandboxDir, ".claude");
1956
+ const settingsPath = join8(claudeDir, "settings.json");
1957
+ if (await fileExistsSafe(settingsPath)) {
1958
+ await mkdir3(sandboxClaudeDir, { recursive: true });
1959
+ await cp(settingsPath, join8(sandboxClaudeDir, "settings.json"));
1960
+ }
1961
+ const rulesDir = join8(claudeDir, "rules");
1962
+ if (await fileExistsSafe(rulesDir)) {
1963
+ await cp(rulesDir, join8(sandboxClaudeDir, "rules"), { recursive: true });
1964
+ }
1965
+ const ignorePath = join8(projectRoot, ".claudeignore");
1966
+ if (await fileExistsSafe(ignorePath)) {
1967
+ await cp(ignorePath, join8(sandboxDir, ".claudeignore"));
1968
+ }
1969
+ }
1970
+ async function fileExistsSafe(path) {
1971
+ try {
1972
+ await access6(path);
1973
+ return true;
1974
+ } catch {
1975
+ return false;
1976
+ }
1977
+ }
1838
1978
  async function runClaudeInSandbox(cwd, prompt, timeout, model) {
1839
1979
  try {
1840
1980
  const sdk = await import("@anthropic-ai/claude-agent-sdk");
@@ -1991,6 +2131,35 @@ async function listAllFiles(dir) {
1991
2131
  function createEvalCommand() {
1992
2132
  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) => {
1993
2133
  printBanner();
2134
+ const hasFlags = opts.suite || opts.model || opts.runs !== "3" || opts.json || opts.debug;
2135
+ if (!hasFlags) {
2136
+ opts.suite = await select({
2137
+ message: "Suite",
2138
+ choices: [
2139
+ { name: "security (6 scenarios)", value: "security" },
2140
+ { name: "conventions (5 scenarios)", value: "conventions" },
2141
+ { name: "workflow (2 scenarios)", value: "workflow" },
2142
+ { name: "all (13 scenarios)", value: void 0 }
2143
+ ]
2144
+ });
2145
+ opts.runs = await select({
2146
+ message: "Runs per scenario",
2147
+ choices: [
2148
+ { name: "1 \u2014 fast", value: "1" },
2149
+ { name: "3 \u2014 default", value: "3" },
2150
+ { name: "5 \u2014 thorough", value: "5" }
2151
+ ]
2152
+ });
2153
+ opts.model = await select({
2154
+ message: "Model",
2155
+ choices: [
2156
+ { name: "haiku \u2014 cheapest", value: "haiku" },
2157
+ { name: "sonnet \u2014 balanced", value: "sonnet" },
2158
+ { name: "opus \u2014 best", value: "opus" }
2159
+ ]
2160
+ });
2161
+ log.blank();
2162
+ }
1994
2163
  const claudeAvailable = await checkClaudeCli();
1995
2164
  if (!claudeAvailable) {
1996
2165
  log.error("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code");
@@ -2154,7 +2323,7 @@ async function checkClaudeCli() {
2154
2323
  import { Command as Command4 } from "commander";
2155
2324
  import { spawn, execFile as execFile2 } from "child_process";
2156
2325
  import { promisify as promisify2 } from "util";
2157
- import { access as access6 } from "fs/promises";
2326
+ import { access as access7 } from "fs/promises";
2158
2327
  import { join as join10 } from "path";
2159
2328
  var execAsync = promisify2(execFile2);
2160
2329
  var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
@@ -2192,7 +2361,7 @@ function createEnhanceCommand() {
2192
2361
  const root = opts.path;
2193
2362
  const claudeMdPath = join10(root, "CLAUDE.md");
2194
2363
  try {
2195
- await access6(claudeMdPath);
2364
+ await access7(claudeMdPath);
2196
2365
  } catch {
2197
2366
  log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
2198
2367
  process.exit(1);