claude-launchpad 0.15.2 → 0.16.1
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 +3 -3
- package/dist/{chunk-4D3EBDNB.js → chunk-24VLPHJU.js} +16 -2
- package/dist/chunk-24VLPHJU.js.map +1 -0
- package/dist/{chunk-Z6FBT44W.js → chunk-IZFQTQ4C.js} +2 -2
- package/dist/{chunk-IPVN6SO4.js → chunk-JQDMBE7W.js} +14 -65
- package/dist/chunk-JQDMBE7W.js.map +1 -0
- package/dist/{chunk-EDKY7JWY.js → chunk-NLZGJHE5.js} +3 -3
- package/dist/chunk-S4WIADYE.js +1139 -0
- package/dist/chunk-S4WIADYE.js.map +1 -0
- package/dist/{chunk-AHEX2RG7.js → chunk-SVWFSAYP.js} +3 -3
- package/dist/chunk-TSTTFR4B.js +68 -0
- package/dist/chunk-TSTTFR4B.js.map +1 -0
- package/dist/cli.js +224 -991
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +4 -4
- package/dist/{context-XPXKLF5V.js → context-53PKQOMI.js} +6 -6
- package/dist/{install-UXO5BISS.js → install-PFTFTNIF.js} +57 -16
- package/dist/install-PFTFTNIF.js.map +1 -0
- package/dist/{pull-3O4B6EL2.js → pull-4QTS57DQ.js} +13 -11
- package/dist/{pull-3O4B6EL2.js.map → pull-4QTS57DQ.js.map} +1 -1
- package/dist/{push-BGN3DVV5.js → push-JOCEW3VG.js} +22 -12
- package/dist/push-JOCEW3VG.js.map +1 -0
- package/dist/{require-deps-NKRCPVAO.js → require-deps-SUGLVBM2.js} +3 -3
- package/dist/{stats-P7S3FTCQ.js → stats-7WFCVXBX.js} +7 -7
- package/dist/{tui-GUCKDWDM.js → tui-DKWRY5PT.js} +116 -49
- package/dist/tui-DKWRY5PT.js.map +1 -0
- package/package.json +3 -2
- package/dist/chunk-4D3EBDNB.js.map +0 -1
- package/dist/chunk-IPVN6SO4.js.map +0 -1
- package/dist/chunk-KOSJII4R.js +0 -62
- package/dist/chunk-KOSJII4R.js.map +0 -1
- package/dist/chunk-RJGXPH7P.js +0 -107
- package/dist/chunk-RJGXPH7P.js.map +0 -1
- package/dist/install-UXO5BISS.js.map +0 -1
- package/dist/push-BGN3DVV5.js.map +0 -1
- package/dist/tui-GUCKDWDM.js.map +0 -1
- /package/dist/{chunk-Z6FBT44W.js.map → chunk-IZFQTQ4C.js.map} +0 -0
- /package/dist/{chunk-EDKY7JWY.js.map → chunk-NLZGJHE5.js.map} +0 -0
- /package/dist/{chunk-AHEX2RG7.js.map → chunk-SVWFSAYP.js.map} +0 -0
- /package/dist/{context-XPXKLF5V.js.map → context-53PKQOMI.js.map} +0 -0
- /package/dist/{require-deps-NKRCPVAO.js.map → require-deps-SUGLVBM2.js.map} +0 -0
- /package/dist/{stats-P7S3FTCQ.js.map → stats-7WFCVXBX.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,231 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readSettingsLocalJson,
|
|
6
|
-
writeSettingsJson,
|
|
7
|
-
writeSettingsLocalJson
|
|
8
|
-
} from "./chunk-KOSJII4R.js";
|
|
3
|
+
readSyncConfig
|
|
4
|
+
} from "./chunk-JQDMBE7W.js";
|
|
9
5
|
import {
|
|
6
|
+
ENHANCE_SKILL_VERSION,
|
|
7
|
+
applyFixes,
|
|
8
|
+
detectProject,
|
|
9
|
+
fileExists,
|
|
10
|
+
generateClaudeignore,
|
|
11
|
+
generateEnhanceSkill,
|
|
10
12
|
log,
|
|
11
13
|
printBanner,
|
|
12
14
|
printScoreCard,
|
|
15
|
+
readFileOrNull,
|
|
13
16
|
renderDoctorReport
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-S4WIADYE.js";
|
|
15
18
|
|
|
16
19
|
// src/cli.ts
|
|
17
20
|
import { Command as Command5 } from "commander";
|
|
18
|
-
import { join as
|
|
21
|
+
import { join as join9 } from "path";
|
|
19
22
|
|
|
20
23
|
// src/commands/init/index.ts
|
|
21
24
|
import { Command } from "commander";
|
|
22
25
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
23
|
-
import { writeFile, mkdir, readFile
|
|
26
|
+
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
24
27
|
import { homedir } from "os";
|
|
25
|
-
import { join
|
|
26
|
-
|
|
27
|
-
// src/lib/fs-utils.ts
|
|
28
|
-
import { readFile, access } from "fs/promises";
|
|
29
|
-
async function fileExists(path) {
|
|
30
|
-
try {
|
|
31
|
-
await access(path);
|
|
32
|
-
return true;
|
|
33
|
-
} catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function readFileOrNull(path) {
|
|
38
|
-
try {
|
|
39
|
-
return await readFile(path, "utf-8");
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async function readJsonOrNull(path) {
|
|
45
|
-
try {
|
|
46
|
-
const content = await readFile(path, "utf-8");
|
|
47
|
-
return JSON.parse(content);
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/lib/detect.ts
|
|
54
|
-
import { join, basename } from "path";
|
|
55
|
-
async function detectProject(root) {
|
|
56
|
-
const name = basename(root);
|
|
57
|
-
const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradleGroovy, buildGradleKts, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
|
|
58
|
-
readJsonOrNull(join(root, "package.json")),
|
|
59
|
-
fileExists(join(root, "go.mod")),
|
|
60
|
-
readFileOrNull(join(root, "pyproject.toml")),
|
|
61
|
-
fileExists(join(root, "Gemfile")),
|
|
62
|
-
fileExists(join(root, "Cargo.toml")),
|
|
63
|
-
fileExists(join(root, "pubspec.yaml")),
|
|
64
|
-
readJsonOrNull(join(root, "composer.json")),
|
|
65
|
-
fileExists(join(root, "pom.xml")),
|
|
66
|
-
fileExists(join(root, "build.gradle")),
|
|
67
|
-
fileExists(join(root, "build.gradle.kts")),
|
|
68
|
-
fileExists(join(root, "Package.swift")),
|
|
69
|
-
fileExists(join(root, "mix.exs")),
|
|
70
|
-
globExists(root, "*.csproj"),
|
|
71
|
-
detectLockfiles(root)
|
|
72
|
-
]);
|
|
73
|
-
const buildGradle = buildGradleGroovy || buildGradleKts;
|
|
74
|
-
const manifests = {
|
|
75
|
-
pkgJson,
|
|
76
|
-
goMod,
|
|
77
|
-
pyProject,
|
|
78
|
-
gemfile,
|
|
79
|
-
cargo,
|
|
80
|
-
pubspec,
|
|
81
|
-
composerJson,
|
|
82
|
-
pomXml,
|
|
83
|
-
buildGradle,
|
|
84
|
-
packageSwift,
|
|
85
|
-
mixExs,
|
|
86
|
-
csproj
|
|
87
|
-
};
|
|
88
|
-
const language = detectLanguage(manifests);
|
|
89
|
-
const framework = detectFramework(manifests);
|
|
90
|
-
const packageManager = detectPackageManager(manifests, lockfiles);
|
|
91
|
-
const scripts = detectScripts({ pkgJson, pyProject, goMod, gemfile, composerJson, language });
|
|
92
|
-
return {
|
|
93
|
-
name,
|
|
94
|
-
language,
|
|
95
|
-
framework,
|
|
96
|
-
packageManager,
|
|
97
|
-
hasTests: scripts.testCommand !== null,
|
|
98
|
-
hasLinter: scripts.lintCommand !== null,
|
|
99
|
-
hasFormatter: scripts.formatCommand !== null,
|
|
100
|
-
...scripts
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function detectLanguage(m) {
|
|
104
|
-
if (m.pkgJson?.devDependencies?.typescript || m.pkgJson?.dependencies?.typescript) return "TypeScript";
|
|
105
|
-
if (m.pkgJson) return "JavaScript";
|
|
106
|
-
if (m.goMod) return "Go";
|
|
107
|
-
if (m.pyProject) return "Python";
|
|
108
|
-
if (m.gemfile) return "Ruby";
|
|
109
|
-
if (m.cargo) return "Rust";
|
|
110
|
-
if (m.pubspec) return "Dart";
|
|
111
|
-
if (m.composerJson) return "PHP";
|
|
112
|
-
if (m.buildGradle) return "Kotlin";
|
|
113
|
-
if (m.pomXml) return "Java";
|
|
114
|
-
if (m.packageSwift) return "Swift";
|
|
115
|
-
if (m.mixExs) return "Elixir";
|
|
116
|
-
if (m.csproj) return "C#";
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
function detectFramework(m) {
|
|
120
|
-
const deps = { ...m.pkgJson?.dependencies, ...m.pkgJson?.devDependencies };
|
|
121
|
-
if (deps.next) return "Next.js";
|
|
122
|
-
if (deps.nuxt) return "Nuxt";
|
|
123
|
-
if (deps.svelte || deps["@sveltejs/kit"]) return "SvelteKit";
|
|
124
|
-
if (deps.astro) return "Astro";
|
|
125
|
-
if (deps["@angular/core"]) return "Angular";
|
|
126
|
-
if (deps.remix || deps["@remix-run/react"]) return "Remix";
|
|
127
|
-
if (deps.vue) return "Vue";
|
|
128
|
-
if (deps.react && !deps.next) return "React";
|
|
129
|
-
if (deps.express) return "Express";
|
|
130
|
-
if (deps.fastify) return "Fastify";
|
|
131
|
-
if (deps.hono) return "Hono";
|
|
132
|
-
if (deps.nestjs || deps["@nestjs/core"]) return "NestJS";
|
|
133
|
-
if (m.pyProject) {
|
|
134
|
-
if (m.pyProject.includes("fastapi")) return "FastAPI";
|
|
135
|
-
if (m.pyProject.includes("django")) return "Django";
|
|
136
|
-
if (m.pyProject.includes("flask")) return "Flask";
|
|
137
|
-
}
|
|
138
|
-
if (m.composerJson) {
|
|
139
|
-
const phpDeps = { ...m.composerJson.require, ...m.composerJson["require-dev"] };
|
|
140
|
-
if (phpDeps["laravel/framework"]) return "Laravel";
|
|
141
|
-
if (phpDeps["symfony/framework-bundle"]) return "Symfony";
|
|
142
|
-
}
|
|
143
|
-
if (m.gemfile) return "Rails";
|
|
144
|
-
if (m.buildGradle) return "Gradle";
|
|
145
|
-
if (m.pomXml) return "Maven";
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
async function detectLockfiles(root) {
|
|
149
|
-
const [pnpmLock, yarnLock, bunLock, npmLock] = await Promise.all([
|
|
150
|
-
fileExists(join(root, "pnpm-lock.yaml")),
|
|
151
|
-
fileExists(join(root, "yarn.lock")),
|
|
152
|
-
fileExists(join(root, "bun.lockb")),
|
|
153
|
-
fileExists(join(root, "package-lock.json"))
|
|
154
|
-
]);
|
|
155
|
-
return { pnpmLock, yarnLock, bunLock, npmLock };
|
|
156
|
-
}
|
|
157
|
-
function detectPackageManager(m, lockfiles) {
|
|
158
|
-
if (m.pkgJson) {
|
|
159
|
-
const pm = m.pkgJson.packageManager;
|
|
160
|
-
if (pm?.startsWith("pnpm")) return "pnpm";
|
|
161
|
-
if (pm?.startsWith("yarn")) return "yarn";
|
|
162
|
-
if (pm?.startsWith("bun")) return "bun";
|
|
163
|
-
if (pm?.startsWith("npm")) return "npm";
|
|
164
|
-
if (lockfiles.pnpmLock) return "pnpm";
|
|
165
|
-
if (lockfiles.yarnLock) return "yarn";
|
|
166
|
-
if (lockfiles.bunLock) return "bun";
|
|
167
|
-
if (lockfiles.npmLock) return "npm";
|
|
168
|
-
return "npm";
|
|
169
|
-
}
|
|
170
|
-
if (m.goMod) return "go modules";
|
|
171
|
-
if (m.pyProject) {
|
|
172
|
-
if (m.pyProject.includes("[tool.uv]")) return "uv";
|
|
173
|
-
if (m.pyProject.includes("[tool.poetry]")) return "poetry";
|
|
174
|
-
return "pip";
|
|
175
|
-
}
|
|
176
|
-
if (m.gemfile) return "bundler";
|
|
177
|
-
if (m.cargo) return "cargo";
|
|
178
|
-
if (m.composerJson) return "composer";
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
var LANGUAGE_SCRIPTS = {
|
|
182
|
-
Go: { devCommand: "go run .", buildCommand: "go build .", testCommand: "go test ./...", lintCommand: "golangci-lint run", formatCommand: "gofmt -w ." },
|
|
183
|
-
Ruby: { devCommand: "bin/dev", buildCommand: null, testCommand: "bin/rails test", lintCommand: "bin/rubocop", formatCommand: null },
|
|
184
|
-
PHP: { devCommand: "php artisan serve", buildCommand: null, testCommand: "php artisan test", lintCommand: "vendor/bin/phpstan analyse", formatCommand: "vendor/bin/pint" },
|
|
185
|
-
Rust: { devCommand: "cargo run", buildCommand: "cargo build", testCommand: "cargo test", lintCommand: "cargo clippy", formatCommand: "cargo fmt" },
|
|
186
|
-
Java: { devCommand: null, buildCommand: "mvn package", testCommand: "mvn test", lintCommand: null, formatCommand: null },
|
|
187
|
-
Kotlin: { devCommand: null, buildCommand: "mvn package", testCommand: "mvn test", lintCommand: null, formatCommand: null },
|
|
188
|
-
Swift: { devCommand: null, buildCommand: "swift build", testCommand: "swift test", lintCommand: "swiftlint", formatCommand: "swift-format format -r ." },
|
|
189
|
-
Elixir: { devCommand: "mix phx.server", buildCommand: "mix compile", testCommand: "mix test", lintCommand: "mix credo", formatCommand: "mix format" },
|
|
190
|
-
"C#": { devCommand: "dotnet run", buildCommand: "dotnet build", testCommand: "dotnet test", lintCommand: null, formatCommand: "dotnet format" }
|
|
191
|
-
};
|
|
192
|
-
function detectScripts(m) {
|
|
193
|
-
if (m.pkgJson) {
|
|
194
|
-
const scripts = m.pkgJson.scripts ?? {};
|
|
195
|
-
const run = pmRun(m.pkgJson);
|
|
196
|
-
return {
|
|
197
|
-
devCommand: scripts.dev ? `${run} dev` : null,
|
|
198
|
-
buildCommand: scripts.build ? `${run} build` : null,
|
|
199
|
-
testCommand: scripts.test ? `${run} test` : null,
|
|
200
|
-
lintCommand: scripts.lint ? `${run} lint` : null,
|
|
201
|
-
formatCommand: scripts.format ? `${run} format` : null
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
if (m.language === "Python") {
|
|
205
|
-
const r = m.pyProject?.includes("[tool.uv]") ? "uv run" : "python -m";
|
|
206
|
-
return { devCommand: null, buildCommand: null, testCommand: `${r} pytest`, lintCommand: `${r} ruff check .`, formatCommand: `${r} ruff format .` };
|
|
207
|
-
}
|
|
208
|
-
if (m.language && LANGUAGE_SCRIPTS[m.language]) {
|
|
209
|
-
return LANGUAGE_SCRIPTS[m.language];
|
|
210
|
-
}
|
|
211
|
-
return { devCommand: null, buildCommand: null, testCommand: null, lintCommand: null, formatCommand: null };
|
|
212
|
-
}
|
|
213
|
-
function pmRun(pkg) {
|
|
214
|
-
const pm = pkg.packageManager;
|
|
215
|
-
if (pm?.startsWith("pnpm")) return "pnpm";
|
|
216
|
-
if (pm?.startsWith("yarn")) return "yarn";
|
|
217
|
-
if (pm?.startsWith("bun")) return "bun";
|
|
218
|
-
return "npm run";
|
|
219
|
-
}
|
|
220
|
-
async function globExists(dir, pattern) {
|
|
221
|
-
const { readdir: readdir5 } = await import("fs/promises");
|
|
222
|
-
try {
|
|
223
|
-
const entries = await readdir5(dir);
|
|
224
|
-
return entries.some((e) => e.endsWith(pattern.replace("*", "")));
|
|
225
|
-
} catch {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
28
|
+
import { join } from "path";
|
|
229
29
|
|
|
230
30
|
// src/commands/init/generators/claude-md.ts
|
|
231
31
|
function generateClaudeMd(options, detected) {
|
|
@@ -400,334 +200,6 @@ function buildFormatHook(detected) {
|
|
|
400
200
|
};
|
|
401
201
|
}
|
|
402
202
|
|
|
403
|
-
// src/commands/init/generators/claudeignore.ts
|
|
404
|
-
function generateClaudeignore(detected) {
|
|
405
|
-
const sections = ["# Generated by claude-launchpad"];
|
|
406
|
-
sections.push(`
|
|
407
|
-
# Dependencies
|
|
408
|
-
node_modules/
|
|
409
|
-
.pnp/
|
|
410
|
-
.yarn/
|
|
411
|
-
|
|
412
|
-
# Build output
|
|
413
|
-
dist/
|
|
414
|
-
build/
|
|
415
|
-
out/
|
|
416
|
-
.next/
|
|
417
|
-
.nuxt/
|
|
418
|
-
.output/
|
|
419
|
-
.svelte-kit/
|
|
420
|
-
.vercel/
|
|
421
|
-
.turbo/
|
|
422
|
-
|
|
423
|
-
# Package manager
|
|
424
|
-
pnpm-lock.yaml
|
|
425
|
-
package-lock.json
|
|
426
|
-
yarn.lock
|
|
427
|
-
bun.lockb
|
|
428
|
-
|
|
429
|
-
# IDE & OS
|
|
430
|
-
.vscode/
|
|
431
|
-
.idea/
|
|
432
|
-
*.swp
|
|
433
|
-
*.swo
|
|
434
|
-
.DS_Store
|
|
435
|
-
Thumbs.db
|
|
436
|
-
|
|
437
|
-
# Test & coverage
|
|
438
|
-
coverage/
|
|
439
|
-
.nyc_output/
|
|
440
|
-
__snapshots__/
|
|
441
|
-
|
|
442
|
-
# Environment (should never be read)
|
|
443
|
-
.env
|
|
444
|
-
.env.*
|
|
445
|
-
!.env.example`);
|
|
446
|
-
const lang = detected.language;
|
|
447
|
-
if (lang === "Python") {
|
|
448
|
-
sections.push(`
|
|
449
|
-
# Python
|
|
450
|
-
__pycache__/
|
|
451
|
-
*.pyc
|
|
452
|
-
*.pyo
|
|
453
|
-
.venv/
|
|
454
|
-
venv/
|
|
455
|
-
.mypy_cache/
|
|
456
|
-
.ruff_cache/
|
|
457
|
-
.pytest_cache/
|
|
458
|
-
*.egg-info/`);
|
|
459
|
-
}
|
|
460
|
-
if (lang === "Go") {
|
|
461
|
-
sections.push(`
|
|
462
|
-
# Go
|
|
463
|
-
bin/
|
|
464
|
-
vendor/`);
|
|
465
|
-
}
|
|
466
|
-
if (lang === "Rust") {
|
|
467
|
-
sections.push(`
|
|
468
|
-
# Rust
|
|
469
|
-
target/
|
|
470
|
-
Cargo.lock`);
|
|
471
|
-
}
|
|
472
|
-
if (lang === "Ruby") {
|
|
473
|
-
sections.push(`
|
|
474
|
-
# Ruby
|
|
475
|
-
vendor/bundle/
|
|
476
|
-
.bundle/
|
|
477
|
-
tmp/
|
|
478
|
-
log/`);
|
|
479
|
-
}
|
|
480
|
-
if (lang === "Java" || lang === "Kotlin") {
|
|
481
|
-
sections.push(`
|
|
482
|
-
# JVM
|
|
483
|
-
target/
|
|
484
|
-
build/
|
|
485
|
-
.gradle/
|
|
486
|
-
*.class
|
|
487
|
-
*.jar`);
|
|
488
|
-
}
|
|
489
|
-
if (lang === "Dart") {
|
|
490
|
-
sections.push(`
|
|
491
|
-
# Dart/Flutter
|
|
492
|
-
.dart_tool/
|
|
493
|
-
.packages
|
|
494
|
-
build/`);
|
|
495
|
-
}
|
|
496
|
-
if (lang === "PHP") {
|
|
497
|
-
sections.push(`
|
|
498
|
-
# PHP
|
|
499
|
-
vendor/
|
|
500
|
-
composer.lock`);
|
|
501
|
-
}
|
|
502
|
-
if (lang === "C#") {
|
|
503
|
-
sections.push(`
|
|
504
|
-
# .NET
|
|
505
|
-
bin/
|
|
506
|
-
obj/
|
|
507
|
-
*.dll`);
|
|
508
|
-
}
|
|
509
|
-
if (lang === "Elixir") {
|
|
510
|
-
sections.push(`
|
|
511
|
-
# Elixir
|
|
512
|
-
_build/
|
|
513
|
-
deps/
|
|
514
|
-
.elixir_ls/`);
|
|
515
|
-
}
|
|
516
|
-
if (lang === "Swift") {
|
|
517
|
-
sections.push(`
|
|
518
|
-
# Swift
|
|
519
|
-
.build/
|
|
520
|
-
DerivedData/
|
|
521
|
-
*.xcuserdata`);
|
|
522
|
-
}
|
|
523
|
-
return sections.join("\n") + "\n";
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/commands/init/generators/skill-enhance.ts
|
|
527
|
-
var ENHANCE_SKILL_VERSION = 5;
|
|
528
|
-
function generateEnhanceSkill() {
|
|
529
|
-
return [
|
|
530
|
-
"---",
|
|
531
|
-
"name: lp-enhance",
|
|
532
|
-
"description: |",
|
|
533
|
-
" AI-improve your CLAUDE.md based on codebase analysis. Fills in architecture, conventions, guardrails, and suggests hooks and MCP servers.",
|
|
534
|
-
' TRIGGER when: user runs /lp-enhance, asks to "improve CLAUDE.md", "fill in architecture", or after major refactors.',
|
|
535
|
-
" DO NOT TRIGGER when: user is editing CLAUDE.md manually, doing normal coding, or running doctor/eval.",
|
|
536
|
-
"allowed-tools: Read, Glob, Grep, Edit, Write",
|
|
537
|
-
"argument-hint: (no arguments needed)",
|
|
538
|
-
"---",
|
|
539
|
-
"",
|
|
540
|
-
`<!-- lp-enhance-version: ${ENHANCE_SKILL_VERSION} -->`,
|
|
541
|
-
"",
|
|
542
|
-
"# lp-enhance - AI-powered CLAUDE.md improver",
|
|
543
|
-
"",
|
|
544
|
-
"Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.",
|
|
545
|
-
"",
|
|
546
|
-
"## Phase 1: Research",
|
|
547
|
-
"",
|
|
548
|
-
"1. Read CLAUDE.md (if it exists)",
|
|
549
|
-
"2. Read .claude/settings.json (hooks, permissions, MCP)",
|
|
550
|
-
"3. Read .claude/rules/*.md (existing rules)",
|
|
551
|
-
"4. Read .claudeignore (if it exists)",
|
|
552
|
-
"5. Scan src/ directory structure (top-level dirs, key files)",
|
|
553
|
-
"6. Read package.json / go.mod / pyproject.toml for stack detection",
|
|
554
|
-
"7. Check for monorepo indicators (workspaces, nx.json, lerna.json)",
|
|
555
|
-
"8. Check scenarios/ directory for existing eval scenarios",
|
|
556
|
-
"",
|
|
557
|
-
"**Done when:** you have a mental model of the stack, architecture, and existing config.",
|
|
558
|
-
"",
|
|
559
|
-
"## Phase 2: Plan",
|
|
560
|
-
"",
|
|
561
|
-
"Count current CLAUDE.md actionable lines. Budget is 200 lines max. Plan which sections to add or improve:",
|
|
562
|
-
"",
|
|
563
|
-
"1. **## Stack** - detect language, framework, package manager",
|
|
564
|
-
"2. **## Architecture** - 3-5 bullets describing codebase shape",
|
|
565
|
-
"3. **## Conventions** - max 8 key patterns. Overflow to .claude/rules/conventions.md",
|
|
566
|
-
"4. **## Off-Limits** - max 8 guardrails specific to this project",
|
|
567
|
-
"5. **## Memory** - ONLY if agentic-memory is configured in settings.json. Max 6 bullets.",
|
|
568
|
-
"6. **## Key Decisions** - only decisions that affect how Claude works in this codebase",
|
|
569
|
-
"",
|
|
570
|
-
"7. **Skill Authoring** - if .claude/rules/conventions.md lacks a Skill Authoring section, plan to add one",
|
|
571
|
-
"",
|
|
572
|
-
"If any section would exceed 8 bullets, plan a .claude/rules/ file for the overflow.",
|
|
573
|
-
"",
|
|
574
|
-
"**Done when:** you know exactly what to add/change and the line count stays under 200.",
|
|
575
|
-
"",
|
|
576
|
-
"## Phase 3: Execute",
|
|
577
|
-
"",
|
|
578
|
-
"Edit CLAUDE.md with the planned changes. Then:",
|
|
579
|
-
"",
|
|
580
|
-
"1. Create or update .claude/rules/ files for overflow content",
|
|
581
|
-
"2. Generate path-scoped rules if the project has distinct areas (see below)",
|
|
582
|
-
"3. Review .claudeignore and print suggestions (see below)",
|
|
583
|
-
"4. Generate 2-3 custom eval scenarios in scenarios/custom/ (see below)",
|
|
584
|
-
"5. Verify line count is under 200",
|
|
585
|
-
"",
|
|
586
|
-
"**Rules:**",
|
|
587
|
-
"- Don't remove existing content, only add or improve",
|
|
588
|
-
"- Be specific to THIS project, not generic advice",
|
|
589
|
-
"- Use bullet points, not paragraphs",
|
|
590
|
-
"",
|
|
591
|
-
"## Phase 4: Verify",
|
|
592
|
-
"",
|
|
593
|
-
"1. Run `claude-launchpad doctor` to check the score improved",
|
|
594
|
-
"2. Print suggested hooks (exact JSON) for .claude/settings.json but don't modify it",
|
|
595
|
-
"3. Print suggested MCP servers if external services detected (Postgres, Redis, Stripe, etc.)",
|
|
596
|
-
'4. If eval scenarios were generated, print: "Run this in your terminal (not inside Claude Code): `claude-launchpad eval --scenarios scenarios/ --runs 1`"',
|
|
597
|
-
"",
|
|
598
|
-
"**Done when:** doctor score is equal or higher, suggestions printed, eval scenarios created if applicable.",
|
|
599
|
-
"",
|
|
600
|
-
"## Path-scoped rules generation",
|
|
601
|
-
"",
|
|
602
|
-
"Scan the project structure and generate focused .claude/rules/ files with paths: frontmatter. These load ONLY when Claude works on matching files, saving context tokens.",
|
|
603
|
-
"",
|
|
604
|
-
"**How to detect areas:**",
|
|
605
|
-
"1. List top-level directories under src/ (or equivalent). Each distinct area (api, components, lib, tests) is a candidate.",
|
|
606
|
-
"2. Check for monorepo indicators: workspaces in package.json, pnpm-workspace.yaml, nx.json, lerna.json. Each workspace is a candidate.",
|
|
607
|
-
"3. Check for docs/, tests/, scripts/ as separate scopes.",
|
|
608
|
-
"",
|
|
609
|
-
"**For each detected area, create a rules file with this format:**",
|
|
610
|
-
"",
|
|
611
|
-
"---",
|
|
612
|
-
'paths: ["src/api/**"]',
|
|
613
|
-
"---",
|
|
614
|
-
"# API Rules",
|
|
615
|
-
"- Validate all request input with zod schemas",
|
|
616
|
-
"- Return typed error responses, never throw raw errors",
|
|
617
|
-
"- Keep route handlers under 30 lines",
|
|
618
|
-
"",
|
|
619
|
-
"**Stack-specific patterns to include:**",
|
|
620
|
-
`- Next.js app/: "Use Server Components by default, add 'use client' only when needed"`,
|
|
621
|
-
'- API routes / src/api/: "Validate input at boundaries, typed error responses"',
|
|
622
|
-
'- React components: "Colocate components near usage, props interface above component"',
|
|
623
|
-
'- Tests: "One assertion per test when possible, descriptive test names"',
|
|
624
|
-
'- Database / prisma/ / drizzle/: "Never write raw SQL, use the ORM, migrations required"',
|
|
625
|
-
'- Docs: "No em dashes, max 3 sentences per paragraph, code examples required"',
|
|
626
|
-
"",
|
|
627
|
-
"**When NOT to generate:**",
|
|
628
|
-
"- Small projects with < 5 source files (one conventions.md is enough)",
|
|
629
|
-
"- Projects where all code is in one flat directory",
|
|
630
|
-
"- If path-scoped rules already exist, don't overwrite them",
|
|
631
|
-
"",
|
|
632
|
-
"**Monorepo handling:**",
|
|
633
|
-
"- Each package gets its own rules file: .claude/rules/packages-<name>.md",
|
|
634
|
-
"- Suggest claudeMdExcludes in settings.json to skip irrelevant package CLAUDE.md files",
|
|
635
|
-
"",
|
|
636
|
-
"## Skill authoring conventions",
|
|
637
|
-
"",
|
|
638
|
-
"If .claude/rules/conventions.md exists but has no Skill Authoring section, add this:",
|
|
639
|
-
"",
|
|
640
|
-
"## Skill Authoring",
|
|
641
|
-
"",
|
|
642
|
-
"When creating Claude Code skills (.claude/skills/*/SKILL.md):",
|
|
643
|
-
"",
|
|
644
|
-
"- Add TRIGGER when / DO NOT TRIGGER when clauses in the description for auto-invocation",
|
|
645
|
-
"- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)",
|
|
646
|
-
"- Add argument-hint in frontmatter showing the expected input format",
|
|
647
|
-
'- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase',
|
|
648
|
-
"- Handle edge cases and preconditions before execution",
|
|
649
|
-
"",
|
|
650
|
-
"## Hook review",
|
|
651
|
-
"",
|
|
652
|
-
"Review .claude/settings.json hooks:",
|
|
653
|
-
"- If you see project-specific patterns that deserve hooks, suggest them",
|
|
654
|
-
"- If no PostCompact hook exists, suggest one that re-injects TASKS.md",
|
|
655
|
-
"- If no SessionStart hook exists, suggest one that injects TASKS.md",
|
|
656
|
-
"- DO NOT modify settings.json directly. Print exact JSON to add.",
|
|
657
|
-
"",
|
|
658
|
-
"## .claudeignore review",
|
|
659
|
-
"",
|
|
660
|
-
"Read .claudeignore and check if the patterns make sense for the detected stack:",
|
|
661
|
-
"",
|
|
662
|
-
"**Always flag:**",
|
|
663
|
-
"- Missing node_modules/ (JS/TS projects)",
|
|
664
|
-
"- Missing __pycache__/ or .venv/ (Python projects)",
|
|
665
|
-
"- Missing target/ (Rust/Java projects)",
|
|
666
|
-
"- Missing .env / .env.* patterns",
|
|
667
|
-
"- Missing lock files (pnpm-lock.yaml, package-lock.json, yarn.lock, etc.)",
|
|
668
|
-
"- Missing coverage/ directory",
|
|
669
|
-
"- Large generated files that waste context (*.min.js, *.map, migrations/)",
|
|
670
|
-
"",
|
|
671
|
-
"**Never flag:**",
|
|
672
|
-
"- Patterns the user clearly added intentionally",
|
|
673
|
-
"- Test fixtures or seed data (might be needed for context)",
|
|
674
|
-
"",
|
|
675
|
-
"If .claudeignore is missing entirely, create one with sensible defaults for the detected stack.",
|
|
676
|
-
"If it exists but has gaps, print suggested additions. Do NOT modify it directly.",
|
|
677
|
-
"",
|
|
678
|
-
"## Eval scenario generation",
|
|
679
|
-
"",
|
|
680
|
-
"After improving CLAUDE.md, generate 2-3 custom eval scenarios that test whether Claude follows the project's specific rules. Write them as YAML files in scenarios/ at the project root.",
|
|
681
|
-
"",
|
|
682
|
-
"**Scenario YAML format:**",
|
|
683
|
-
"```yaml",
|
|
684
|
-
"name: custom/scenario-name",
|
|
685
|
-
"description: What this scenario tests",
|
|
686
|
-
"setup:",
|
|
687
|
-
" files:",
|
|
688
|
-
" - path: src/example.ts",
|
|
689
|
-
" content: |",
|
|
690
|
-
" // Starter file that tempts Claude to break a rule",
|
|
691
|
-
" instructions: |",
|
|
692
|
-
" The specific rule from CLAUDE.md being tested.",
|
|
693
|
-
'prompt: "A task that would tempt Claude to break the rule"',
|
|
694
|
-
"checks:",
|
|
695
|
-
" - type: grep",
|
|
696
|
-
' pattern: "expected_pattern"',
|
|
697
|
-
" target: src/example.ts",
|
|
698
|
-
" expect: present",
|
|
699
|
-
" points: 5",
|
|
700
|
-
" label: What this check verifies",
|
|
701
|
-
" - type: file-exists",
|
|
702
|
-
" target: path/to/expected/file",
|
|
703
|
-
" expect: present",
|
|
704
|
-
" points: 5",
|
|
705
|
-
" label: What this check verifies",
|
|
706
|
-
"passingScore: 7",
|
|
707
|
-
"runs: 3",
|
|
708
|
-
"```",
|
|
709
|
-
"",
|
|
710
|
-
"**How to choose scenarios:**",
|
|
711
|
-
"1. Pick the 2-3 most important rules from ## Off-Limits and ## Conventions",
|
|
712
|
-
"2. Design a task that naturally tempts Claude to break each rule",
|
|
713
|
-
"3. Write checks that verify compliance (grep for patterns, file-exists for structure)",
|
|
714
|
-
"",
|
|
715
|
-
"**Check types available:** `grep` (pattern in file), `file-exists` (present/absent), `max-lines` (file length)",
|
|
716
|
-
"",
|
|
717
|
-
"**Examples of good custom scenarios:**",
|
|
718
|
-
'- Off-limits says "never use any" \u2192 task asks to build types, check for no `any` keyword',
|
|
719
|
-
'- Convention says "max 400 lines per file" \u2192 task asks to generate a large module, check line count',
|
|
720
|
-
'- Off-limits says "no raw SQL" \u2192 task asks to add a query, check for ORM usage',
|
|
721
|
-
"",
|
|
722
|
-
"**Skip if:** scenarios/ already has 3+ YAML files, or CLAUDE.md has no project-specific rules worth testing.",
|
|
723
|
-
"",
|
|
724
|
-
"## Other advanced configuration",
|
|
725
|
-
"",
|
|
726
|
-
"- If the project uses external APIs, suggest sandbox.network.allowedDomains",
|
|
727
|
-
"- If you detect a monorepo, suggest claudeMdExcludes in settings.json"
|
|
728
|
-
].join("\n");
|
|
729
|
-
}
|
|
730
|
-
|
|
731
203
|
// src/commands/init/generators/backlog.ts
|
|
732
204
|
function generateBacklogMd(options) {
|
|
733
205
|
return `# ${options.name} - Backlog
|
|
@@ -766,7 +238,7 @@ function createInitCommand() {
|
|
|
766
238
|
message: "One-line description (optional):"
|
|
767
239
|
});
|
|
768
240
|
const options = { name: name.trim(), description: description.trim() };
|
|
769
|
-
const hasClaudeMd = await fileExists(
|
|
241
|
+
const hasClaudeMd = await fileExists(join(root, "CLAUDE.md"));
|
|
770
242
|
if (hasClaudeMd && !opts.yes) {
|
|
771
243
|
const overwrite = await confirm({
|
|
772
244
|
message: "CLAUDE.md already exists. Overwrite?",
|
|
@@ -789,20 +261,20 @@ async function scaffold(root, options, detected, skipPrompts) {
|
|
|
789
261
|
const backlogMd = generateBacklogMd(options);
|
|
790
262
|
const settings = generateSettings(detected);
|
|
791
263
|
const claudeignore = generateClaudeignore(detected);
|
|
792
|
-
await mkdir(
|
|
793
|
-
const settingsPath =
|
|
264
|
+
await mkdir(join(root, ".claude", "rules"), { recursive: true });
|
|
265
|
+
const settingsPath = join(root, ".claude", "settings.json");
|
|
794
266
|
const mergedSettings = await mergeSettings(settingsPath, settings);
|
|
795
|
-
const backlogPath =
|
|
267
|
+
const backlogPath = join(root, "BACKLOG.md");
|
|
796
268
|
const hasBacklog = await fileExists(backlogPath);
|
|
797
|
-
const claudeignorePath =
|
|
269
|
+
const claudeignorePath = join(root, ".claudeignore");
|
|
798
270
|
const hasClaudeignore = await fileExists(claudeignorePath);
|
|
799
|
-
const claudeGitignorePath =
|
|
271
|
+
const claudeGitignorePath = join(root, ".claude", ".gitignore");
|
|
800
272
|
const hasClaudeGitignore = await fileExists(claudeGitignorePath);
|
|
801
|
-
const rulesPath =
|
|
273
|
+
const rulesPath = join(root, ".claude", "rules", "conventions.md");
|
|
802
274
|
const hasRules = await fileExists(rulesPath);
|
|
803
275
|
const writes = [
|
|
804
|
-
writeFile(
|
|
805
|
-
writeFile(
|
|
276
|
+
writeFile(join(root, "CLAUDE.md"), claudeMd),
|
|
277
|
+
writeFile(join(root, "TASKS.md"), tasksMd),
|
|
806
278
|
writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n")
|
|
807
279
|
];
|
|
808
280
|
if (!hasBacklog) {
|
|
@@ -883,10 +355,10 @@ function generateStarterRules(detected) {
|
|
|
883
355
|
return lines.join("\n");
|
|
884
356
|
}
|
|
885
357
|
async function createEnhanceSkillPrompt(root, skipPrompts) {
|
|
886
|
-
const projectPath =
|
|
887
|
-
const globalPath =
|
|
888
|
-
const legacyProject =
|
|
889
|
-
const legacyGlobal =
|
|
358
|
+
const projectPath = join(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
359
|
+
const globalPath = join(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
360
|
+
const legacyProject = join(root, ".claude", "commands", "lp-enhance.md");
|
|
361
|
+
const legacyGlobal = join(homedir(), ".claude", "commands", "lp-enhance.md");
|
|
890
362
|
if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return;
|
|
891
363
|
const scope = skipPrompts ? "project" : await select({
|
|
892
364
|
message: "Install /lp-enhance skill (AI-powered CLAUDE.md improver):",
|
|
@@ -897,14 +369,14 @@ async function createEnhanceSkillPrompt(root, skipPrompts) {
|
|
|
897
369
|
]
|
|
898
370
|
});
|
|
899
371
|
if (scope === "skip") return;
|
|
900
|
-
const targetDir = scope === "global" ?
|
|
372
|
+
const targetDir = scope === "global" ? join(homedir(), ".claude", "skills", "lp-enhance") : join(root, ".claude", "skills", "lp-enhance");
|
|
901
373
|
await mkdir(targetDir, { recursive: true });
|
|
902
|
-
await writeFile(
|
|
374
|
+
await writeFile(join(targetDir, "SKILL.md"), generateEnhanceSkill());
|
|
903
375
|
log.success(`Generated /lp-enhance skill (${scope} scope)`);
|
|
904
376
|
}
|
|
905
377
|
async function mergeSettings(existingPath, generated) {
|
|
906
378
|
try {
|
|
907
|
-
const existing = JSON.parse(await
|
|
379
|
+
const existing = JSON.parse(await readFile(existingPath, "utf-8"));
|
|
908
380
|
const existingHooks = existing.hooks ?? {};
|
|
909
381
|
const generatedHooks = generated.hooks ?? {};
|
|
910
382
|
const mergedHooks = { ...existingHooks };
|
|
@@ -928,8 +400,8 @@ import { Command as Command2 } from "commander";
|
|
|
928
400
|
import chalk from "chalk";
|
|
929
401
|
|
|
930
402
|
// src/lib/parser.ts
|
|
931
|
-
import { readdir, access
|
|
932
|
-
import { join as
|
|
403
|
+
import { readdir, access } from "fs/promises";
|
|
404
|
+
import { join as join2, resolve } from "path";
|
|
933
405
|
var CLAUDE_MD = "CLAUDE.md";
|
|
934
406
|
var CLAUDE_DIR = ".claude";
|
|
935
407
|
var SETTINGS_FILE = "settings.json";
|
|
@@ -937,24 +409,24 @@ var SETTINGS_LOCAL_FILE = "settings.local.json";
|
|
|
937
409
|
var RULES_DIR = "rules";
|
|
938
410
|
async function parseClaudeConfig(projectRoot) {
|
|
939
411
|
const root = resolve(projectRoot);
|
|
940
|
-
const claudeDir =
|
|
412
|
+
const claudeDir = join2(root, CLAUDE_DIR);
|
|
941
413
|
const [claudeMd, localClaudeMd, settings, localSettings, hooks, rules, mcpServers, skills, claudeignore] = await Promise.all([
|
|
942
414
|
readClaudeMd(root),
|
|
943
|
-
readFileOrNull(
|
|
415
|
+
readFileOrNull(join2(claudeDir, CLAUDE_MD)),
|
|
944
416
|
readSettings(claudeDir),
|
|
945
417
|
readSettingsFromFile(claudeDir, SETTINGS_LOCAL_FILE),
|
|
946
418
|
readHooks(claudeDir),
|
|
947
419
|
readRules(claudeDir),
|
|
948
|
-
readMcpServers(claudeDir),
|
|
420
|
+
readMcpServers(claudeDir, root),
|
|
949
421
|
readSkills(claudeDir),
|
|
950
|
-
readFileOrNull(
|
|
422
|
+
readFileOrNull(join2(root, ".claudeignore"))
|
|
951
423
|
]);
|
|
952
424
|
const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
|
|
953
425
|
return {
|
|
954
|
-
claudeMdPath: claudeMd !== null ?
|
|
426
|
+
claudeMdPath: claudeMd !== null ? join2(root, CLAUDE_MD) : null,
|
|
955
427
|
claudeMdContent: claudeMd,
|
|
956
428
|
claudeMdInstructionCount: instructionCount,
|
|
957
|
-
settingsPath: settings !== null ?
|
|
429
|
+
settingsPath: settings !== null ? join2(claudeDir, SETTINGS_FILE) : null,
|
|
958
430
|
settings,
|
|
959
431
|
localClaudeMdContent: localClaudeMd,
|
|
960
432
|
localSettings,
|
|
@@ -962,12 +434,12 @@ async function parseClaudeConfig(projectRoot) {
|
|
|
962
434
|
rules,
|
|
963
435
|
mcpServers,
|
|
964
436
|
skills,
|
|
965
|
-
claudeignorePath: claudeignore !== null ?
|
|
437
|
+
claudeignorePath: claudeignore !== null ? join2(root, ".claudeignore") : null,
|
|
966
438
|
claudeignoreContent: claudeignore
|
|
967
439
|
};
|
|
968
440
|
}
|
|
969
441
|
async function readClaudeMd(root) {
|
|
970
|
-
return readFileOrNull(
|
|
442
|
+
return readFileOrNull(join2(root, CLAUDE_MD));
|
|
971
443
|
}
|
|
972
444
|
function countInstructions(content) {
|
|
973
445
|
const lines = content.split("\n");
|
|
@@ -986,7 +458,7 @@ async function readSettings(claudeDir) {
|
|
|
986
458
|
return readSettingsFromFile(claudeDir, SETTINGS_FILE);
|
|
987
459
|
}
|
|
988
460
|
async function readSettingsFromFile(claudeDir, filename) {
|
|
989
|
-
const raw = await readFileOrNull(
|
|
461
|
+
const raw = await readFileOrNull(join2(claudeDir, filename));
|
|
990
462
|
if (raw === null) return null;
|
|
991
463
|
try {
|
|
992
464
|
return JSON.parse(raw);
|
|
@@ -995,7 +467,14 @@ async function readSettingsFromFile(claudeDir, filename) {
|
|
|
995
467
|
}
|
|
996
468
|
}
|
|
997
469
|
async function readHooks(claudeDir) {
|
|
998
|
-
const
|
|
470
|
+
const [shared, local] = await Promise.all([
|
|
471
|
+
readHooksFromFile(join2(claudeDir, SETTINGS_FILE)),
|
|
472
|
+
readHooksFromFile(join2(claudeDir, SETTINGS_LOCAL_FILE))
|
|
473
|
+
]);
|
|
474
|
+
return [...shared, ...local];
|
|
475
|
+
}
|
|
476
|
+
async function readHooksFromFile(settingsPath) {
|
|
477
|
+
const settingsRaw = await readFileOrNull(settingsPath);
|
|
999
478
|
if (settingsRaw === null) return [];
|
|
1000
479
|
try {
|
|
1001
480
|
const settings = JSON.parse(settingsRaw);
|
|
@@ -1036,11 +515,49 @@ async function readHooks(claudeDir) {
|
|
|
1036
515
|
}
|
|
1037
516
|
}
|
|
1038
517
|
async function readRules(claudeDir) {
|
|
1039
|
-
const rulesDir =
|
|
518
|
+
const rulesDir = join2(claudeDir, RULES_DIR);
|
|
1040
519
|
return listFilesRecursive(rulesDir, ".md");
|
|
1041
520
|
}
|
|
1042
|
-
async function readMcpServers(claudeDir) {
|
|
1043
|
-
const
|
|
521
|
+
async function readMcpServers(claudeDir, projectRoot) {
|
|
522
|
+
const [fromMcpJson, fromSettings, fromLocalSettings] = await Promise.all([
|
|
523
|
+
readMcpJsonFile(join2(projectRoot, ".mcp.json")),
|
|
524
|
+
readMcpServersFromSettings(join2(claudeDir, SETTINGS_FILE)),
|
|
525
|
+
readMcpServersFromSettings(join2(claudeDir, SETTINGS_LOCAL_FILE))
|
|
526
|
+
]);
|
|
527
|
+
const seen = /* @__PURE__ */ new Set();
|
|
528
|
+
const result = [];
|
|
529
|
+
for (const server of [...fromMcpJson, ...fromLocalSettings, ...fromSettings]) {
|
|
530
|
+
if (!seen.has(server.name)) {
|
|
531
|
+
seen.add(server.name);
|
|
532
|
+
result.push(server);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
async function readMcpJsonFile(mcpJsonPath) {
|
|
538
|
+
const raw = await readFileOrNull(mcpJsonPath);
|
|
539
|
+
if (raw === null) return [];
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(raw);
|
|
542
|
+
const servers = parsed.mcpServers;
|
|
543
|
+
if (!servers || typeof servers !== "object") return [];
|
|
544
|
+
const result = [];
|
|
545
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
546
|
+
const c = config;
|
|
547
|
+
result.push({
|
|
548
|
+
name,
|
|
549
|
+
transport: c.transport ?? c.type ?? "stdio",
|
|
550
|
+
command: c.command,
|
|
551
|
+
url: c.url
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
} catch {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function readMcpServersFromSettings(settingsPath) {
|
|
560
|
+
const settingsRaw = await readFileOrNull(settingsPath);
|
|
1044
561
|
if (settingsRaw === null) return [];
|
|
1045
562
|
try {
|
|
1046
563
|
const settings = JSON.parse(settingsRaw);
|
|
@@ -1051,7 +568,7 @@ async function readMcpServers(claudeDir) {
|
|
|
1051
568
|
const c = config;
|
|
1052
569
|
result.push({
|
|
1053
570
|
name,
|
|
1054
|
-
transport: c.transport ?? "stdio",
|
|
571
|
+
transport: c.transport ?? c.type ?? "stdio",
|
|
1055
572
|
command: c.command,
|
|
1056
573
|
url: c.url
|
|
1057
574
|
});
|
|
@@ -1062,8 +579,8 @@ async function readMcpServers(claudeDir) {
|
|
|
1062
579
|
}
|
|
1063
580
|
}
|
|
1064
581
|
async function readSkills(claudeDir) {
|
|
1065
|
-
const commandsDir =
|
|
1066
|
-
const skillsDir =
|
|
582
|
+
const commandsDir = join2(claudeDir, "commands");
|
|
583
|
+
const skillsDir = join2(claudeDir, "skills");
|
|
1067
584
|
const [commands, skills] = await Promise.all([
|
|
1068
585
|
listFilesRecursive(commandsDir, ".md"),
|
|
1069
586
|
listFilesRecursive(skillsDir, ".md")
|
|
@@ -1072,14 +589,14 @@ async function readSkills(claudeDir) {
|
|
|
1072
589
|
}
|
|
1073
590
|
async function listFilesRecursive(dir, ext) {
|
|
1074
591
|
try {
|
|
1075
|
-
await
|
|
592
|
+
await access(dir);
|
|
1076
593
|
} catch {
|
|
1077
594
|
return [];
|
|
1078
595
|
}
|
|
1079
596
|
const results = [];
|
|
1080
597
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
1081
598
|
for (const entry of entries) {
|
|
1082
|
-
const fullPath =
|
|
599
|
+
const fullPath = join2(dir, entry.name);
|
|
1083
600
|
if (entry.isDirectory()) {
|
|
1084
601
|
const nested = await listFilesRecursive(fullPath, ext);
|
|
1085
602
|
results.push(...nested);
|
|
@@ -1279,13 +796,13 @@ async function analyzeHooks(config) {
|
|
|
1279
796
|
}
|
|
1280
797
|
|
|
1281
798
|
// src/commands/doctor/analyzers/rules.ts
|
|
1282
|
-
import { readFile as
|
|
1283
|
-
import { basename
|
|
799
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
800
|
+
import { basename, join as join3, dirname } from "path";
|
|
1284
801
|
import { homedir as homedir2 } from "os";
|
|
1285
802
|
async function analyzeRules(config) {
|
|
1286
803
|
const issues = [];
|
|
1287
804
|
const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
|
|
1288
|
-
const hasBacklog = await fileExists(
|
|
805
|
+
const hasBacklog = await fileExists(join3(projectRoot, "BACKLOG.md"));
|
|
1289
806
|
if (!hasBacklog) {
|
|
1290
807
|
issues.push({
|
|
1291
808
|
analyzer: "Rules",
|
|
@@ -1294,7 +811,7 @@ async function analyzeRules(config) {
|
|
|
1294
811
|
fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
|
|
1295
812
|
});
|
|
1296
813
|
}
|
|
1297
|
-
const hasClaudeignore = await fileExists(
|
|
814
|
+
const hasClaudeignore = await fileExists(join3(projectRoot, ".claudeignore"));
|
|
1298
815
|
if (!hasClaudeignore) {
|
|
1299
816
|
issues.push({
|
|
1300
817
|
analyzer: "Rules",
|
|
@@ -1304,9 +821,9 @@ async function analyzeRules(config) {
|
|
|
1304
821
|
});
|
|
1305
822
|
}
|
|
1306
823
|
const hasSkillInProject = config.skills.some(
|
|
1307
|
-
(s) =>
|
|
824
|
+
(s) => basename(s) === "SKILL.md" && s.includes("lp-enhance") || basename(s) === "lp-enhance.md"
|
|
1308
825
|
);
|
|
1309
|
-
const hasSkillGlobal = await fileExists(
|
|
826
|
+
const hasSkillGlobal = await fileExists(join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")) || await fileExists(join3(homedir2(), ".claude", "commands", "lp-enhance.md"));
|
|
1310
827
|
if (!hasSkillInProject && !hasSkillGlobal) {
|
|
1311
828
|
issues.push({
|
|
1312
829
|
analyzer: "Rules",
|
|
@@ -1336,27 +853,27 @@ async function analyzeRules(config) {
|
|
|
1336
853
|
}
|
|
1337
854
|
for (const rulePath of config.rules) {
|
|
1338
855
|
try {
|
|
1339
|
-
const content = await
|
|
856
|
+
const content = await readFile2(rulePath, "utf-8");
|
|
1340
857
|
const trimmed = content.trim();
|
|
1341
858
|
if (trimmed.length === 0) {
|
|
1342
859
|
issues.push({
|
|
1343
860
|
analyzer: "Rules",
|
|
1344
861
|
severity: "low",
|
|
1345
|
-
message: `Empty rule file: ${
|
|
1346
|
-
fix: `Add content to ${
|
|
862
|
+
message: `Empty rule file: ${basename(rulePath)}`,
|
|
863
|
+
fix: `Add content to ${basename(rulePath)} or delete it`
|
|
1347
864
|
});
|
|
1348
865
|
} else if (trimmed.length < 20) {
|
|
1349
866
|
issues.push({
|
|
1350
867
|
analyzer: "Rules",
|
|
1351
868
|
severity: "info",
|
|
1352
|
-
message: `Very short rule file (${trimmed.length} chars): ${
|
|
869
|
+
message: `Very short rule file (${trimmed.length} chars): ${basename(rulePath)}`
|
|
1353
870
|
});
|
|
1354
871
|
}
|
|
1355
872
|
} catch {
|
|
1356
873
|
issues.push({
|
|
1357
874
|
analyzer: "Rules",
|
|
1358
875
|
severity: "low",
|
|
1359
|
-
message: `Could not read rule file: ${
|
|
876
|
+
message: `Could not read rule file: ${basename(rulePath)}`
|
|
1360
877
|
});
|
|
1361
878
|
}
|
|
1362
879
|
}
|
|
@@ -1365,12 +882,12 @@ async function analyzeRules(config) {
|
|
|
1365
882
|
}
|
|
1366
883
|
async function getSkillVersion(projectRoot) {
|
|
1367
884
|
const paths = [
|
|
1368
|
-
|
|
1369
|
-
|
|
885
|
+
join3(projectRoot, ".claude", "skills", "lp-enhance", "SKILL.md"),
|
|
886
|
+
join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")
|
|
1370
887
|
];
|
|
1371
888
|
for (const p of paths) {
|
|
1372
889
|
try {
|
|
1373
|
-
const content = await
|
|
890
|
+
const content = await readFile2(p, "utf-8");
|
|
1374
891
|
const match = content.match(/<!-- lp-enhance-version: (\d+) -->/);
|
|
1375
892
|
if (match) return parseInt(match[1], 10);
|
|
1376
893
|
return 0;
|
|
@@ -1479,7 +996,7 @@ async function analyzePermissions(config) {
|
|
|
1479
996
|
}
|
|
1480
997
|
|
|
1481
998
|
// src/commands/doctor/analyzers/mcp.ts
|
|
1482
|
-
import { access as
|
|
999
|
+
import { access as access2 } from "fs/promises";
|
|
1483
1000
|
async function analyzeMcp(config) {
|
|
1484
1001
|
const issues = [];
|
|
1485
1002
|
const servers = config.mcpServers;
|
|
@@ -1512,7 +1029,7 @@ async function analyzeMcp(config) {
|
|
|
1512
1029
|
const executable = server.command.split(" ")[0];
|
|
1513
1030
|
if (executable.startsWith("/") || executable.startsWith("./")) {
|
|
1514
1031
|
try {
|
|
1515
|
-
await
|
|
1032
|
+
await access2(executable);
|
|
1516
1033
|
} catch {
|
|
1517
1034
|
issues.push({
|
|
1518
1035
|
analyzer: "MCP",
|
|
@@ -1524,6 +1041,32 @@ async function analyzeMcp(config) {
|
|
|
1524
1041
|
}
|
|
1525
1042
|
}
|
|
1526
1043
|
}
|
|
1044
|
+
if (servers.length > 0) {
|
|
1045
|
+
const settings = config.settings ?? {};
|
|
1046
|
+
const localSettings = config.localSettings ?? {};
|
|
1047
|
+
const hasAllowList = settings.allowedMcpServers || localSettings.allowedMcpServers;
|
|
1048
|
+
if (!hasAllowList) {
|
|
1049
|
+
issues.push({
|
|
1050
|
+
analyzer: "MCP",
|
|
1051
|
+
severity: "medium",
|
|
1052
|
+
message: "MCP servers configured but no allowedMcpServers list \u2014 any added server is auto-trusted",
|
|
1053
|
+
fix: "Add allowedMcpServers to settings.json listing only trusted server names"
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (servers.length > 0) {
|
|
1058
|
+
const sandbox = config.settings?.sandbox ?? config.localSettings?.sandbox;
|
|
1059
|
+
const hasNetwork = sandbox?.network !== void 0;
|
|
1060
|
+
const httpServers = servers.filter((s) => s.transport === "sse" || s.transport === "http");
|
|
1061
|
+
if (sandbox?.enabled === true && httpServers.length > 0 && !hasNetwork) {
|
|
1062
|
+
issues.push({
|
|
1063
|
+
analyzer: "MCP",
|
|
1064
|
+
severity: "low",
|
|
1065
|
+
message: "Sandbox enabled with HTTP MCP servers but no network restrictions configured",
|
|
1066
|
+
fix: "Add sandbox.network.allowedHosts to restrict which hosts MCP servers can reach"
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1527
1070
|
const score = Math.max(0, 100 - issues.filter((i) => i.severity !== "info").length * 25);
|
|
1528
1071
|
return { name: "MCP Servers", issues, score };
|
|
1529
1072
|
}
|
|
@@ -1539,7 +1082,16 @@ var MEMORY_MCP_TOOLS = [
|
|
|
1539
1082
|
"mcp__agentic-memory__memory_update"
|
|
1540
1083
|
];
|
|
1541
1084
|
function hasMemoryIndicators(config) {
|
|
1542
|
-
|
|
1085
|
+
if (config.mcpServers.some((s) => s.name === "agentic-memory")) return true;
|
|
1086
|
+
const permissions = config.settings?.permissions ?? {};
|
|
1087
|
+
const localPermissions = config.localSettings?.permissions ?? {};
|
|
1088
|
+
const allowList = [
|
|
1089
|
+
...permissions.allow ?? [],
|
|
1090
|
+
...localPermissions.allow ?? []
|
|
1091
|
+
];
|
|
1092
|
+
if (allowList.some((t) => t.startsWith("mcp__agentic-memory__"))) return true;
|
|
1093
|
+
if (config.hooks.some((h) => h.event === "SessionStart" && h.command?.includes("memory context"))) return true;
|
|
1094
|
+
return false;
|
|
1543
1095
|
}
|
|
1544
1096
|
async function analyzeMemory(config) {
|
|
1545
1097
|
if (!hasMemoryIndicators(config)) return null;
|
|
@@ -1599,6 +1151,31 @@ async function analyzeMemory(config) {
|
|
|
1599
1151
|
fix: "Add all agentic-memory tool names to allowedTools in .claude/settings.json"
|
|
1600
1152
|
});
|
|
1601
1153
|
}
|
|
1154
|
+
const syncConfig = readSyncConfig();
|
|
1155
|
+
if (syncConfig) {
|
|
1156
|
+
const hasSessionStartPull = config.hooks.some(
|
|
1157
|
+
(h) => h.event === "SessionStart" && h.command?.includes("memory pull")
|
|
1158
|
+
);
|
|
1159
|
+
if (!hasSessionStartPull) {
|
|
1160
|
+
issues.push({
|
|
1161
|
+
analyzer: "Memory",
|
|
1162
|
+
severity: "medium",
|
|
1163
|
+
message: "Sync configured but no SessionStart hook to auto-pull memories before context injection",
|
|
1164
|
+
fix: "Run `doctor --fix` to add a SessionStart hook that pulls memories automatically"
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const hasSessionEndPush = config.hooks.some(
|
|
1168
|
+
(h) => h.event === "SessionEnd" && h.command?.includes("memory push")
|
|
1169
|
+
);
|
|
1170
|
+
if (!hasSessionEndPush) {
|
|
1171
|
+
issues.push({
|
|
1172
|
+
analyzer: "Memory",
|
|
1173
|
+
severity: "medium",
|
|
1174
|
+
message: "Sync configured but no SessionEnd hook to auto-push memories after each session",
|
|
1175
|
+
fix: "Run `doctor --fix` to add a SessionEnd hook that pushes memories automatically"
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1602
1179
|
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
1603
1180
|
const high = issues.filter((i) => i.severity === "high").length;
|
|
1604
1181
|
const medium = issues.filter((i) => i.severity === "medium").length;
|
|
@@ -1693,353 +1270,9 @@ async function analyzeQuality(config) {
|
|
|
1693
1270
|
return { name: "CLAUDE.md Quality", issues, score };
|
|
1694
1271
|
}
|
|
1695
1272
|
|
|
1696
|
-
// src/commands/doctor/fixer.ts
|
|
1697
|
-
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2, access as access4 } from "fs/promises";
|
|
1698
|
-
import { join as join5 } from "path";
|
|
1699
|
-
import { homedir as homedir3 } from "os";
|
|
1700
|
-
async function applyFixes(issues, projectRoot) {
|
|
1701
|
-
const detected = await detectProject(projectRoot);
|
|
1702
|
-
const hasMemoryIssues = issues.some((i) => i.analyzer === "Memory");
|
|
1703
|
-
const placement = hasMemoryIssues ? await getMemoryPlacement(projectRoot) : "shared";
|
|
1704
|
-
let fixed = 0;
|
|
1705
|
-
let skipped = 0;
|
|
1706
|
-
for (const issue of issues) {
|
|
1707
|
-
const applied = await tryFix(issue, projectRoot, detected, placement);
|
|
1708
|
-
if (applied) {
|
|
1709
|
-
fixed++;
|
|
1710
|
-
} else {
|
|
1711
|
-
skipped++;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
return { fixed, skipped };
|
|
1715
|
-
}
|
|
1716
|
-
var FIX_TABLE = [
|
|
1717
|
-
{ analyzer: "Hooks", match: "No hooks configured", fix: async (root, detected) => {
|
|
1718
|
-
const a = await addEnvProtectionHook(root);
|
|
1719
|
-
const b = await addAutoFormatHook(root, detected);
|
|
1720
|
-
const c = await addForcePushProtection(root);
|
|
1721
|
-
return a || b || c;
|
|
1722
|
-
} },
|
|
1723
|
-
{ analyzer: "Hooks", match: ".env file protection", fix: (root) => addEnvProtectionHook(root) },
|
|
1724
|
-
{ analyzer: "Hooks", match: "auto-format", fix: (root, detected) => addAutoFormatHook(root, detected) },
|
|
1725
|
-
{ analyzer: "Hooks", match: "No PreToolUse", fix: (root) => addEnvProtectionHook(root) },
|
|
1726
|
-
{ analyzer: "Quality", match: "Architecture", fix: (root) => addClaudeMdSection(root, "## Architecture", "<!-- TODO: Describe your codebase structure. Run `/lp-enhance` to auto-fill this. -->") },
|
|
1727
|
-
{ analyzer: "Quality", match: "Off-Limits", fix: (root) => addClaudeMdSection(root, "## Off-Limits", "- Never hardcode secrets - use environment variables\n- Never write to `.env` files\n- Never expose internal error details in API responses") },
|
|
1728
|
-
{ analyzer: "Quality", match: "Commands", fix: (root) => addClaudeMdSection(root, "## Commands", "<!-- TODO: Add your dev/build/test commands -->") },
|
|
1729
|
-
{ analyzer: "Quality", match: "Stack", fix: (root, detected) => {
|
|
1730
|
-
const content = detected.language ? `- **Language**: ${detected.language}${detected.framework ? `
|
|
1731
|
-
- **Framework**: ${detected.framework}` : ""}${detected.packageManager ? `
|
|
1732
|
-
- **Package Manager**: ${detected.packageManager}` : ""}` : "<!-- TODO: Define your tech stack -->";
|
|
1733
|
-
return addClaudeMdSection(root, "## Stack", content);
|
|
1734
|
-
} },
|
|
1735
|
-
{ analyzer: "Quality", match: "Session Start", fix: (root) => addClaudeMdSection(root, "## Session Start", "- ALWAYS read @TASKS.md first - it tracks progress across sessions\n- Update TASKS.md as you complete work") },
|
|
1736
|
-
{ analyzer: "Quality", match: "Backlog", fix: (root) => addClaudeMdSection(root, "## Backlog", "- When a feature is discussed but deferred, add it to BACKLOG.md immediately\n- Never leave future ideas only in TASKS.md or conversation \u2014 they get lost\n- BACKLOG.md is the single source of truth for parked features") },
|
|
1737
|
-
{ analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
|
|
1738
|
-
{ analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
|
|
1739
|
-
{ analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
|
|
1740
|
-
{ analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
|
|
1741
|
-
{ analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
|
|
1742
|
-
{ analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
|
|
1743
|
-
{ analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
|
|
1744
|
-
{ analyzer: "Permissions", match: "Sandbox not enabled", fix: (root) => addSandboxSettings(root) },
|
|
1745
|
-
{ analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
|
|
1746
|
-
{ analyzer: "Rules", match: "No /lp-enhance skill", fix: (root) => createEnhanceSkill(root) },
|
|
1747
|
-
{ analyzer: "Rules", match: "lp-enhance skill is outdated", fix: (root) => updateEnhanceSkill(root) },
|
|
1748
|
-
{ analyzer: "Settings", match: "Deprecated includeCoAuthoredBy", fix: (root) => migrateAttribution(root) },
|
|
1749
|
-
{ analyzer: "Hooks", match: "SessionStart", fix: (root) => addSessionStartHook(root) },
|
|
1750
|
-
{ analyzer: "Memory", match: "Deprecated Stop hook", fix: (root) => removeStaleStopHook(root) },
|
|
1751
|
-
{ analyzer: "Memory", match: "autoMemoryEnabled not disabled", fix: (root, _det, placement) => disableAutoMemory(root, placement) },
|
|
1752
|
-
{ analyzer: "Memory", match: "MCP tool permission", fix: (root, _det, placement) => addMemoryToolPermissions(root, placement) },
|
|
1753
|
-
{ analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
|
|
1754
|
-
const content = "Use agentic-memory to persist knowledge across sessions:\n- Memories are automatically injected at session start\n- STORE IMMEDIATELY when: a dependency strategy changes, an architecture decision is made, a convention is established, a bug pattern is discovered, or a feature is killed/added\n- Use memory_search before memory_store to check for duplicates\n- NEVER store credentials, API keys, tokens, or secrets in memories";
|
|
1755
|
-
const target = placement === "local" ? join5(root, ".claude", "CLAUDE.md") : void 0;
|
|
1756
|
-
return addClaudeMdSection(root, "## Memory", content, target);
|
|
1757
|
-
} }
|
|
1758
|
-
];
|
|
1759
|
-
async function tryFix(issue, root, detected, placement) {
|
|
1760
|
-
const entry = FIX_TABLE.find(
|
|
1761
|
-
(e) => e.analyzer === issue.analyzer && issue.message.includes(e.match)
|
|
1762
|
-
);
|
|
1763
|
-
return entry ? entry.fix(root, detected, placement) : false;
|
|
1764
|
-
}
|
|
1765
|
-
async function addHook(root, event, dedupKeyword, entry, successMsg) {
|
|
1766
|
-
const settings = await readSettingsJson(root);
|
|
1767
|
-
const hooks = settings.hooks ?? {};
|
|
1768
|
-
const hookList = hooks[event] ?? [];
|
|
1769
|
-
const alreadyHas = hookList.some((g) => {
|
|
1770
|
-
const nested = g.hooks;
|
|
1771
|
-
return nested?.some((h) => String(h.command ?? "").includes(dedupKeyword));
|
|
1772
|
-
});
|
|
1773
|
-
if (alreadyHas) return false;
|
|
1774
|
-
hookList.push(entry);
|
|
1775
|
-
settings.hooks = { ...hooks, [event]: hookList };
|
|
1776
|
-
await writeSettingsJson(root, settings);
|
|
1777
|
-
log.success(successMsg);
|
|
1778
|
-
return true;
|
|
1779
|
-
}
|
|
1780
|
-
async function addEnvProtectionHook(root) {
|
|
1781
|
-
return addHook(root, "PreToolUse", ".env", {
|
|
1782
|
-
matcher: "Read|Write|Edit",
|
|
1783
|
-
hooks: [{
|
|
1784
|
-
type: "command",
|
|
1785
|
-
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`
|
|
1786
|
-
}]
|
|
1787
|
-
}, "Added .env file protection hook (PreToolUse)");
|
|
1788
|
-
}
|
|
1789
|
-
async function addAutoFormatHook(root, detected) {
|
|
1790
|
-
if (!detected.language) return false;
|
|
1791
|
-
const formatters = {
|
|
1792
|
-
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
1793
|
-
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
1794
|
-
Python: { extensions: ["py"], command: "ruff format" },
|
|
1795
|
-
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
1796
|
-
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
1797
|
-
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
1798
|
-
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
1799
|
-
};
|
|
1800
|
-
const config = formatters[detected.language];
|
|
1801
|
-
if (!config) return false;
|
|
1802
|
-
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
1803
|
-
return addHook(root, "PostToolUse", "format", {
|
|
1804
|
-
matcher: "Write|Edit",
|
|
1805
|
-
hooks: [{
|
|
1806
|
-
type: "command",
|
|
1807
|
-
command: `ext=\${TOOL_INPUT_FILE_PATH##*.}; (${extChecks}) && ${config.command} "$TOOL_INPUT_FILE_PATH" 2>/dev/null; exit 0`
|
|
1808
|
-
}]
|
|
1809
|
-
}, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
|
|
1810
|
-
}
|
|
1811
|
-
async function addForcePushProtection(root) {
|
|
1812
|
-
return addHook(root, "PreToolUse", "force", {
|
|
1813
|
-
matcher: "Bash",
|
|
1814
|
-
hooks: [{
|
|
1815
|
-
type: "command",
|
|
1816
|
-
command: `echo "$TOOL_INPUT_COMMAND" | grep -qE 'push.*--force|push.*-f' && echo 'WARNING: Force push detected \u2014 this can destroy remote history' && exit 1; exit 0`
|
|
1817
|
-
}]
|
|
1818
|
-
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
1819
|
-
}
|
|
1820
|
-
async function addPostCompactHook(root) {
|
|
1821
|
-
return addHook(root, "PostCompact", "TASKS.md", {
|
|
1822
|
-
matcher: "",
|
|
1823
|
-
hooks: [{
|
|
1824
|
-
type: "command",
|
|
1825
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
1826
|
-
}]
|
|
1827
|
-
}, "Added PostCompact hook (re-injects TASKS.md after compaction)");
|
|
1828
|
-
}
|
|
1829
|
-
async function addSessionStartHook(root) {
|
|
1830
|
-
return addHook(root, "SessionStart", "TASKS.md", {
|
|
1831
|
-
matcher: "startup|resume",
|
|
1832
|
-
hooks: [{
|
|
1833
|
-
type: "command",
|
|
1834
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
1835
|
-
}]
|
|
1836
|
-
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
1837
|
-
}
|
|
1838
|
-
async function migrateAttribution(root) {
|
|
1839
|
-
const settings = await readSettingsJson(root);
|
|
1840
|
-
if (settings.includeCoAuthoredBy === void 0) return false;
|
|
1841
|
-
const { includeCoAuthoredBy: _, ...rest } = settings;
|
|
1842
|
-
const updated = { ...rest, attribution: { commit: "", pr: "" } };
|
|
1843
|
-
await writeSettingsJson(root, updated);
|
|
1844
|
-
log.success("Migrated includeCoAuthoredBy \u2192 attribution object");
|
|
1845
|
-
return true;
|
|
1846
|
-
}
|
|
1847
|
-
async function addCredentialDenyRules(root) {
|
|
1848
|
-
const settings = await readSettingsJson(root);
|
|
1849
|
-
const permissions = settings.permissions ?? {};
|
|
1850
|
-
const deny = permissions.deny ?? [];
|
|
1851
|
-
const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
|
|
1852
|
-
const missing = toAdd.filter((p) => !deny.includes(p));
|
|
1853
|
-
if (missing.length === 0) return false;
|
|
1854
|
-
settings.permissions = { ...permissions, deny: [...deny, ...missing] };
|
|
1855
|
-
await writeSettingsJson(root, settings);
|
|
1856
|
-
log.success("Added credential deny rules (SSH, AWS, npm)");
|
|
1857
|
-
return true;
|
|
1858
|
-
}
|
|
1859
|
-
async function addBypassDisable(root) {
|
|
1860
|
-
const settings = await readSettingsJson(root);
|
|
1861
|
-
if (settings.disableBypassPermissionsMode === "disable") return false;
|
|
1862
|
-
settings.disableBypassPermissionsMode = "disable";
|
|
1863
|
-
await writeSettingsJson(root, settings);
|
|
1864
|
-
log.success("Added disableBypassPermissionsMode: disable");
|
|
1865
|
-
return true;
|
|
1866
|
-
}
|
|
1867
|
-
async function addSandboxSettings(root) {
|
|
1868
|
-
const settings = await readSettingsJson(root);
|
|
1869
|
-
const sandbox = settings.sandbox;
|
|
1870
|
-
if (sandbox?.enabled === true) return false;
|
|
1871
|
-
settings.sandbox = { enabled: true, failIfUnavailable: true };
|
|
1872
|
-
await writeSettingsJson(root, settings);
|
|
1873
|
-
log.success("Enabled sandbox with failIfUnavailable");
|
|
1874
|
-
return true;
|
|
1875
|
-
}
|
|
1876
|
-
async function addEnvToClaudeignore(root) {
|
|
1877
|
-
const ignorePath = join5(root, ".claudeignore");
|
|
1878
|
-
let content;
|
|
1879
|
-
try {
|
|
1880
|
-
content = await readFile4(ignorePath, "utf-8");
|
|
1881
|
-
} catch {
|
|
1882
|
-
return false;
|
|
1883
|
-
}
|
|
1884
|
-
const lines = content.split("\n").map((l) => l.trim());
|
|
1885
|
-
if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
|
|
1886
|
-
await writeFile2(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
|
|
1887
|
-
log.success("Added .env to .claudeignore");
|
|
1888
|
-
return true;
|
|
1889
|
-
}
|
|
1890
|
-
async function addClaudeMdSection(root, heading, content, targetPath) {
|
|
1891
|
-
const claudeMdPath = targetPath ?? join5(root, "CLAUDE.md");
|
|
1892
|
-
let existing;
|
|
1893
|
-
try {
|
|
1894
|
-
existing = await readFile4(claudeMdPath, "utf-8");
|
|
1895
|
-
} catch {
|
|
1896
|
-
if (!targetPath) return false;
|
|
1897
|
-
await mkdir2(join5(root, ".claude"), { recursive: true });
|
|
1898
|
-
existing = "# Local Claude Config\n";
|
|
1899
|
-
}
|
|
1900
|
-
if (existing.includes(heading)) return false;
|
|
1901
|
-
const keyDecisionsIdx = existing.indexOf("## Key Decisions");
|
|
1902
|
-
const insertAt = keyDecisionsIdx > -1 ? keyDecisionsIdx : existing.length;
|
|
1903
|
-
const section = `
|
|
1904
|
-
${heading}
|
|
1905
|
-
${content}
|
|
1906
|
-
|
|
1907
|
-
`;
|
|
1908
|
-
const updated = existing.slice(0, insertAt) + section + existing.slice(insertAt);
|
|
1909
|
-
await writeFile2(claudeMdPath, updated);
|
|
1910
|
-
const label = targetPath ? ".claude/CLAUDE.md" : "CLAUDE.md";
|
|
1911
|
-
log.success(`Added "${heading}" section to ${label}`);
|
|
1912
|
-
return true;
|
|
1913
|
-
}
|
|
1914
|
-
async function createBacklogMd(root) {
|
|
1915
|
-
const backlogPath = join5(root, "BACKLOG.md");
|
|
1916
|
-
try {
|
|
1917
|
-
await access4(backlogPath);
|
|
1918
|
-
return false;
|
|
1919
|
-
} catch {
|
|
1920
|
-
}
|
|
1921
|
-
const name = root.split("/").pop() ?? "Project";
|
|
1922
|
-
await writeFile2(backlogPath, `# ${name} - Backlog
|
|
1923
|
-
|
|
1924
|
-
> Features discussed but deferred. Pick up when relevant.
|
|
1925
|
-
> Priority: P0 = next sprint, P1 = soon, P2 = when relevant.
|
|
1926
|
-
`);
|
|
1927
|
-
log.success("Generated BACKLOG.md");
|
|
1928
|
-
return true;
|
|
1929
|
-
}
|
|
1930
|
-
async function createClaudeignore(root, detected) {
|
|
1931
|
-
const ignorePath = join5(root, ".claudeignore");
|
|
1932
|
-
try {
|
|
1933
|
-
await access4(ignorePath);
|
|
1934
|
-
return false;
|
|
1935
|
-
} catch {
|
|
1936
|
-
}
|
|
1937
|
-
const content = generateClaudeignore(detected);
|
|
1938
|
-
await writeFile2(ignorePath, content);
|
|
1939
|
-
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
1940
|
-
return true;
|
|
1941
|
-
}
|
|
1942
|
-
async function createStarterRules(root) {
|
|
1943
|
-
const rulesDir = join5(root, ".claude", "rules");
|
|
1944
|
-
try {
|
|
1945
|
-
await access4(rulesDir);
|
|
1946
|
-
return false;
|
|
1947
|
-
} catch {
|
|
1948
|
-
}
|
|
1949
|
-
await mkdir2(rulesDir, { recursive: true });
|
|
1950
|
-
await writeFile2(
|
|
1951
|
-
join5(rulesDir, "conventions.md"),
|
|
1952
|
-
`# Project Conventions
|
|
1953
|
-
|
|
1954
|
-
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
1955
|
-
- Keep files under 400 lines, functions under 50 lines
|
|
1956
|
-
- Handle errors explicitly \u2014 no empty catch blocks
|
|
1957
|
-
- Validate input at system boundaries
|
|
1958
|
-
`
|
|
1959
|
-
);
|
|
1960
|
-
log.success("Created .claude/rules/conventions.md with starter rules");
|
|
1961
|
-
return true;
|
|
1962
|
-
}
|
|
1963
|
-
async function disableAutoMemory(root, placement) {
|
|
1964
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1965
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1966
|
-
const settings = await read(root);
|
|
1967
|
-
if (settings.autoMemoryEnabled === false) return false;
|
|
1968
|
-
settings.autoMemoryEnabled = false;
|
|
1969
|
-
await write(root, settings);
|
|
1970
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1971
|
-
log.success(`Set autoMemoryEnabled: false in ${target}`);
|
|
1972
|
-
return true;
|
|
1973
|
-
}
|
|
1974
|
-
async function addMemoryToolPermissions(root, placement) {
|
|
1975
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1976
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1977
|
-
const settings = await read(root);
|
|
1978
|
-
const permissions = settings.permissions ?? {};
|
|
1979
|
-
const allow = permissions.allow ?? [];
|
|
1980
|
-
const tools = [
|
|
1981
|
-
"mcp__agentic-memory__memory_store",
|
|
1982
|
-
"mcp__agentic-memory__memory_search",
|
|
1983
|
-
"mcp__agentic-memory__memory_recent",
|
|
1984
|
-
"mcp__agentic-memory__memory_forget",
|
|
1985
|
-
"mcp__agentic-memory__memory_relate",
|
|
1986
|
-
"mcp__agentic-memory__memory_stats",
|
|
1987
|
-
"mcp__agentic-memory__memory_update"
|
|
1988
|
-
];
|
|
1989
|
-
const missing = tools.filter((t) => !allow.includes(t));
|
|
1990
|
-
if (missing.length === 0) return false;
|
|
1991
|
-
settings.permissions = { ...permissions, allow: [...allow, ...missing] };
|
|
1992
|
-
await write(root, settings);
|
|
1993
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1994
|
-
log.success(`Added agentic-memory MCP tool permissions to ${target}`);
|
|
1995
|
-
return true;
|
|
1996
|
-
}
|
|
1997
|
-
async function createEnhanceSkill(root) {
|
|
1998
|
-
const skillDir = join5(root, ".claude", "skills", "lp-enhance");
|
|
1999
|
-
const skillPath = join5(skillDir, "SKILL.md");
|
|
2000
|
-
const globalPath = join5(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2001
|
-
const legacyProject = join5(root, ".claude", "commands", "lp-enhance.md");
|
|
2002
|
-
const legacyGlobal = join5(homedir3(), ".claude", "commands", "lp-enhance.md");
|
|
2003
|
-
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
|
|
2004
|
-
await mkdir2(skillDir, { recursive: true });
|
|
2005
|
-
await writeFile2(skillPath, generateEnhanceSkill());
|
|
2006
|
-
log.success("Generated /lp-enhance skill (.claude/skills/lp-enhance/)");
|
|
2007
|
-
return true;
|
|
2008
|
-
}
|
|
2009
|
-
async function updateEnhanceSkill(root) {
|
|
2010
|
-
const projectPath = join5(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2011
|
-
const globalPath = join5(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2012
|
-
const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
|
|
2013
|
-
if (!targetPath) return false;
|
|
2014
|
-
await writeFile2(targetPath, generateEnhanceSkill());
|
|
2015
|
-
log.success("Updated /lp-enhance skill to latest version");
|
|
2016
|
-
return true;
|
|
2017
|
-
}
|
|
2018
|
-
async function removeStaleStopHook(root) {
|
|
2019
|
-
const settings = await readSettingsJson(root);
|
|
2020
|
-
const hooks = settings.hooks;
|
|
2021
|
-
if (!hooks?.Stop) return false;
|
|
2022
|
-
const stopHooks = hooks.Stop;
|
|
2023
|
-
const filtered = stopHooks.filter((h) => {
|
|
2024
|
-
const innerHooks = h.hooks;
|
|
2025
|
-
return !innerHooks?.some(
|
|
2026
|
-
(ih) => typeof ih.command === "string" && ih.command.includes("memory extract")
|
|
2027
|
-
);
|
|
2028
|
-
});
|
|
2029
|
-
if (filtered.length === stopHooks.length) return false;
|
|
2030
|
-
if (filtered.length === 0) {
|
|
2031
|
-
delete hooks.Stop;
|
|
2032
|
-
} else {
|
|
2033
|
-
hooks.Stop = filtered;
|
|
2034
|
-
}
|
|
2035
|
-
await writeSettingsJson(root, settings);
|
|
2036
|
-
log.success("Removed deprecated Stop hook (memory extract)");
|
|
2037
|
-
return true;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
1273
|
// src/commands/doctor/watcher.ts
|
|
2041
1274
|
import { readdir as readdir2, stat } from "fs/promises";
|
|
2042
|
-
import { join as
|
|
1275
|
+
import { join as join4 } from "path";
|
|
2043
1276
|
async function watchConfig(projectRoot) {
|
|
2044
1277
|
await runAndDisplay(projectRoot);
|
|
2045
1278
|
log.blank();
|
|
@@ -2062,16 +1295,16 @@ async function watchConfig(projectRoot) {
|
|
|
2062
1295
|
}
|
|
2063
1296
|
async function getFileSnapshot(projectRoot) {
|
|
2064
1297
|
const files = [
|
|
2065
|
-
|
|
2066
|
-
|
|
1298
|
+
join4(projectRoot, "CLAUDE.md"),
|
|
1299
|
+
join4(projectRoot, ".claudeignore")
|
|
2067
1300
|
];
|
|
2068
|
-
const claudeDir =
|
|
1301
|
+
const claudeDir = join4(projectRoot, ".claude");
|
|
2069
1302
|
try {
|
|
2070
1303
|
const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
|
|
2071
1304
|
for (const entry of entries) {
|
|
2072
1305
|
if (entry.isFile()) {
|
|
2073
1306
|
const parentPath = entry.parentPath ?? claudeDir;
|
|
2074
|
-
files.push(
|
|
1307
|
+
files.push(join4(parentPath, entry.name));
|
|
2075
1308
|
}
|
|
2076
1309
|
}
|
|
2077
1310
|
} catch {
|
|
@@ -2215,12 +1448,12 @@ import { Command as Command3 } from "commander";
|
|
|
2215
1448
|
import { select as select2 } from "@inquirer/prompts";
|
|
2216
1449
|
import ora from "ora";
|
|
2217
1450
|
import chalk2 from "chalk";
|
|
2218
|
-
import { mkdir as
|
|
2219
|
-
import { join as
|
|
1451
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1452
|
+
import { join as join7 } from "path";
|
|
2220
1453
|
|
|
2221
1454
|
// src/commands/eval/loader.ts
|
|
2222
|
-
import { readFile as
|
|
2223
|
-
import { join as
|
|
1455
|
+
import { readFile as readFile3, readdir as readdir3, access as access3 } from "fs/promises";
|
|
1456
|
+
import { join as join5, resolve as resolve2, dirname as dirname2 } from "path";
|
|
2224
1457
|
import { fileURLToPath } from "url";
|
|
2225
1458
|
import { parse as parseYaml } from "yaml";
|
|
2226
1459
|
|
|
@@ -2334,7 +1567,7 @@ async function findScenariosDir() {
|
|
|
2334
1567
|
}
|
|
2335
1568
|
async function dirExists(path) {
|
|
2336
1569
|
try {
|
|
2337
|
-
await
|
|
1570
|
+
await access3(path);
|
|
2338
1571
|
return true;
|
|
2339
1572
|
} catch {
|
|
2340
1573
|
return false;
|
|
@@ -2343,14 +1576,14 @@ async function dirExists(path) {
|
|
|
2343
1576
|
async function loadScenarios(options) {
|
|
2344
1577
|
const { suite, customPath } = options;
|
|
2345
1578
|
const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
|
|
2346
|
-
const dirs = suite ? [
|
|
1579
|
+
const dirs = suite ? [join5(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
|
|
2347
1580
|
const allDirs = [scenarioDir, ...dirs];
|
|
2348
1581
|
const scenarios = [];
|
|
2349
1582
|
for (const dir of allDirs) {
|
|
2350
1583
|
const files = await listYamlFiles(dir);
|
|
2351
1584
|
for (const file of files) {
|
|
2352
1585
|
try {
|
|
2353
|
-
const content = await
|
|
1586
|
+
const content = await readFile3(file, "utf-8");
|
|
2354
1587
|
const raw = parseYaml(content);
|
|
2355
1588
|
const scenario = validateScenario(raw, file);
|
|
2356
1589
|
scenarios.push(scenario);
|
|
@@ -2365,7 +1598,7 @@ async function loadScenarios(options) {
|
|
|
2365
1598
|
async function getSubdirectories(dir) {
|
|
2366
1599
|
try {
|
|
2367
1600
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2368
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1601
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join5(dir, e.name));
|
|
2369
1602
|
} catch {
|
|
2370
1603
|
return [];
|
|
2371
1604
|
}
|
|
@@ -2373,22 +1606,22 @@ async function getSubdirectories(dir) {
|
|
|
2373
1606
|
async function listYamlFiles(dir) {
|
|
2374
1607
|
try {
|
|
2375
1608
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2376
|
-
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) =>
|
|
1609
|
+
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join5(dir, e.name));
|
|
2377
1610
|
} catch {
|
|
2378
1611
|
return [];
|
|
2379
1612
|
}
|
|
2380
1613
|
}
|
|
2381
1614
|
|
|
2382
1615
|
// src/commands/eval/runner.ts
|
|
2383
|
-
import { mkdir as
|
|
2384
|
-
import { join as
|
|
1616
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, readdir as readdir4, rm, cp } from "fs/promises";
|
|
1617
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
2385
1618
|
import { tmpdir } from "os";
|
|
2386
1619
|
import { randomUUID } from "crypto";
|
|
2387
1620
|
import { execFile } from "child_process";
|
|
2388
1621
|
import { promisify } from "util";
|
|
2389
1622
|
var exec = promisify(execFile);
|
|
2390
1623
|
async function runScenario(scenario, options) {
|
|
2391
|
-
const sandboxDir =
|
|
1624
|
+
const sandboxDir = join6(tmpdir(), `claude-eval-${randomUUID()}`);
|
|
2392
1625
|
try {
|
|
2393
1626
|
await setupSandbox(sandboxDir, scenario, options.projectRoot);
|
|
2394
1627
|
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
|
|
@@ -2412,16 +1645,16 @@ async function runScenarioWithRetries(scenario, options) {
|
|
|
2412
1645
|
return sorted[Math.floor(sorted.length / 2)];
|
|
2413
1646
|
}
|
|
2414
1647
|
async function setupSandbox(sandboxDir, scenario, projectRoot) {
|
|
2415
|
-
await
|
|
1648
|
+
await mkdir2(sandboxDir, { recursive: true });
|
|
2416
1649
|
for (const file of scenario.setup.files) {
|
|
2417
|
-
const filePath =
|
|
2418
|
-
await
|
|
2419
|
-
await
|
|
1650
|
+
const filePath = join6(sandboxDir, file.path);
|
|
1651
|
+
await mkdir2(dirname3(filePath), { recursive: true });
|
|
1652
|
+
await writeFile2(filePath, file.content);
|
|
2420
1653
|
}
|
|
2421
1654
|
await copyProjectConfig(sandboxDir, projectRoot);
|
|
2422
1655
|
if (scenario.setup.instructions) {
|
|
2423
|
-
await
|
|
2424
|
-
|
|
1656
|
+
await writeFile2(
|
|
1657
|
+
join6(sandboxDir, "CLAUDE.md"),
|
|
2425
1658
|
`# Eval Scenario
|
|
2426
1659
|
|
|
2427
1660
|
${scenario.setup.instructions}
|
|
@@ -2442,20 +1675,20 @@ ${scenario.setup.instructions}
|
|
|
2442
1675
|
], { cwd: sandboxDir });
|
|
2443
1676
|
}
|
|
2444
1677
|
async function copyProjectConfig(sandboxDir, projectRoot) {
|
|
2445
|
-
const claudeDir =
|
|
2446
|
-
const sandboxClaudeDir =
|
|
2447
|
-
const settingsPath =
|
|
1678
|
+
const claudeDir = join6(projectRoot, ".claude");
|
|
1679
|
+
const sandboxClaudeDir = join6(sandboxDir, ".claude");
|
|
1680
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
2448
1681
|
if (await fileExists(settingsPath)) {
|
|
2449
|
-
await
|
|
2450
|
-
await cp(settingsPath,
|
|
1682
|
+
await mkdir2(sandboxClaudeDir, { recursive: true });
|
|
1683
|
+
await cp(settingsPath, join6(sandboxClaudeDir, "settings.json"));
|
|
2451
1684
|
}
|
|
2452
|
-
const rulesDir =
|
|
1685
|
+
const rulesDir = join6(claudeDir, "rules");
|
|
2453
1686
|
if (await fileExists(rulesDir)) {
|
|
2454
|
-
await cp(rulesDir,
|
|
1687
|
+
await cp(rulesDir, join6(sandboxClaudeDir, "rules"), { recursive: true });
|
|
2455
1688
|
}
|
|
2456
|
-
const ignorePath =
|
|
1689
|
+
const ignorePath = join6(projectRoot, ".claudeignore");
|
|
2457
1690
|
if (await fileExists(ignorePath)) {
|
|
2458
|
-
await cp(ignorePath,
|
|
1691
|
+
await cp(ignorePath, join6(sandboxDir, ".claudeignore"));
|
|
2459
1692
|
}
|
|
2460
1693
|
}
|
|
2461
1694
|
async function runClaudeInSandbox(cwd, prompt, timeout, model) {
|
|
@@ -2549,7 +1782,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
|
|
|
2549
1782
|
async function checkGrep(check, sandboxDir) {
|
|
2550
1783
|
if (!check.pattern) return false;
|
|
2551
1784
|
try {
|
|
2552
|
-
const content = await
|
|
1785
|
+
const content = await readFile4(join6(sandboxDir, check.target), "utf-8");
|
|
2553
1786
|
let found;
|
|
2554
1787
|
try {
|
|
2555
1788
|
found = new RegExp(check.pattern).test(content);
|
|
@@ -2563,7 +1796,7 @@ async function checkGrep(check, sandboxDir) {
|
|
|
2563
1796
|
}
|
|
2564
1797
|
async function checkFilePresence(check, sandboxDir) {
|
|
2565
1798
|
try {
|
|
2566
|
-
await
|
|
1799
|
+
await readFile4(join6(sandboxDir, check.target));
|
|
2567
1800
|
return check.expect === "present";
|
|
2568
1801
|
} catch {
|
|
2569
1802
|
return check.expect === "absent";
|
|
@@ -2572,9 +1805,9 @@ async function checkFilePresence(check, sandboxDir) {
|
|
|
2572
1805
|
async function checkMaxLines(check, sandboxDir) {
|
|
2573
1806
|
const maxLines = parseInt(check.pattern ?? "800", 10);
|
|
2574
1807
|
try {
|
|
2575
|
-
const files = await listAllFiles(
|
|
1808
|
+
const files = await listAllFiles(join6(sandboxDir, check.target));
|
|
2576
1809
|
for (const file of files) {
|
|
2577
|
-
const content = await
|
|
1810
|
+
const content = await readFile4(file, "utf-8");
|
|
2578
1811
|
if (content.split("\n").length > maxLines) {
|
|
2579
1812
|
return check.expect === "absent";
|
|
2580
1813
|
}
|
|
@@ -2589,7 +1822,7 @@ async function listAllFiles(dir) {
|
|
|
2589
1822
|
try {
|
|
2590
1823
|
const entries = await readdir4(dir, { withFileTypes: true });
|
|
2591
1824
|
for (const entry of entries) {
|
|
2592
|
-
const fullPath =
|
|
1825
|
+
const fullPath = join6(dir, entry.name);
|
|
2593
1826
|
if (entry.isDirectory()) {
|
|
2594
1827
|
results.push(...await listAllFiles(fullPath));
|
|
2595
1828
|
} else {
|
|
@@ -2775,10 +2008,10 @@ async function saveEvalReport(results, projectRoot, suite, model) {
|
|
|
2775
2008
|
lines.push("");
|
|
2776
2009
|
}
|
|
2777
2010
|
}
|
|
2778
|
-
const evalDir =
|
|
2779
|
-
await
|
|
2011
|
+
const evalDir = join7(projectRoot, ".claude", "eval");
|
|
2012
|
+
await mkdir3(evalDir, { recursive: true });
|
|
2780
2013
|
const filename = `eval-${suite ?? "all"}-${timestamp}.md`;
|
|
2781
|
-
await
|
|
2014
|
+
await writeFile3(join7(evalDir, filename), lines.join("\n"));
|
|
2782
2015
|
log.success(`Report saved to .claude/eval/${filename}`);
|
|
2783
2016
|
}
|
|
2784
2017
|
async function checkClaudeCli() {
|
|
@@ -2795,12 +2028,12 @@ async function checkClaudeCli() {
|
|
|
2795
2028
|
|
|
2796
2029
|
// src/commands/memory/index.ts
|
|
2797
2030
|
import { readFileSync } from "fs";
|
|
2798
|
-
import { join as
|
|
2031
|
+
import { join as join8 } from "path";
|
|
2799
2032
|
import { Command as Command4 } from "commander";
|
|
2800
2033
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2801
2034
|
function isMemoryInstalled() {
|
|
2802
2035
|
const cwd = process.cwd();
|
|
2803
|
-
return hasMemoryHook(
|
|
2036
|
+
return hasMemoryHook(join8(cwd, ".claude", "settings.json")) || hasMemoryHook(join8(cwd, ".claude", "settings.local.json"));
|
|
2804
2037
|
}
|
|
2805
2038
|
function hasMemoryHook(path) {
|
|
2806
2039
|
try {
|
|
@@ -2823,14 +2056,14 @@ function createMemoryCommand() {
|
|
|
2823
2056
|
log.error("Knowledge base not set up yet. Run `claude-launchpad memory` first.");
|
|
2824
2057
|
return;
|
|
2825
2058
|
}
|
|
2826
|
-
const { requireMemoryDeps } = await import("./require-deps-
|
|
2059
|
+
const { requireMemoryDeps } = await import("./require-deps-SUGLVBM2.js");
|
|
2827
2060
|
await requireMemoryDeps();
|
|
2828
|
-
const { startTui } = await import("./tui-
|
|
2061
|
+
const { startTui } = await import("./tui-DKWRY5PT.js");
|
|
2829
2062
|
await startTui();
|
|
2830
2063
|
return;
|
|
2831
2064
|
}
|
|
2832
2065
|
if (!isMemoryInstalled()) {
|
|
2833
|
-
const { detectExistingSetup } = await import("./install-
|
|
2066
|
+
const { detectExistingSetup } = await import("./install-PFTFTNIF.js");
|
|
2834
2067
|
const existing = detectExistingSetup(process.cwd());
|
|
2835
2068
|
if (existing) {
|
|
2836
2069
|
const location = existing === "local" ? ".claude/CLAUDE.md + settings.local.json" : "CLAUDE.md + settings.json";
|
|
@@ -2856,18 +2089,18 @@ function createMemoryCommand() {
|
|
|
2856
2089
|
log.info("Skipped.");
|
|
2857
2090
|
return;
|
|
2858
2091
|
}
|
|
2859
|
-
const { runInstall } = await import("./install-
|
|
2092
|
+
const { runInstall } = await import("./install-PFTFTNIF.js");
|
|
2860
2093
|
await runInstall({});
|
|
2861
2094
|
} else {
|
|
2862
|
-
const { requireMemoryDeps } = await import("./require-deps-
|
|
2095
|
+
const { requireMemoryDeps } = await import("./require-deps-SUGLVBM2.js");
|
|
2863
2096
|
await requireMemoryDeps();
|
|
2864
|
-
const { runStats } = await import("./stats-
|
|
2097
|
+
const { runStats } = await import("./stats-7WFCVXBX.js");
|
|
2865
2098
|
await runStats({});
|
|
2866
2099
|
}
|
|
2867
2100
|
});
|
|
2868
2101
|
memory.addCommand(
|
|
2869
2102
|
new Command4("context").description("Load session context (hook handler)").option("--json", "JSON output").action(async (opts) => {
|
|
2870
|
-
const { runContext } = await import("./context-
|
|
2103
|
+
const { runContext } = await import("./context-53PKQOMI.js");
|
|
2871
2104
|
await runContext(opts);
|
|
2872
2105
|
}).helpCommand(false),
|
|
2873
2106
|
{ hidden: true }
|
|
@@ -2881,13 +2114,13 @@ function createMemoryCommand() {
|
|
|
2881
2114
|
);
|
|
2882
2115
|
memory.addCommand(
|
|
2883
2116
|
new Command4("push").description("Push current project's memories to GitHub Gist").option("--all", "Push all projects").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
2884
|
-
const { runPush } = await import("./push-
|
|
2117
|
+
const { runPush } = await import("./push-JOCEW3VG.js");
|
|
2885
2118
|
await runPush(opts);
|
|
2886
2119
|
})
|
|
2887
2120
|
);
|
|
2888
2121
|
memory.addCommand(
|
|
2889
2122
|
new Command4("pull").description("Pull current project's memories from GitHub Gist").option("--all", "Pull all projects").action(async (opts) => {
|
|
2890
|
-
const { runPull } = await import("./pull-
|
|
2123
|
+
const { runPull } = await import("./pull-4QTS57DQ.js");
|
|
2891
2124
|
await runPull(opts);
|
|
2892
2125
|
})
|
|
2893
2126
|
);
|
|
@@ -2895,8 +2128,8 @@ function createMemoryCommand() {
|
|
|
2895
2128
|
}
|
|
2896
2129
|
|
|
2897
2130
|
// src/cli.ts
|
|
2898
|
-
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.
|
|
2899
|
-
const hasConfig = await fileExists(
|
|
2131
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.16.1", "-v, --version").action(async () => {
|
|
2132
|
+
const hasConfig = await fileExists(join9(process.cwd(), "CLAUDE.md")) || await fileExists(join9(process.cwd(), ".claude", "settings.json"));
|
|
2900
2133
|
if (hasConfig) {
|
|
2901
2134
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
2902
2135
|
} else {
|