claude-launchpad 0.3.0 → 0.3.2
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 +18 -14
- package/dist/cli.js +111 -198
- 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,24 +113,26 @@ 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
138
|
Each scenario is a YAML file. [Write your own](scenarios/CONTRIBUTING.md).
|
|
@@ -180,7 +182,9 @@ jobs:
|
|
|
180
182
|
|
|
181
183
|
Exit code is 1 when score is below the threshold, 0 when it passes.
|
|
182
184
|
|
|
183
|
-
##
|
|
185
|
+
## Plugin (pending marketplace review)
|
|
186
|
+
|
|
187
|
+
The plugin has been submitted to the Claude Code marketplace. Once approved:
|
|
184
188
|
|
|
185
189
|
```bash
|
|
186
190
|
claude plugin install claude-launchpad
|
|
@@ -206,7 +210,7 @@ Claude Launchpad gives you a number. Fix the issues, re-run, watch the number go
|
|
|
206
210
|
- **Enhance uses Claude.** Spawns an interactive session to understand your codebase — costs tokens but produces a CLAUDE.md that actually knows your project.
|
|
207
211
|
- **Eval uses the Agent SDK.** Runs Claude headless in sandboxes with explicit tool permissions — proof that your config works.
|
|
208
212
|
- **Works with any stack.** Auto-detects your project. No fixed menu of supported frameworks.
|
|
209
|
-
- **
|
|
213
|
+
- **57 tests.** The tool that tests configs is itself well-tested.
|
|
210
214
|
- **You never clone this repo.** It's a tool you run with `npx`, not a template you fork.
|
|
211
215
|
|
|
212
216
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command as Command5 } from "commander";
|
|
5
|
-
import { access as access8 } from "fs/promises";
|
|
6
5
|
import { join as join10 } from "path";
|
|
7
6
|
|
|
8
7
|
// src/commands/init/index.ts
|
|
@@ -70,9 +69,60 @@ function printIssue(severity, analyzer, message, fix) {
|
|
|
70
69
|
}
|
|
71
70
|
console.log();
|
|
72
71
|
}
|
|
72
|
+
function renderDoctorReport(results) {
|
|
73
|
+
const overallScore = Math.round(
|
|
74
|
+
results.reduce((sum, r) => sum + r.score, 0) / results.length
|
|
75
|
+
);
|
|
76
|
+
for (const result of results) {
|
|
77
|
+
printScoreCard(result.name, result.score);
|
|
78
|
+
}
|
|
79
|
+
log.blank();
|
|
80
|
+
printScoreCard("Overall", overallScore);
|
|
81
|
+
log.blank();
|
|
82
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
83
|
+
const actionable = allIssues.filter((i) => i.severity !== "info");
|
|
84
|
+
if (actionable.length === 0) {
|
|
85
|
+
log.success("No issues found. Your configuration looks solid.");
|
|
86
|
+
return { overallScore, actionableCount: 0 };
|
|
87
|
+
}
|
|
88
|
+
const sorted = [...actionable].sort((a, b) => {
|
|
89
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
90
|
+
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
|
|
91
|
+
});
|
|
92
|
+
for (const issue of sorted) {
|
|
93
|
+
printIssue(issue.severity, issue.analyzer, issue.message, issue.fix);
|
|
94
|
+
}
|
|
95
|
+
log.info(`${actionable.length} issue(s) found. Fix critical/high first.`);
|
|
96
|
+
return { overallScore, actionableCount: actionable.length };
|
|
97
|
+
}
|
|
73
98
|
|
|
74
|
-
// src/lib/
|
|
99
|
+
// src/lib/fs-utils.ts
|
|
75
100
|
import { readFile, access } from "fs/promises";
|
|
101
|
+
async function fileExists(path) {
|
|
102
|
+
try {
|
|
103
|
+
await access(path);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function readFileOrNull(path) {
|
|
110
|
+
try {
|
|
111
|
+
return await readFile(path, "utf-8");
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function readJsonOrNull(path) {
|
|
117
|
+
try {
|
|
118
|
+
const content = await readFile(path, "utf-8");
|
|
119
|
+
return JSON.parse(content);
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/lib/detect.ts
|
|
76
126
|
import { join, basename } from "path";
|
|
77
127
|
async function detectProject(root) {
|
|
78
128
|
const name = basename(root);
|
|
@@ -300,29 +350,6 @@ function pmRun(pkg) {
|
|
|
300
350
|
if (pm?.startsWith("bun")) return "bun";
|
|
301
351
|
return "npm run";
|
|
302
352
|
}
|
|
303
|
-
async function readJsonOrNull(path) {
|
|
304
|
-
try {
|
|
305
|
-
const content = await readFile(path, "utf-8");
|
|
306
|
-
return JSON.parse(content);
|
|
307
|
-
} catch {
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
async function readFileOrNull(path) {
|
|
312
|
-
try {
|
|
313
|
-
return await readFile(path, "utf-8");
|
|
314
|
-
} catch {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
async function fileExists(path) {
|
|
319
|
-
try {
|
|
320
|
-
await access(path);
|
|
321
|
-
return true;
|
|
322
|
-
} catch {
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
353
|
async function globExists(dir, pattern) {
|
|
327
354
|
const { readdir: readdir5 } = await import("fs/promises");
|
|
328
355
|
try {
|
|
@@ -425,63 +452,24 @@ function generateSettings(detected) {
|
|
|
425
452
|
if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
|
|
426
453
|
return Object.keys(hooks).length > 0 ? { hooks } : {};
|
|
427
454
|
}
|
|
455
|
+
var SAFE_FORMATTERS = {
|
|
456
|
+
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
457
|
+
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
458
|
+
Python: { extensions: ["py"], command: "ruff format" },
|
|
459
|
+
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
460
|
+
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
461
|
+
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
462
|
+
Dart: { extensions: ["dart"], command: "dart format" },
|
|
463
|
+
PHP: { extensions: ["php"], command: "vendor/bin/pint" },
|
|
464
|
+
Kotlin: { extensions: ["kt", "kts"], command: "ktlint -F" },
|
|
465
|
+
Java: { extensions: ["java"], command: "google-java-format -i" },
|
|
466
|
+
Swift: { extensions: ["swift"], command: "swift-format format -i" },
|
|
467
|
+
Elixir: { extensions: ["ex", "exs"], command: "mix format" },
|
|
468
|
+
"C#": { extensions: ["cs"], command: "dotnet format" }
|
|
469
|
+
};
|
|
428
470
|
function buildFormatHook(detected) {
|
|
429
471
|
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];
|
|
472
|
+
const config = SAFE_FORMATTERS[detected.language];
|
|
485
473
|
if (!config) return null;
|
|
486
474
|
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
487
475
|
return {
|
|
@@ -640,7 +628,7 @@ function createInitCommand() {
|
|
|
640
628
|
message: "One-line description (optional):"
|
|
641
629
|
});
|
|
642
630
|
const options = { name: name.trim(), description: description.trim() };
|
|
643
|
-
const hasClaudeMd = await
|
|
631
|
+
const hasClaudeMd = await fileExists(join2(root, "CLAUDE.md"));
|
|
644
632
|
if (hasClaudeMd && !opts.yes) {
|
|
645
633
|
const overwrite = await confirm({
|
|
646
634
|
message: "CLAUDE.md already exists. Overwrite?",
|
|
@@ -665,7 +653,7 @@ async function scaffold(root, options, detected) {
|
|
|
665
653
|
const settingsPath = join2(root, ".claude", "settings.json");
|
|
666
654
|
const mergedSettings = await mergeSettings(settingsPath, settings);
|
|
667
655
|
const claudeignorePath = join2(root, ".claudeignore");
|
|
668
|
-
const hasClaudeignore = await
|
|
656
|
+
const hasClaudeignore = await fileExists(claudeignorePath);
|
|
669
657
|
const writes = [
|
|
670
658
|
writeFile(join2(root, "CLAUDE.md"), claudeMd),
|
|
671
659
|
writeFile(join2(root, "TASKS.md"), tasksMd),
|
|
@@ -686,14 +674,6 @@ async function scaffold(root, options, detected) {
|
|
|
686
674
|
log.info("Run `claude-launchpad doctor` to check your config quality.");
|
|
687
675
|
log.blank();
|
|
688
676
|
}
|
|
689
|
-
async function fileExists2(path) {
|
|
690
|
-
try {
|
|
691
|
-
await readFile2(path);
|
|
692
|
-
return true;
|
|
693
|
-
} catch {
|
|
694
|
-
return false;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
677
|
async function mergeSettings(existingPath, generated) {
|
|
698
678
|
try {
|
|
699
679
|
const existing = JSON.parse(await readFile2(existingPath, "utf-8"));
|
|
@@ -719,7 +699,7 @@ async function mergeSettings(existingPath, generated) {
|
|
|
719
699
|
import { Command as Command2 } from "commander";
|
|
720
700
|
|
|
721
701
|
// src/lib/parser.ts
|
|
722
|
-
import {
|
|
702
|
+
import { readdir, access as access2 } from "fs/promises";
|
|
723
703
|
import { join as join3, resolve } from "path";
|
|
724
704
|
var CLAUDE_MD = "CLAUDE.md";
|
|
725
705
|
var CLAUDE_DIR = ".claude";
|
|
@@ -750,7 +730,7 @@ async function parseClaudeConfig(projectRoot) {
|
|
|
750
730
|
};
|
|
751
731
|
}
|
|
752
732
|
async function readClaudeMd(root) {
|
|
753
|
-
return
|
|
733
|
+
return readFileOrNull(join3(root, CLAUDE_MD));
|
|
754
734
|
}
|
|
755
735
|
function countInstructions(content) {
|
|
756
736
|
const lines = content.split("\n");
|
|
@@ -766,7 +746,7 @@ function countInstructions(content) {
|
|
|
766
746
|
return count;
|
|
767
747
|
}
|
|
768
748
|
async function readSettings(claudeDir) {
|
|
769
|
-
const raw = await
|
|
749
|
+
const raw = await readFileOrNull(join3(claudeDir, SETTINGS_FILE));
|
|
770
750
|
if (raw === null) return null;
|
|
771
751
|
try {
|
|
772
752
|
return JSON.parse(raw);
|
|
@@ -775,7 +755,7 @@ async function readSettings(claudeDir) {
|
|
|
775
755
|
}
|
|
776
756
|
}
|
|
777
757
|
async function readHooks(claudeDir) {
|
|
778
|
-
const settingsRaw = await
|
|
758
|
+
const settingsRaw = await readFileOrNull(join3(claudeDir, SETTINGS_FILE));
|
|
779
759
|
if (settingsRaw === null) return [];
|
|
780
760
|
try {
|
|
781
761
|
const settings = JSON.parse(settingsRaw);
|
|
@@ -818,7 +798,7 @@ async function readRules(claudeDir) {
|
|
|
818
798
|
return listFilesRecursive(rulesDir, ".md");
|
|
819
799
|
}
|
|
820
800
|
async function readMcpServers(claudeDir) {
|
|
821
|
-
const settingsRaw = await
|
|
801
|
+
const settingsRaw = await readFileOrNull(join3(claudeDir, SETTINGS_FILE));
|
|
822
802
|
if (settingsRaw === null) return [];
|
|
823
803
|
try {
|
|
824
804
|
const settings = JSON.parse(settingsRaw);
|
|
@@ -848,13 +828,6 @@ async function readSkills(claudeDir) {
|
|
|
848
828
|
]);
|
|
849
829
|
return [...commands, ...skills];
|
|
850
830
|
}
|
|
851
|
-
async function readFileOrNull2(path) {
|
|
852
|
-
try {
|
|
853
|
-
return await readFile3(path, "utf-8");
|
|
854
|
-
} catch {
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
831
|
async function listFilesRecursive(dir, ext) {
|
|
859
832
|
try {
|
|
860
833
|
await access2(dir);
|
|
@@ -1028,12 +1001,12 @@ async function analyzeHooks(config) {
|
|
|
1028
1001
|
}
|
|
1029
1002
|
|
|
1030
1003
|
// src/commands/doctor/analyzers/rules.ts
|
|
1031
|
-
import { readFile as
|
|
1004
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1032
1005
|
import { basename as basename2, join as join4, dirname } from "path";
|
|
1033
1006
|
async function analyzeRules(config) {
|
|
1034
1007
|
const issues = [];
|
|
1035
1008
|
const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
|
|
1036
|
-
const hasClaudeignore = await
|
|
1009
|
+
const hasClaudeignore = await fileExists(join4(projectRoot, ".claudeignore"));
|
|
1037
1010
|
if (!hasClaudeignore) {
|
|
1038
1011
|
issues.push({
|
|
1039
1012
|
analyzer: "Rules",
|
|
@@ -1053,7 +1026,7 @@ async function analyzeRules(config) {
|
|
|
1053
1026
|
}
|
|
1054
1027
|
for (const rulePath of config.rules) {
|
|
1055
1028
|
try {
|
|
1056
|
-
const content = await
|
|
1029
|
+
const content = await readFile3(rulePath, "utf-8");
|
|
1057
1030
|
const trimmed = content.trim();
|
|
1058
1031
|
if (trimmed.length === 0) {
|
|
1059
1032
|
issues.push({
|
|
@@ -1080,14 +1053,6 @@ async function analyzeRules(config) {
|
|
|
1080
1053
|
const score = Math.max(0, 100 - issues.length * 10);
|
|
1081
1054
|
return { name: "Rules", issues, score };
|
|
1082
1055
|
}
|
|
1083
|
-
async function fileExists3(path) {
|
|
1084
|
-
try {
|
|
1085
|
-
await access3(path);
|
|
1086
|
-
return true;
|
|
1087
|
-
} catch {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
1056
|
|
|
1092
1057
|
// src/commands/doctor/analyzers/permissions.ts
|
|
1093
1058
|
async function analyzePermissions(config) {
|
|
@@ -1134,7 +1099,7 @@ async function analyzePermissions(config) {
|
|
|
1134
1099
|
}
|
|
1135
1100
|
|
|
1136
1101
|
// src/commands/doctor/analyzers/mcp.ts
|
|
1137
|
-
import { access as
|
|
1102
|
+
import { access as access3 } from "fs/promises";
|
|
1138
1103
|
async function analyzeMcp(config) {
|
|
1139
1104
|
const issues = [];
|
|
1140
1105
|
const servers = config.mcpServers;
|
|
@@ -1167,7 +1132,7 @@ async function analyzeMcp(config) {
|
|
|
1167
1132
|
const executable = server.command.split(" ")[0];
|
|
1168
1133
|
if (executable.startsWith("/") || executable.startsWith("./")) {
|
|
1169
1134
|
try {
|
|
1170
|
-
await
|
|
1135
|
+
await access3(executable);
|
|
1171
1136
|
} catch {
|
|
1172
1137
|
issues.push({
|
|
1173
1138
|
analyzer: "MCP",
|
|
@@ -1266,7 +1231,7 @@ async function analyzeQuality(config) {
|
|
|
1266
1231
|
}
|
|
1267
1232
|
|
|
1268
1233
|
// src/commands/doctor/fixer.ts
|
|
1269
|
-
import { readFile as
|
|
1234
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2, access as access4 } from "fs/promises";
|
|
1270
1235
|
import { join as join5 } from "path";
|
|
1271
1236
|
async function applyFixes(issues, projectRoot) {
|
|
1272
1237
|
const detected = await detectProject(projectRoot);
|
|
@@ -1351,13 +1316,13 @@ async function addEnvProtectionHook(root) {
|
|
|
1351
1316
|
async function addAutoFormatHook(root, detected) {
|
|
1352
1317
|
if (!detected.language) return false;
|
|
1353
1318
|
const formatters = {
|
|
1354
|
-
TypeScript: { extensions: ["ts", "tsx"], command:
|
|
1355
|
-
JavaScript: { extensions: ["js", "jsx"], command:
|
|
1356
|
-
Python: { extensions: ["py"], command:
|
|
1319
|
+
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
1320
|
+
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
1321
|
+
Python: { extensions: ["py"], command: "ruff format" },
|
|
1357
1322
|
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
1358
1323
|
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
1359
1324
|
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
1360
|
-
PHP: { extensions: ["php"], command:
|
|
1325
|
+
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
1361
1326
|
};
|
|
1362
1327
|
const config = formatters[detected.language];
|
|
1363
1328
|
if (!config) return false;
|
|
@@ -1407,7 +1372,7 @@ async function addClaudeMdSection(root, heading, content) {
|
|
|
1407
1372
|
const claudeMdPath = join5(root, "CLAUDE.md");
|
|
1408
1373
|
let existing;
|
|
1409
1374
|
try {
|
|
1410
|
-
existing = await
|
|
1375
|
+
existing = await readFile4(claudeMdPath, "utf-8");
|
|
1411
1376
|
} catch {
|
|
1412
1377
|
return false;
|
|
1413
1378
|
}
|
|
@@ -1427,7 +1392,7 @@ ${content}
|
|
|
1427
1392
|
async function createClaudeignore(root, detected) {
|
|
1428
1393
|
const ignorePath = join5(root, ".claudeignore");
|
|
1429
1394
|
try {
|
|
1430
|
-
await
|
|
1395
|
+
await access4(ignorePath);
|
|
1431
1396
|
return false;
|
|
1432
1397
|
} catch {
|
|
1433
1398
|
}
|
|
@@ -1439,7 +1404,7 @@ async function createClaudeignore(root, detected) {
|
|
|
1439
1404
|
async function createStarterRules(root) {
|
|
1440
1405
|
const rulesDir = join5(root, ".claude", "rules");
|
|
1441
1406
|
try {
|
|
1442
|
-
await
|
|
1407
|
+
await access4(rulesDir);
|
|
1443
1408
|
return false;
|
|
1444
1409
|
} catch {
|
|
1445
1410
|
}
|
|
@@ -1460,7 +1425,7 @@ async function createStarterRules(root) {
|
|
|
1460
1425
|
async function readSettingsJson(root) {
|
|
1461
1426
|
const path = join5(root, ".claude", "settings.json");
|
|
1462
1427
|
try {
|
|
1463
|
-
const content = await
|
|
1428
|
+
const content = await readFile4(path, "utf-8");
|
|
1464
1429
|
return JSON.parse(content);
|
|
1465
1430
|
} catch {
|
|
1466
1431
|
return {};
|
|
@@ -1540,29 +1505,7 @@ async function runAndDisplay(projectRoot) {
|
|
|
1540
1505
|
analyzePermissions(config),
|
|
1541
1506
|
analyzeMcp(config)
|
|
1542
1507
|
]);
|
|
1543
|
-
|
|
1544
|
-
results.reduce((sum, r) => sum + r.score, 0) / results.length
|
|
1545
|
-
);
|
|
1546
|
-
for (const result of results) {
|
|
1547
|
-
printScoreCard(result.name, result.score);
|
|
1548
|
-
}
|
|
1549
|
-
log.blank();
|
|
1550
|
-
printScoreCard("Overall", overallScore);
|
|
1551
|
-
log.blank();
|
|
1552
|
-
const allIssues = results.flatMap((r) => r.issues);
|
|
1553
|
-
const actionable = allIssues.filter((i) => i.severity !== "info");
|
|
1554
|
-
if (actionable.length === 0) {
|
|
1555
|
-
log.success("No issues found. Your configuration looks solid.");
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
const sorted = [...actionable].sort((a, b) => {
|
|
1559
|
-
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
1560
|
-
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
|
|
1561
|
-
});
|
|
1562
|
-
for (const issue of sorted) {
|
|
1563
|
-
printIssue(issue.severity, issue.analyzer, issue.message, issue.fix);
|
|
1564
|
-
}
|
|
1565
|
-
log.info(`${actionable.length} issue(s) found. Fix critical/high first.`);
|
|
1508
|
+
renderDoctorReport(results);
|
|
1566
1509
|
}
|
|
1567
1510
|
|
|
1568
1511
|
// src/commands/doctor/index.ts
|
|
@@ -1591,13 +1534,13 @@ function createDoctorCommand() {
|
|
|
1591
1534
|
analyzeMcp(config)
|
|
1592
1535
|
]);
|
|
1593
1536
|
if (opts.json) {
|
|
1594
|
-
const
|
|
1537
|
+
const overallScore2 = Math.round(
|
|
1595
1538
|
results.reduce((sum, r) => sum + r.score, 0) / results.length
|
|
1596
1539
|
);
|
|
1597
|
-
console.log(JSON.stringify({ overallScore, analyzers: results, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
1540
|
+
console.log(JSON.stringify({ overallScore: overallScore2, analyzers: results, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
1598
1541
|
return;
|
|
1599
1542
|
}
|
|
1600
|
-
|
|
1543
|
+
const { overallScore } = renderDoctorReport(results);
|
|
1601
1544
|
if (opts.fix) {
|
|
1602
1545
|
const allIssues = results.flatMap((r) => r.issues);
|
|
1603
1546
|
const fixable = allIssues.filter((i) => i.severity !== "info");
|
|
@@ -1616,9 +1559,6 @@ function createDoctorCommand() {
|
|
|
1616
1559
|
}
|
|
1617
1560
|
}
|
|
1618
1561
|
if (opts.minScore) {
|
|
1619
|
-
const overallScore = Math.round(
|
|
1620
|
-
results.reduce((sum, r) => sum + r.score, 0) / results.length
|
|
1621
|
-
);
|
|
1622
1562
|
const threshold = parseInt(opts.minScore, 10);
|
|
1623
1563
|
if (overallScore < threshold) {
|
|
1624
1564
|
process.exit(1);
|
|
@@ -1626,31 +1566,6 @@ function createDoctorCommand() {
|
|
|
1626
1566
|
}
|
|
1627
1567
|
});
|
|
1628
1568
|
}
|
|
1629
|
-
function renderReport(results) {
|
|
1630
|
-
const overallScore = Math.round(
|
|
1631
|
-
results.reduce((sum, r) => sum + r.score, 0) / results.length
|
|
1632
|
-
);
|
|
1633
|
-
for (const result of results) {
|
|
1634
|
-
printScoreCard(result.name, result.score);
|
|
1635
|
-
}
|
|
1636
|
-
log.blank();
|
|
1637
|
-
printScoreCard("Overall", overallScore);
|
|
1638
|
-
log.blank();
|
|
1639
|
-
const allIssues = results.flatMap((r) => r.issues);
|
|
1640
|
-
const actionable = allIssues.filter((i) => i.severity !== "info");
|
|
1641
|
-
if (actionable.length === 0) {
|
|
1642
|
-
log.success("No issues found. Your configuration looks solid.");
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
for (const issue of sortBySeverity(actionable)) {
|
|
1646
|
-
printIssue(issue.severity, issue.analyzer, issue.message, issue.fix);
|
|
1647
|
-
}
|
|
1648
|
-
log.info(`${actionable.length} issue(s) found. Fix critical/high first.`);
|
|
1649
|
-
}
|
|
1650
|
-
function sortBySeverity(issues) {
|
|
1651
|
-
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
1652
|
-
return [...issues].sort((a, b) => (order[a.severity] ?? 4) - (order[b.severity] ?? 4));
|
|
1653
|
-
}
|
|
1654
1569
|
|
|
1655
1570
|
// src/commands/eval/index.ts
|
|
1656
1571
|
import { Command as Command3 } from "commander";
|
|
@@ -1658,7 +1573,7 @@ import ora from "ora";
|
|
|
1658
1573
|
import chalk2 from "chalk";
|
|
1659
1574
|
|
|
1660
1575
|
// src/commands/eval/loader.ts
|
|
1661
|
-
import { readFile as
|
|
1576
|
+
import { readFile as readFile5, readdir as readdir3, access as access5 } from "fs/promises";
|
|
1662
1577
|
import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
|
|
1663
1578
|
import { fileURLToPath } from "url";
|
|
1664
1579
|
import { parse as parseYaml } from "yaml";
|
|
@@ -1773,7 +1688,7 @@ async function findScenariosDir() {
|
|
|
1773
1688
|
}
|
|
1774
1689
|
async function dirExists(path) {
|
|
1775
1690
|
try {
|
|
1776
|
-
await
|
|
1691
|
+
await access5(path);
|
|
1777
1692
|
return true;
|
|
1778
1693
|
} catch {
|
|
1779
1694
|
return false;
|
|
@@ -1789,7 +1704,7 @@ async function loadScenarios(options) {
|
|
|
1789
1704
|
const files = await listYamlFiles(dir);
|
|
1790
1705
|
for (const file of files) {
|
|
1791
1706
|
try {
|
|
1792
|
-
const content = await
|
|
1707
|
+
const content = await readFile5(file, "utf-8");
|
|
1793
1708
|
const raw = parseYaml(content);
|
|
1794
1709
|
const scenario = validateScenario(raw, file);
|
|
1795
1710
|
scenarios.push(scenario);
|
|
@@ -1819,7 +1734,7 @@ async function listYamlFiles(dir) {
|
|
|
1819
1734
|
}
|
|
1820
1735
|
|
|
1821
1736
|
// src/commands/eval/runner.ts
|
|
1822
|
-
import { mkdir as mkdir3, writeFile as writeFile3, readFile as
|
|
1737
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile6, readdir as readdir4, rm } from "fs/promises";
|
|
1823
1738
|
import { join as join8, dirname as dirname3 } from "path";
|
|
1824
1739
|
import { tmpdir } from "os";
|
|
1825
1740
|
import { randomUUID } from "crypto";
|
|
@@ -1972,7 +1887,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
|
|
|
1972
1887
|
async function checkGrep(check, sandboxDir) {
|
|
1973
1888
|
if (!check.pattern) return false;
|
|
1974
1889
|
try {
|
|
1975
|
-
const content = await
|
|
1890
|
+
const content = await readFile6(join8(sandboxDir, check.target), "utf-8");
|
|
1976
1891
|
let found;
|
|
1977
1892
|
try {
|
|
1978
1893
|
found = new RegExp(check.pattern).test(content);
|
|
@@ -1986,7 +1901,7 @@ async function checkGrep(check, sandboxDir) {
|
|
|
1986
1901
|
}
|
|
1987
1902
|
async function checkFileExists(check, sandboxDir) {
|
|
1988
1903
|
try {
|
|
1989
|
-
await
|
|
1904
|
+
await readFile6(join8(sandboxDir, check.target));
|
|
1990
1905
|
return check.expect === "present";
|
|
1991
1906
|
} catch {
|
|
1992
1907
|
return check.expect === "absent";
|
|
@@ -1994,7 +1909,7 @@ async function checkFileExists(check, sandboxDir) {
|
|
|
1994
1909
|
}
|
|
1995
1910
|
async function checkFileAbsent(check, sandboxDir) {
|
|
1996
1911
|
try {
|
|
1997
|
-
await
|
|
1912
|
+
await readFile6(join8(sandboxDir, check.target));
|
|
1998
1913
|
return check.expect === "absent";
|
|
1999
1914
|
} catch {
|
|
2000
1915
|
return check.expect === "present";
|
|
@@ -2005,7 +1920,7 @@ async function checkMaxLines(check, sandboxDir) {
|
|
|
2005
1920
|
try {
|
|
2006
1921
|
const files = await listAllFiles(join8(sandboxDir, check.target));
|
|
2007
1922
|
for (const file of files) {
|
|
2008
|
-
const content = await
|
|
1923
|
+
const content = await readFile6(file, "utf-8");
|
|
2009
1924
|
if (content.split("\n").length > maxLines) {
|
|
2010
1925
|
return check.expect === "absent";
|
|
2011
1926
|
}
|
|
@@ -2145,7 +2060,7 @@ async function checkClaudeCli() {
|
|
|
2145
2060
|
import { Command as Command4 } from "commander";
|
|
2146
2061
|
import { spawn, execFile as execFile2 } from "child_process";
|
|
2147
2062
|
import { promisify as promisify2 } from "util";
|
|
2148
|
-
import { access as
|
|
2063
|
+
import { access as access6 } from "fs/promises";
|
|
2149
2064
|
import { join as join9 } from "path";
|
|
2150
2065
|
var execAsync = promisify2(execFile2);
|
|
2151
2066
|
var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
|
|
@@ -2163,6 +2078,12 @@ Sections to fill in or preserve (DO NOT remove any existing section):
|
|
|
2163
2078
|
5. **## Key Decisions** \u2014 only decisions that affect how Claude should work in this codebase
|
|
2164
2079
|
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.
|
|
2165
2080
|
|
|
2081
|
+
Also review .claude/settings.json hooks:
|
|
2082
|
+
- Read the existing hooks in .claude/settings.json
|
|
2083
|
+
- If you see project-specific patterns that deserve hooks (e.g., protected directories, test file patterns, migration files), suggest adding them
|
|
2084
|
+
- DO NOT overwrite existing hooks \u2014 only add new ones that are specific to this project
|
|
2085
|
+
- Print hook suggestions at the end with the exact JSON to add, don't modify settings.json directly
|
|
2086
|
+
|
|
2166
2087
|
Rules:
|
|
2167
2088
|
- Don't remove existing content \u2014 only add or improve
|
|
2168
2089
|
- Be specific to THIS project, not generic advice
|
|
@@ -2175,7 +2096,7 @@ function createEnhanceCommand() {
|
|
|
2175
2096
|
const root = opts.path;
|
|
2176
2097
|
const claudeMdPath = join9(root, "CLAUDE.md");
|
|
2177
2098
|
try {
|
|
2178
|
-
await
|
|
2099
|
+
await access6(claudeMdPath);
|
|
2179
2100
|
} catch {
|
|
2180
2101
|
log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
|
|
2181
2102
|
process.exit(1);
|
|
@@ -2202,8 +2123,8 @@ function createEnhanceCommand() {
|
|
|
2202
2123
|
}
|
|
2203
2124
|
|
|
2204
2125
|
// src/cli.ts
|
|
2205
|
-
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.
|
|
2206
|
-
const hasConfig = await
|
|
2126
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.2", "-v, --version").action(async () => {
|
|
2127
|
+
const hasConfig = await fileExists(join10(process.cwd(), "CLAUDE.md")) || await fileExists(join10(process.cwd(), ".claude", "settings.json"));
|
|
2207
2128
|
if (hasConfig) {
|
|
2208
2129
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
2209
2130
|
} else {
|
|
@@ -2221,12 +2142,4 @@ program.addCommand(createDoctorCommand());
|
|
|
2221
2142
|
program.addCommand(createEnhanceCommand());
|
|
2222
2143
|
program.addCommand(createEvalCommand());
|
|
2223
2144
|
program.parse();
|
|
2224
|
-
async function fileExists4(path) {
|
|
2225
|
-
try {
|
|
2226
|
-
await access8(path);
|
|
2227
|
-
return true;
|
|
2228
|
-
} catch {
|
|
2229
|
-
return false;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
2145
|
//# sourceMappingURL=cli.js.map
|