claude-launchpad 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/dist/{chunk-JQDMBE7W.js → chunk-3UJYOWGF.js} +18 -1
- package/dist/chunk-3UJYOWGF.js.map +1 -0
- package/dist/chunk-4JNFXVVC.js +1139 -0
- package/dist/chunk-4JNFXVVC.js.map +1 -0
- package/dist/{chunk-5MWCQLNL.js → chunk-AUV2JTXX.js} +3 -3
- package/dist/{chunk-Z6FBT44W.js → chunk-PGDSAUP4.js} +2 -2
- package/dist/{chunk-24VLPHJU.js → chunk-V4NXT4KB.js} +43 -16
- package/dist/chunk-V4NXT4KB.js.map +1 -0
- package/dist/{chunk-EDKY7JWY.js → chunk-ZXJK7CHB.js} +31 -4
- package/dist/chunk-ZXJK7CHB.js.map +1 -0
- package/dist/cli.js +156 -1065
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +9 -4
- package/dist/commands/memory/server.js.map +1 -1
- package/dist/{context-CWJUUTTU.js → context-Q5ZQBY7O.js} +6 -6
- package/dist/{install-MVATZUXZ.js → install-LS7DTMIE.js} +65 -63
- package/dist/install-LS7DTMIE.js.map +1 -0
- package/dist/{pull-YOESZ3UC.js → pull-4NRD4GQ4.js} +13 -9
- package/dist/pull-4NRD4GQ4.js.map +1 -0
- package/dist/{push-74XC5CUK.js → push-BHYEETGP.js} +9 -9
- package/dist/{require-deps-NKRCPVAO.js → require-deps-3GIE6TAG.js} +3 -3
- package/dist/{stats-QUBHHPV7.js → stats-NQ5NRUZC.js} +7 -7
- package/dist/sync-clean-QWEQVAYO.js +53 -0
- package/dist/sync-clean-QWEQVAYO.js.map +1 -0
- package/dist/sync-status-ZMXMEBGC.js +70 -0
- package/dist/sync-status-ZMXMEBGC.js.map +1 -0
- package/dist/{tui-XIYOOUP6.js → tui-YL5NWME5.js} +5 -5
- package/package.json +1 -1
- package/dist/chunk-24VLPHJU.js.map +0 -1
- package/dist/chunk-EDKY7JWY.js.map +0 -1
- package/dist/chunk-JQDMBE7W.js.map +0 -1
- package/dist/chunk-RJGXPH7P.js +0 -107
- package/dist/chunk-RJGXPH7P.js.map +0 -1
- package/dist/chunk-SBA5KYQU.js +0 -76
- package/dist/chunk-SBA5KYQU.js.map +0 -1
- package/dist/install-MVATZUXZ.js.map +0 -1
- package/dist/pull-YOESZ3UC.js.map +0 -1
- /package/dist/{chunk-5MWCQLNL.js.map → chunk-AUV2JTXX.js.map} +0 -0
- /package/dist/{chunk-Z6FBT44W.js.map → chunk-PGDSAUP4.js.map} +0 -0
- /package/dist/{context-CWJUUTTU.js.map → context-Q5ZQBY7O.js.map} +0 -0
- /package/dist/{push-74XC5CUK.js.map → push-BHYEETGP.js.map} +0 -0
- /package/dist/{require-deps-NKRCPVAO.js.map → require-deps-3GIE6TAG.js.map} +0 -0
- /package/dist/{stats-QUBHHPV7.js.map → stats-NQ5NRUZC.js.map} +0 -0
- /package/dist/{tui-XIYOOUP6.js.map → tui-YL5NWME5.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,234 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getMemoryPlacement,
|
|
4
|
-
readSettingsJson,
|
|
5
|
-
readSettingsLocalJson,
|
|
6
|
-
writeSettingsJson,
|
|
7
|
-
writeSettingsLocalJson
|
|
8
|
-
} from "./chunk-SBA5KYQU.js";
|
|
9
2
|
import {
|
|
10
3
|
readSyncConfig
|
|
11
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3UJYOWGF.js";
|
|
12
5
|
import {
|
|
6
|
+
ENHANCE_SKILL_VERSION,
|
|
7
|
+
applyFixes,
|
|
8
|
+
detectProject,
|
|
9
|
+
fileExists,
|
|
10
|
+
generateClaudeignore,
|
|
11
|
+
generateEnhanceSkill,
|
|
13
12
|
log,
|
|
14
13
|
printBanner,
|
|
15
14
|
printScoreCard,
|
|
15
|
+
readFileOrNull,
|
|
16
16
|
renderDoctorReport
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-4JNFXVVC.js";
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
20
20
|
import { Command as Command5 } from "commander";
|
|
21
|
-
import { join as
|
|
21
|
+
import { join as join9 } from "path";
|
|
22
22
|
|
|
23
23
|
// src/commands/init/index.ts
|
|
24
24
|
import { Command } from "commander";
|
|
25
25
|
import { input, confirm, select } from "@inquirer/prompts";
|
|
26
|
-
import { writeFile, mkdir, readFile
|
|
26
|
+
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
27
27
|
import { homedir } from "os";
|
|
28
|
-
import { join
|
|
29
|
-
|
|
30
|
-
// src/lib/fs-utils.ts
|
|
31
|
-
import { readFile, access } from "fs/promises";
|
|
32
|
-
async function fileExists(path) {
|
|
33
|
-
try {
|
|
34
|
-
await access(path);
|
|
35
|
-
return true;
|
|
36
|
-
} catch {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
async function readFileOrNull(path) {
|
|
41
|
-
try {
|
|
42
|
-
return await readFile(path, "utf-8");
|
|
43
|
-
} catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function readJsonOrNull(path) {
|
|
48
|
-
try {
|
|
49
|
-
const content = await readFile(path, "utf-8");
|
|
50
|
-
return JSON.parse(content);
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/lib/detect.ts
|
|
57
|
-
import { join, basename } from "path";
|
|
58
|
-
async function detectProject(root) {
|
|
59
|
-
const name = basename(root);
|
|
60
|
-
const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradleGroovy, buildGradleKts, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
|
|
61
|
-
readJsonOrNull(join(root, "package.json")),
|
|
62
|
-
fileExists(join(root, "go.mod")),
|
|
63
|
-
readFileOrNull(join(root, "pyproject.toml")),
|
|
64
|
-
fileExists(join(root, "Gemfile")),
|
|
65
|
-
fileExists(join(root, "Cargo.toml")),
|
|
66
|
-
fileExists(join(root, "pubspec.yaml")),
|
|
67
|
-
readJsonOrNull(join(root, "composer.json")),
|
|
68
|
-
fileExists(join(root, "pom.xml")),
|
|
69
|
-
fileExists(join(root, "build.gradle")),
|
|
70
|
-
fileExists(join(root, "build.gradle.kts")),
|
|
71
|
-
fileExists(join(root, "Package.swift")),
|
|
72
|
-
fileExists(join(root, "mix.exs")),
|
|
73
|
-
globExists(root, "*.csproj"),
|
|
74
|
-
detectLockfiles(root)
|
|
75
|
-
]);
|
|
76
|
-
const buildGradle = buildGradleGroovy || buildGradleKts;
|
|
77
|
-
const manifests = {
|
|
78
|
-
pkgJson,
|
|
79
|
-
goMod,
|
|
80
|
-
pyProject,
|
|
81
|
-
gemfile,
|
|
82
|
-
cargo,
|
|
83
|
-
pubspec,
|
|
84
|
-
composerJson,
|
|
85
|
-
pomXml,
|
|
86
|
-
buildGradle,
|
|
87
|
-
packageSwift,
|
|
88
|
-
mixExs,
|
|
89
|
-
csproj
|
|
90
|
-
};
|
|
91
|
-
const language = detectLanguage(manifests);
|
|
92
|
-
const framework = detectFramework(manifests);
|
|
93
|
-
const packageManager = detectPackageManager(manifests, lockfiles);
|
|
94
|
-
const scripts = detectScripts({ pkgJson, pyProject, goMod, gemfile, composerJson, language });
|
|
95
|
-
return {
|
|
96
|
-
name,
|
|
97
|
-
language,
|
|
98
|
-
framework,
|
|
99
|
-
packageManager,
|
|
100
|
-
hasTests: scripts.testCommand !== null,
|
|
101
|
-
hasLinter: scripts.lintCommand !== null,
|
|
102
|
-
hasFormatter: scripts.formatCommand !== null,
|
|
103
|
-
...scripts
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function detectLanguage(m) {
|
|
107
|
-
if (m.pkgJson?.devDependencies?.typescript || m.pkgJson?.dependencies?.typescript) return "TypeScript";
|
|
108
|
-
if (m.pkgJson) return "JavaScript";
|
|
109
|
-
if (m.goMod) return "Go";
|
|
110
|
-
if (m.pyProject) return "Python";
|
|
111
|
-
if (m.gemfile) return "Ruby";
|
|
112
|
-
if (m.cargo) return "Rust";
|
|
113
|
-
if (m.pubspec) return "Dart";
|
|
114
|
-
if (m.composerJson) return "PHP";
|
|
115
|
-
if (m.buildGradle) return "Kotlin";
|
|
116
|
-
if (m.pomXml) return "Java";
|
|
117
|
-
if (m.packageSwift) return "Swift";
|
|
118
|
-
if (m.mixExs) return "Elixir";
|
|
119
|
-
if (m.csproj) return "C#";
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
function detectFramework(m) {
|
|
123
|
-
const deps = { ...m.pkgJson?.dependencies, ...m.pkgJson?.devDependencies };
|
|
124
|
-
if (deps.next) return "Next.js";
|
|
125
|
-
if (deps.nuxt) return "Nuxt";
|
|
126
|
-
if (deps.svelte || deps["@sveltejs/kit"]) return "SvelteKit";
|
|
127
|
-
if (deps.astro) return "Astro";
|
|
128
|
-
if (deps["@angular/core"]) return "Angular";
|
|
129
|
-
if (deps.remix || deps["@remix-run/react"]) return "Remix";
|
|
130
|
-
if (deps.vue) return "Vue";
|
|
131
|
-
if (deps.react && !deps.next) return "React";
|
|
132
|
-
if (deps.express) return "Express";
|
|
133
|
-
if (deps.fastify) return "Fastify";
|
|
134
|
-
if (deps.hono) return "Hono";
|
|
135
|
-
if (deps.nestjs || deps["@nestjs/core"]) return "NestJS";
|
|
136
|
-
if (m.pyProject) {
|
|
137
|
-
if (m.pyProject.includes("fastapi")) return "FastAPI";
|
|
138
|
-
if (m.pyProject.includes("django")) return "Django";
|
|
139
|
-
if (m.pyProject.includes("flask")) return "Flask";
|
|
140
|
-
}
|
|
141
|
-
if (m.composerJson) {
|
|
142
|
-
const phpDeps = { ...m.composerJson.require, ...m.composerJson["require-dev"] };
|
|
143
|
-
if (phpDeps["laravel/framework"]) return "Laravel";
|
|
144
|
-
if (phpDeps["symfony/framework-bundle"]) return "Symfony";
|
|
145
|
-
}
|
|
146
|
-
if (m.gemfile) return "Rails";
|
|
147
|
-
if (m.buildGradle) return "Gradle";
|
|
148
|
-
if (m.pomXml) return "Maven";
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
async function detectLockfiles(root) {
|
|
152
|
-
const [pnpmLock, yarnLock, bunLock, npmLock] = await Promise.all([
|
|
153
|
-
fileExists(join(root, "pnpm-lock.yaml")),
|
|
154
|
-
fileExists(join(root, "yarn.lock")),
|
|
155
|
-
fileExists(join(root, "bun.lockb")),
|
|
156
|
-
fileExists(join(root, "package-lock.json"))
|
|
157
|
-
]);
|
|
158
|
-
return { pnpmLock, yarnLock, bunLock, npmLock };
|
|
159
|
-
}
|
|
160
|
-
function detectPackageManager(m, lockfiles) {
|
|
161
|
-
if (m.pkgJson) {
|
|
162
|
-
const pm = m.pkgJson.packageManager;
|
|
163
|
-
if (pm?.startsWith("pnpm")) return "pnpm";
|
|
164
|
-
if (pm?.startsWith("yarn")) return "yarn";
|
|
165
|
-
if (pm?.startsWith("bun")) return "bun";
|
|
166
|
-
if (pm?.startsWith("npm")) return "npm";
|
|
167
|
-
if (lockfiles.pnpmLock) return "pnpm";
|
|
168
|
-
if (lockfiles.yarnLock) return "yarn";
|
|
169
|
-
if (lockfiles.bunLock) return "bun";
|
|
170
|
-
if (lockfiles.npmLock) return "npm";
|
|
171
|
-
return "npm";
|
|
172
|
-
}
|
|
173
|
-
if (m.goMod) return "go modules";
|
|
174
|
-
if (m.pyProject) {
|
|
175
|
-
if (m.pyProject.includes("[tool.uv]")) return "uv";
|
|
176
|
-
if (m.pyProject.includes("[tool.poetry]")) return "poetry";
|
|
177
|
-
return "pip";
|
|
178
|
-
}
|
|
179
|
-
if (m.gemfile) return "bundler";
|
|
180
|
-
if (m.cargo) return "cargo";
|
|
181
|
-
if (m.composerJson) return "composer";
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
var LANGUAGE_SCRIPTS = {
|
|
185
|
-
Go: { devCommand: "go run .", buildCommand: "go build .", testCommand: "go test ./...", lintCommand: "golangci-lint run", formatCommand: "gofmt -w ." },
|
|
186
|
-
Ruby: { devCommand: "bin/dev", buildCommand: null, testCommand: "bin/rails test", lintCommand: "bin/rubocop", formatCommand: null },
|
|
187
|
-
PHP: { devCommand: "php artisan serve", buildCommand: null, testCommand: "php artisan test", lintCommand: "vendor/bin/phpstan analyse", formatCommand: "vendor/bin/pint" },
|
|
188
|
-
Rust: { devCommand: "cargo run", buildCommand: "cargo build", testCommand: "cargo test", lintCommand: "cargo clippy", formatCommand: "cargo fmt" },
|
|
189
|
-
Java: { devCommand: null, buildCommand: "mvn package", testCommand: "mvn test", lintCommand: null, formatCommand: null },
|
|
190
|
-
Kotlin: { devCommand: null, buildCommand: "mvn package", testCommand: "mvn test", lintCommand: null, formatCommand: null },
|
|
191
|
-
Swift: { devCommand: null, buildCommand: "swift build", testCommand: "swift test", lintCommand: "swiftlint", formatCommand: "swift-format format -r ." },
|
|
192
|
-
Elixir: { devCommand: "mix phx.server", buildCommand: "mix compile", testCommand: "mix test", lintCommand: "mix credo", formatCommand: "mix format" },
|
|
193
|
-
"C#": { devCommand: "dotnet run", buildCommand: "dotnet build", testCommand: "dotnet test", lintCommand: null, formatCommand: "dotnet format" }
|
|
194
|
-
};
|
|
195
|
-
function detectScripts(m) {
|
|
196
|
-
if (m.pkgJson) {
|
|
197
|
-
const scripts = m.pkgJson.scripts ?? {};
|
|
198
|
-
const run = pmRun(m.pkgJson);
|
|
199
|
-
return {
|
|
200
|
-
devCommand: scripts.dev ? `${run} dev` : null,
|
|
201
|
-
buildCommand: scripts.build ? `${run} build` : null,
|
|
202
|
-
testCommand: scripts.test ? `${run} test` : null,
|
|
203
|
-
lintCommand: scripts.lint ? `${run} lint` : null,
|
|
204
|
-
formatCommand: scripts.format ? `${run} format` : null
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (m.language === "Python") {
|
|
208
|
-
const r = m.pyProject?.includes("[tool.uv]") ? "uv run" : "python -m";
|
|
209
|
-
return { devCommand: null, buildCommand: null, testCommand: `${r} pytest`, lintCommand: `${r} ruff check .`, formatCommand: `${r} ruff format .` };
|
|
210
|
-
}
|
|
211
|
-
if (m.language && LANGUAGE_SCRIPTS[m.language]) {
|
|
212
|
-
return LANGUAGE_SCRIPTS[m.language];
|
|
213
|
-
}
|
|
214
|
-
return { devCommand: null, buildCommand: null, testCommand: null, lintCommand: null, formatCommand: null };
|
|
215
|
-
}
|
|
216
|
-
function pmRun(pkg) {
|
|
217
|
-
const pm = pkg.packageManager;
|
|
218
|
-
if (pm?.startsWith("pnpm")) return "pnpm";
|
|
219
|
-
if (pm?.startsWith("yarn")) return "yarn";
|
|
220
|
-
if (pm?.startsWith("bun")) return "bun";
|
|
221
|
-
return "npm run";
|
|
222
|
-
}
|
|
223
|
-
async function globExists(dir, pattern) {
|
|
224
|
-
const { readdir: readdir5 } = await import("fs/promises");
|
|
225
|
-
try {
|
|
226
|
-
const entries = await readdir5(dir);
|
|
227
|
-
return entries.some((e) => e.endsWith(pattern.replace("*", "")));
|
|
228
|
-
} catch {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
28
|
+
import { join } from "path";
|
|
232
29
|
|
|
233
30
|
// src/commands/init/generators/claude-md.ts
|
|
234
31
|
function generateClaudeMd(options, detected) {
|
|
@@ -403,334 +200,6 @@ function buildFormatHook(detected) {
|
|
|
403
200
|
};
|
|
404
201
|
}
|
|
405
202
|
|
|
406
|
-
// src/commands/init/generators/claudeignore.ts
|
|
407
|
-
function generateClaudeignore(detected) {
|
|
408
|
-
const sections = ["# Generated by claude-launchpad"];
|
|
409
|
-
sections.push(`
|
|
410
|
-
# Dependencies
|
|
411
|
-
node_modules/
|
|
412
|
-
.pnp/
|
|
413
|
-
.yarn/
|
|
414
|
-
|
|
415
|
-
# Build output
|
|
416
|
-
dist/
|
|
417
|
-
build/
|
|
418
|
-
out/
|
|
419
|
-
.next/
|
|
420
|
-
.nuxt/
|
|
421
|
-
.output/
|
|
422
|
-
.svelte-kit/
|
|
423
|
-
.vercel/
|
|
424
|
-
.turbo/
|
|
425
|
-
|
|
426
|
-
# Package manager
|
|
427
|
-
pnpm-lock.yaml
|
|
428
|
-
package-lock.json
|
|
429
|
-
yarn.lock
|
|
430
|
-
bun.lockb
|
|
431
|
-
|
|
432
|
-
# IDE & OS
|
|
433
|
-
.vscode/
|
|
434
|
-
.idea/
|
|
435
|
-
*.swp
|
|
436
|
-
*.swo
|
|
437
|
-
.DS_Store
|
|
438
|
-
Thumbs.db
|
|
439
|
-
|
|
440
|
-
# Test & coverage
|
|
441
|
-
coverage/
|
|
442
|
-
.nyc_output/
|
|
443
|
-
__snapshots__/
|
|
444
|
-
|
|
445
|
-
# Environment (should never be read)
|
|
446
|
-
.env
|
|
447
|
-
.env.*
|
|
448
|
-
!.env.example`);
|
|
449
|
-
const lang = detected.language;
|
|
450
|
-
if (lang === "Python") {
|
|
451
|
-
sections.push(`
|
|
452
|
-
# Python
|
|
453
|
-
__pycache__/
|
|
454
|
-
*.pyc
|
|
455
|
-
*.pyo
|
|
456
|
-
.venv/
|
|
457
|
-
venv/
|
|
458
|
-
.mypy_cache/
|
|
459
|
-
.ruff_cache/
|
|
460
|
-
.pytest_cache/
|
|
461
|
-
*.egg-info/`);
|
|
462
|
-
}
|
|
463
|
-
if (lang === "Go") {
|
|
464
|
-
sections.push(`
|
|
465
|
-
# Go
|
|
466
|
-
bin/
|
|
467
|
-
vendor/`);
|
|
468
|
-
}
|
|
469
|
-
if (lang === "Rust") {
|
|
470
|
-
sections.push(`
|
|
471
|
-
# Rust
|
|
472
|
-
target/
|
|
473
|
-
Cargo.lock`);
|
|
474
|
-
}
|
|
475
|
-
if (lang === "Ruby") {
|
|
476
|
-
sections.push(`
|
|
477
|
-
# Ruby
|
|
478
|
-
vendor/bundle/
|
|
479
|
-
.bundle/
|
|
480
|
-
tmp/
|
|
481
|
-
log/`);
|
|
482
|
-
}
|
|
483
|
-
if (lang === "Java" || lang === "Kotlin") {
|
|
484
|
-
sections.push(`
|
|
485
|
-
# JVM
|
|
486
|
-
target/
|
|
487
|
-
build/
|
|
488
|
-
.gradle/
|
|
489
|
-
*.class
|
|
490
|
-
*.jar`);
|
|
491
|
-
}
|
|
492
|
-
if (lang === "Dart") {
|
|
493
|
-
sections.push(`
|
|
494
|
-
# Dart/Flutter
|
|
495
|
-
.dart_tool/
|
|
496
|
-
.packages
|
|
497
|
-
build/`);
|
|
498
|
-
}
|
|
499
|
-
if (lang === "PHP") {
|
|
500
|
-
sections.push(`
|
|
501
|
-
# PHP
|
|
502
|
-
vendor/
|
|
503
|
-
composer.lock`);
|
|
504
|
-
}
|
|
505
|
-
if (lang === "C#") {
|
|
506
|
-
sections.push(`
|
|
507
|
-
# .NET
|
|
508
|
-
bin/
|
|
509
|
-
obj/
|
|
510
|
-
*.dll`);
|
|
511
|
-
}
|
|
512
|
-
if (lang === "Elixir") {
|
|
513
|
-
sections.push(`
|
|
514
|
-
# Elixir
|
|
515
|
-
_build/
|
|
516
|
-
deps/
|
|
517
|
-
.elixir_ls/`);
|
|
518
|
-
}
|
|
519
|
-
if (lang === "Swift") {
|
|
520
|
-
sections.push(`
|
|
521
|
-
# Swift
|
|
522
|
-
.build/
|
|
523
|
-
DerivedData/
|
|
524
|
-
*.xcuserdata`);
|
|
525
|
-
}
|
|
526
|
-
return sections.join("\n") + "\n";
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// src/commands/init/generators/skill-enhance.ts
|
|
530
|
-
var ENHANCE_SKILL_VERSION = 5;
|
|
531
|
-
function generateEnhanceSkill() {
|
|
532
|
-
return [
|
|
533
|
-
"---",
|
|
534
|
-
"name: lp-enhance",
|
|
535
|
-
"description: |",
|
|
536
|
-
" AI-improve your CLAUDE.md based on codebase analysis. Fills in architecture, conventions, guardrails, and suggests hooks and MCP servers.",
|
|
537
|
-
' TRIGGER when: user runs /lp-enhance, asks to "improve CLAUDE.md", "fill in architecture", or after major refactors.',
|
|
538
|
-
" DO NOT TRIGGER when: user is editing CLAUDE.md manually, doing normal coding, or running doctor/eval.",
|
|
539
|
-
"allowed-tools: Read, Glob, Grep, Edit, Write",
|
|
540
|
-
"argument-hint: (no arguments needed)",
|
|
541
|
-
"---",
|
|
542
|
-
"",
|
|
543
|
-
`<!-- lp-enhance-version: ${ENHANCE_SKILL_VERSION} -->`,
|
|
544
|
-
"",
|
|
545
|
-
"# lp-enhance - AI-powered CLAUDE.md improver",
|
|
546
|
-
"",
|
|
547
|
-
"Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.",
|
|
548
|
-
"",
|
|
549
|
-
"## Phase 1: Research",
|
|
550
|
-
"",
|
|
551
|
-
"1. Read CLAUDE.md (if it exists)",
|
|
552
|
-
"2. Read .claude/settings.json (hooks, permissions, MCP)",
|
|
553
|
-
"3. Read .claude/rules/*.md (existing rules)",
|
|
554
|
-
"4. Read .claudeignore (if it exists)",
|
|
555
|
-
"5. Scan src/ directory structure (top-level dirs, key files)",
|
|
556
|
-
"6. Read package.json / go.mod / pyproject.toml for stack detection",
|
|
557
|
-
"7. Check for monorepo indicators (workspaces, nx.json, lerna.json)",
|
|
558
|
-
"8. Check scenarios/ directory for existing eval scenarios",
|
|
559
|
-
"",
|
|
560
|
-
"**Done when:** you have a mental model of the stack, architecture, and existing config.",
|
|
561
|
-
"",
|
|
562
|
-
"## Phase 2: Plan",
|
|
563
|
-
"",
|
|
564
|
-
"Count current CLAUDE.md actionable lines. Budget is 200 lines max. Plan which sections to add or improve:",
|
|
565
|
-
"",
|
|
566
|
-
"1. **## Stack** - detect language, framework, package manager",
|
|
567
|
-
"2. **## Architecture** - 3-5 bullets describing codebase shape",
|
|
568
|
-
"3. **## Conventions** - max 8 key patterns. Overflow to .claude/rules/conventions.md",
|
|
569
|
-
"4. **## Off-Limits** - max 8 guardrails specific to this project",
|
|
570
|
-
"5. **## Memory** - ONLY if agentic-memory is configured in settings.json. Max 6 bullets.",
|
|
571
|
-
"6. **## Key Decisions** - only decisions that affect how Claude works in this codebase",
|
|
572
|
-
"",
|
|
573
|
-
"7. **Skill Authoring** - if .claude/rules/conventions.md lacks a Skill Authoring section, plan to add one",
|
|
574
|
-
"",
|
|
575
|
-
"If any section would exceed 8 bullets, plan a .claude/rules/ file for the overflow.",
|
|
576
|
-
"",
|
|
577
|
-
"**Done when:** you know exactly what to add/change and the line count stays under 200.",
|
|
578
|
-
"",
|
|
579
|
-
"## Phase 3: Execute",
|
|
580
|
-
"",
|
|
581
|
-
"Edit CLAUDE.md with the planned changes. Then:",
|
|
582
|
-
"",
|
|
583
|
-
"1. Create or update .claude/rules/ files for overflow content",
|
|
584
|
-
"2. Generate path-scoped rules if the project has distinct areas (see below)",
|
|
585
|
-
"3. Review .claudeignore and print suggestions (see below)",
|
|
586
|
-
"4. Generate 2-3 custom eval scenarios in scenarios/custom/ (see below)",
|
|
587
|
-
"5. Verify line count is under 200",
|
|
588
|
-
"",
|
|
589
|
-
"**Rules:**",
|
|
590
|
-
"- Don't remove existing content, only add or improve",
|
|
591
|
-
"- Be specific to THIS project, not generic advice",
|
|
592
|
-
"- Use bullet points, not paragraphs",
|
|
593
|
-
"",
|
|
594
|
-
"## Phase 4: Verify",
|
|
595
|
-
"",
|
|
596
|
-
"1. Run `claude-launchpad doctor` to check the score improved",
|
|
597
|
-
"2. Print suggested hooks (exact JSON) for .claude/settings.json but don't modify it",
|
|
598
|
-
"3. Print suggested MCP servers if external services detected (Postgres, Redis, Stripe, etc.)",
|
|
599
|
-
'4. If eval scenarios were generated, print: "Run this in your terminal (not inside Claude Code): `claude-launchpad eval --scenarios scenarios/ --runs 1`"',
|
|
600
|
-
"",
|
|
601
|
-
"**Done when:** doctor score is equal or higher, suggestions printed, eval scenarios created if applicable.",
|
|
602
|
-
"",
|
|
603
|
-
"## Path-scoped rules generation",
|
|
604
|
-
"",
|
|
605
|
-
"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.",
|
|
606
|
-
"",
|
|
607
|
-
"**How to detect areas:**",
|
|
608
|
-
"1. List top-level directories under src/ (or equivalent). Each distinct area (api, components, lib, tests) is a candidate.",
|
|
609
|
-
"2. Check for monorepo indicators: workspaces in package.json, pnpm-workspace.yaml, nx.json, lerna.json. Each workspace is a candidate.",
|
|
610
|
-
"3. Check for docs/, tests/, scripts/ as separate scopes.",
|
|
611
|
-
"",
|
|
612
|
-
"**For each detected area, create a rules file with this format:**",
|
|
613
|
-
"",
|
|
614
|
-
"---",
|
|
615
|
-
'paths: ["src/api/**"]',
|
|
616
|
-
"---",
|
|
617
|
-
"# API Rules",
|
|
618
|
-
"- Validate all request input with zod schemas",
|
|
619
|
-
"- Return typed error responses, never throw raw errors",
|
|
620
|
-
"- Keep route handlers under 30 lines",
|
|
621
|
-
"",
|
|
622
|
-
"**Stack-specific patterns to include:**",
|
|
623
|
-
`- Next.js app/: "Use Server Components by default, add 'use client' only when needed"`,
|
|
624
|
-
'- API routes / src/api/: "Validate input at boundaries, typed error responses"',
|
|
625
|
-
'- React components: "Colocate components near usage, props interface above component"',
|
|
626
|
-
'- Tests: "One assertion per test when possible, descriptive test names"',
|
|
627
|
-
'- Database / prisma/ / drizzle/: "Never write raw SQL, use the ORM, migrations required"',
|
|
628
|
-
'- Docs: "No em dashes, max 3 sentences per paragraph, code examples required"',
|
|
629
|
-
"",
|
|
630
|
-
"**When NOT to generate:**",
|
|
631
|
-
"- Small projects with < 5 source files (one conventions.md is enough)",
|
|
632
|
-
"- Projects where all code is in one flat directory",
|
|
633
|
-
"- If path-scoped rules already exist, don't overwrite them",
|
|
634
|
-
"",
|
|
635
|
-
"**Monorepo handling:**",
|
|
636
|
-
"- Each package gets its own rules file: .claude/rules/packages-<name>.md",
|
|
637
|
-
"- Suggest claudeMdExcludes in settings.json to skip irrelevant package CLAUDE.md files",
|
|
638
|
-
"",
|
|
639
|
-
"## Skill authoring conventions",
|
|
640
|
-
"",
|
|
641
|
-
"If .claude/rules/conventions.md exists but has no Skill Authoring section, add this:",
|
|
642
|
-
"",
|
|
643
|
-
"## Skill Authoring",
|
|
644
|
-
"",
|
|
645
|
-
"When creating Claude Code skills (.claude/skills/*/SKILL.md):",
|
|
646
|
-
"",
|
|
647
|
-
"- Add TRIGGER when / DO NOT TRIGGER when clauses in the description for auto-invocation",
|
|
648
|
-
"- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)",
|
|
649
|
-
"- Add argument-hint in frontmatter showing the expected input format",
|
|
650
|
-
'- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase',
|
|
651
|
-
"- Handle edge cases and preconditions before execution",
|
|
652
|
-
"",
|
|
653
|
-
"## Hook review",
|
|
654
|
-
"",
|
|
655
|
-
"Review .claude/settings.json hooks:",
|
|
656
|
-
"- If you see project-specific patterns that deserve hooks, suggest them",
|
|
657
|
-
"- If no PostCompact hook exists, suggest one that re-injects TASKS.md",
|
|
658
|
-
"- If no SessionStart hook exists, suggest one that injects TASKS.md",
|
|
659
|
-
"- DO NOT modify settings.json directly. Print exact JSON to add.",
|
|
660
|
-
"",
|
|
661
|
-
"## .claudeignore review",
|
|
662
|
-
"",
|
|
663
|
-
"Read .claudeignore and check if the patterns make sense for the detected stack:",
|
|
664
|
-
"",
|
|
665
|
-
"**Always flag:**",
|
|
666
|
-
"- Missing node_modules/ (JS/TS projects)",
|
|
667
|
-
"- Missing __pycache__/ or .venv/ (Python projects)",
|
|
668
|
-
"- Missing target/ (Rust/Java projects)",
|
|
669
|
-
"- Missing .env / .env.* patterns",
|
|
670
|
-
"- Missing lock files (pnpm-lock.yaml, package-lock.json, yarn.lock, etc.)",
|
|
671
|
-
"- Missing coverage/ directory",
|
|
672
|
-
"- Large generated files that waste context (*.min.js, *.map, migrations/)",
|
|
673
|
-
"",
|
|
674
|
-
"**Never flag:**",
|
|
675
|
-
"- Patterns the user clearly added intentionally",
|
|
676
|
-
"- Test fixtures or seed data (might be needed for context)",
|
|
677
|
-
"",
|
|
678
|
-
"If .claudeignore is missing entirely, create one with sensible defaults for the detected stack.",
|
|
679
|
-
"If it exists but has gaps, print suggested additions. Do NOT modify it directly.",
|
|
680
|
-
"",
|
|
681
|
-
"## Eval scenario generation",
|
|
682
|
-
"",
|
|
683
|
-
"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.",
|
|
684
|
-
"",
|
|
685
|
-
"**Scenario YAML format:**",
|
|
686
|
-
"```yaml",
|
|
687
|
-
"name: custom/scenario-name",
|
|
688
|
-
"description: What this scenario tests",
|
|
689
|
-
"setup:",
|
|
690
|
-
" files:",
|
|
691
|
-
" - path: src/example.ts",
|
|
692
|
-
" content: |",
|
|
693
|
-
" // Starter file that tempts Claude to break a rule",
|
|
694
|
-
" instructions: |",
|
|
695
|
-
" The specific rule from CLAUDE.md being tested.",
|
|
696
|
-
'prompt: "A task that would tempt Claude to break the rule"',
|
|
697
|
-
"checks:",
|
|
698
|
-
" - type: grep",
|
|
699
|
-
' pattern: "expected_pattern"',
|
|
700
|
-
" target: src/example.ts",
|
|
701
|
-
" expect: present",
|
|
702
|
-
" points: 5",
|
|
703
|
-
" label: What this check verifies",
|
|
704
|
-
" - type: file-exists",
|
|
705
|
-
" target: path/to/expected/file",
|
|
706
|
-
" expect: present",
|
|
707
|
-
" points: 5",
|
|
708
|
-
" label: What this check verifies",
|
|
709
|
-
"passingScore: 7",
|
|
710
|
-
"runs: 3",
|
|
711
|
-
"```",
|
|
712
|
-
"",
|
|
713
|
-
"**How to choose scenarios:**",
|
|
714
|
-
"1. Pick the 2-3 most important rules from ## Off-Limits and ## Conventions",
|
|
715
|
-
"2. Design a task that naturally tempts Claude to break each rule",
|
|
716
|
-
"3. Write checks that verify compliance (grep for patterns, file-exists for structure)",
|
|
717
|
-
"",
|
|
718
|
-
"**Check types available:** `grep` (pattern in file), `file-exists` (present/absent), `max-lines` (file length)",
|
|
719
|
-
"",
|
|
720
|
-
"**Examples of good custom scenarios:**",
|
|
721
|
-
'- Off-limits says "never use any" \u2192 task asks to build types, check for no `any` keyword',
|
|
722
|
-
'- Convention says "max 400 lines per file" \u2192 task asks to generate a large module, check line count',
|
|
723
|
-
'- Off-limits says "no raw SQL" \u2192 task asks to add a query, check for ORM usage',
|
|
724
|
-
"",
|
|
725
|
-
"**Skip if:** scenarios/ already has 3+ YAML files, or CLAUDE.md has no project-specific rules worth testing.",
|
|
726
|
-
"",
|
|
727
|
-
"## Other advanced configuration",
|
|
728
|
-
"",
|
|
729
|
-
"- If the project uses external APIs, suggest sandbox.network.allowedDomains",
|
|
730
|
-
"- If you detect a monorepo, suggest claudeMdExcludes in settings.json"
|
|
731
|
-
].join("\n");
|
|
732
|
-
}
|
|
733
|
-
|
|
734
203
|
// src/commands/init/generators/backlog.ts
|
|
735
204
|
function generateBacklogMd(options) {
|
|
736
205
|
return `# ${options.name} - Backlog
|
|
@@ -769,8 +238,12 @@ function createInitCommand() {
|
|
|
769
238
|
message: "One-line description (optional):"
|
|
770
239
|
});
|
|
771
240
|
const options = { name: name.trim(), description: description.trim() };
|
|
772
|
-
const hasClaudeMd = await fileExists(
|
|
773
|
-
if (hasClaudeMd
|
|
241
|
+
const hasClaudeMd = await fileExists(join(root, "CLAUDE.md"));
|
|
242
|
+
if (hasClaudeMd) {
|
|
243
|
+
if (opts.yes) {
|
|
244
|
+
log.info("CLAUDE.md already exists. Use `doctor --fix` to update, or re-run without --yes to overwrite.");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
774
247
|
const overwrite = await confirm({
|
|
775
248
|
message: "CLAUDE.md already exists. Overwrite?",
|
|
776
249
|
default: false
|
|
@@ -792,20 +265,20 @@ async function scaffold(root, options, detected, skipPrompts) {
|
|
|
792
265
|
const backlogMd = generateBacklogMd(options);
|
|
793
266
|
const settings = generateSettings(detected);
|
|
794
267
|
const claudeignore = generateClaudeignore(detected);
|
|
795
|
-
await mkdir(
|
|
796
|
-
const settingsPath =
|
|
268
|
+
await mkdir(join(root, ".claude", "rules"), { recursive: true });
|
|
269
|
+
const settingsPath = join(root, ".claude", "settings.json");
|
|
797
270
|
const mergedSettings = await mergeSettings(settingsPath, settings);
|
|
798
|
-
const backlogPath =
|
|
271
|
+
const backlogPath = join(root, "BACKLOG.md");
|
|
799
272
|
const hasBacklog = await fileExists(backlogPath);
|
|
800
|
-
const claudeignorePath =
|
|
273
|
+
const claudeignorePath = join(root, ".claudeignore");
|
|
801
274
|
const hasClaudeignore = await fileExists(claudeignorePath);
|
|
802
|
-
const claudeGitignorePath =
|
|
275
|
+
const claudeGitignorePath = join(root, ".claude", ".gitignore");
|
|
803
276
|
const hasClaudeGitignore = await fileExists(claudeGitignorePath);
|
|
804
|
-
const rulesPath =
|
|
277
|
+
const rulesPath = join(root, ".claude", "rules", "conventions.md");
|
|
805
278
|
const hasRules = await fileExists(rulesPath);
|
|
806
279
|
const writes = [
|
|
807
|
-
writeFile(
|
|
808
|
-
writeFile(
|
|
280
|
+
writeFile(join(root, "CLAUDE.md"), claudeMd),
|
|
281
|
+
writeFile(join(root, "TASKS.md"), tasksMd),
|
|
809
282
|
writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n")
|
|
810
283
|
];
|
|
811
284
|
if (!hasBacklog) {
|
|
@@ -886,10 +359,10 @@ function generateStarterRules(detected) {
|
|
|
886
359
|
return lines.join("\n");
|
|
887
360
|
}
|
|
888
361
|
async function createEnhanceSkillPrompt(root, skipPrompts) {
|
|
889
|
-
const projectPath =
|
|
890
|
-
const globalPath =
|
|
891
|
-
const legacyProject =
|
|
892
|
-
const legacyGlobal =
|
|
362
|
+
const projectPath = join(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
363
|
+
const globalPath = join(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
364
|
+
const legacyProject = join(root, ".claude", "commands", "lp-enhance.md");
|
|
365
|
+
const legacyGlobal = join(homedir(), ".claude", "commands", "lp-enhance.md");
|
|
893
366
|
if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return;
|
|
894
367
|
const scope = skipPrompts ? "project" : await select({
|
|
895
368
|
message: "Install /lp-enhance skill (AI-powered CLAUDE.md improver):",
|
|
@@ -900,14 +373,14 @@ async function createEnhanceSkillPrompt(root, skipPrompts) {
|
|
|
900
373
|
]
|
|
901
374
|
});
|
|
902
375
|
if (scope === "skip") return;
|
|
903
|
-
const targetDir = scope === "global" ?
|
|
376
|
+
const targetDir = scope === "global" ? join(homedir(), ".claude", "skills", "lp-enhance") : join(root, ".claude", "skills", "lp-enhance");
|
|
904
377
|
await mkdir(targetDir, { recursive: true });
|
|
905
|
-
await writeFile(
|
|
378
|
+
await writeFile(join(targetDir, "SKILL.md"), generateEnhanceSkill());
|
|
906
379
|
log.success(`Generated /lp-enhance skill (${scope} scope)`);
|
|
907
380
|
}
|
|
908
381
|
async function mergeSettings(existingPath, generated) {
|
|
909
382
|
try {
|
|
910
|
-
const existing = JSON.parse(await
|
|
383
|
+
const existing = JSON.parse(await readFile(existingPath, "utf-8"));
|
|
911
384
|
const existingHooks = existing.hooks ?? {};
|
|
912
385
|
const generatedHooks = generated.hooks ?? {};
|
|
913
386
|
const mergedHooks = { ...existingHooks };
|
|
@@ -931,8 +404,8 @@ import { Command as Command2 } from "commander";
|
|
|
931
404
|
import chalk from "chalk";
|
|
932
405
|
|
|
933
406
|
// src/lib/parser.ts
|
|
934
|
-
import { readdir, access
|
|
935
|
-
import { join as
|
|
407
|
+
import { readdir, access } from "fs/promises";
|
|
408
|
+
import { join as join2, resolve } from "path";
|
|
936
409
|
var CLAUDE_MD = "CLAUDE.md";
|
|
937
410
|
var CLAUDE_DIR = ".claude";
|
|
938
411
|
var SETTINGS_FILE = "settings.json";
|
|
@@ -940,24 +413,24 @@ var SETTINGS_LOCAL_FILE = "settings.local.json";
|
|
|
940
413
|
var RULES_DIR = "rules";
|
|
941
414
|
async function parseClaudeConfig(projectRoot) {
|
|
942
415
|
const root = resolve(projectRoot);
|
|
943
|
-
const claudeDir =
|
|
416
|
+
const claudeDir = join2(root, CLAUDE_DIR);
|
|
944
417
|
const [claudeMd, localClaudeMd, settings, localSettings, hooks, rules, mcpServers, skills, claudeignore] = await Promise.all([
|
|
945
418
|
readClaudeMd(root),
|
|
946
|
-
readFileOrNull(
|
|
419
|
+
readFileOrNull(join2(claudeDir, CLAUDE_MD)),
|
|
947
420
|
readSettings(claudeDir),
|
|
948
421
|
readSettingsFromFile(claudeDir, SETTINGS_LOCAL_FILE),
|
|
949
422
|
readHooks(claudeDir),
|
|
950
423
|
readRules(claudeDir),
|
|
951
424
|
readMcpServers(claudeDir, root),
|
|
952
425
|
readSkills(claudeDir),
|
|
953
|
-
readFileOrNull(
|
|
426
|
+
readFileOrNull(join2(root, ".claudeignore"))
|
|
954
427
|
]);
|
|
955
428
|
const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
|
|
956
429
|
return {
|
|
957
|
-
claudeMdPath: claudeMd !== null ?
|
|
430
|
+
claudeMdPath: claudeMd !== null ? join2(root, CLAUDE_MD) : null,
|
|
958
431
|
claudeMdContent: claudeMd,
|
|
959
432
|
claudeMdInstructionCount: instructionCount,
|
|
960
|
-
settingsPath: settings !== null ?
|
|
433
|
+
settingsPath: settings !== null ? join2(claudeDir, SETTINGS_FILE) : null,
|
|
961
434
|
settings,
|
|
962
435
|
localClaudeMdContent: localClaudeMd,
|
|
963
436
|
localSettings,
|
|
@@ -965,12 +438,12 @@ async function parseClaudeConfig(projectRoot) {
|
|
|
965
438
|
rules,
|
|
966
439
|
mcpServers,
|
|
967
440
|
skills,
|
|
968
|
-
claudeignorePath: claudeignore !== null ?
|
|
441
|
+
claudeignorePath: claudeignore !== null ? join2(root, ".claudeignore") : null,
|
|
969
442
|
claudeignoreContent: claudeignore
|
|
970
443
|
};
|
|
971
444
|
}
|
|
972
445
|
async function readClaudeMd(root) {
|
|
973
|
-
return readFileOrNull(
|
|
446
|
+
return readFileOrNull(join2(root, CLAUDE_MD));
|
|
974
447
|
}
|
|
975
448
|
function countInstructions(content) {
|
|
976
449
|
const lines = content.split("\n");
|
|
@@ -989,7 +462,7 @@ async function readSettings(claudeDir) {
|
|
|
989
462
|
return readSettingsFromFile(claudeDir, SETTINGS_FILE);
|
|
990
463
|
}
|
|
991
464
|
async function readSettingsFromFile(claudeDir, filename) {
|
|
992
|
-
const raw = await readFileOrNull(
|
|
465
|
+
const raw = await readFileOrNull(join2(claudeDir, filename));
|
|
993
466
|
if (raw === null) return null;
|
|
994
467
|
try {
|
|
995
468
|
return JSON.parse(raw);
|
|
@@ -999,8 +472,8 @@ async function readSettingsFromFile(claudeDir, filename) {
|
|
|
999
472
|
}
|
|
1000
473
|
async function readHooks(claudeDir) {
|
|
1001
474
|
const [shared, local] = await Promise.all([
|
|
1002
|
-
readHooksFromFile(
|
|
1003
|
-
readHooksFromFile(
|
|
475
|
+
readHooksFromFile(join2(claudeDir, SETTINGS_FILE)),
|
|
476
|
+
readHooksFromFile(join2(claudeDir, SETTINGS_LOCAL_FILE))
|
|
1004
477
|
]);
|
|
1005
478
|
return [...shared, ...local];
|
|
1006
479
|
}
|
|
@@ -1046,14 +519,14 @@ async function readHooksFromFile(settingsPath) {
|
|
|
1046
519
|
}
|
|
1047
520
|
}
|
|
1048
521
|
async function readRules(claudeDir) {
|
|
1049
|
-
const rulesDir =
|
|
522
|
+
const rulesDir = join2(claudeDir, RULES_DIR);
|
|
1050
523
|
return listFilesRecursive(rulesDir, ".md");
|
|
1051
524
|
}
|
|
1052
525
|
async function readMcpServers(claudeDir, projectRoot) {
|
|
1053
526
|
const [fromMcpJson, fromSettings, fromLocalSettings] = await Promise.all([
|
|
1054
|
-
readMcpJsonFile(
|
|
1055
|
-
readMcpServersFromSettings(
|
|
1056
|
-
readMcpServersFromSettings(
|
|
527
|
+
readMcpJsonFile(join2(projectRoot, ".mcp.json")),
|
|
528
|
+
readMcpServersFromSettings(join2(claudeDir, SETTINGS_FILE)),
|
|
529
|
+
readMcpServersFromSettings(join2(claudeDir, SETTINGS_LOCAL_FILE))
|
|
1057
530
|
]);
|
|
1058
531
|
const seen = /* @__PURE__ */ new Set();
|
|
1059
532
|
const result = [];
|
|
@@ -1077,7 +550,7 @@ async function readMcpJsonFile(mcpJsonPath) {
|
|
|
1077
550
|
const c = config;
|
|
1078
551
|
result.push({
|
|
1079
552
|
name,
|
|
1080
|
-
transport: c.transport ?? "stdio",
|
|
553
|
+
transport: c.transport ?? c.type ?? "stdio",
|
|
1081
554
|
command: c.command,
|
|
1082
555
|
url: c.url
|
|
1083
556
|
});
|
|
@@ -1099,7 +572,7 @@ async function readMcpServersFromSettings(settingsPath) {
|
|
|
1099
572
|
const c = config;
|
|
1100
573
|
result.push({
|
|
1101
574
|
name,
|
|
1102
|
-
transport: c.transport ?? "stdio",
|
|
575
|
+
transport: c.transport ?? c.type ?? "stdio",
|
|
1103
576
|
command: c.command,
|
|
1104
577
|
url: c.url
|
|
1105
578
|
});
|
|
@@ -1110,8 +583,8 @@ async function readMcpServersFromSettings(settingsPath) {
|
|
|
1110
583
|
}
|
|
1111
584
|
}
|
|
1112
585
|
async function readSkills(claudeDir) {
|
|
1113
|
-
const commandsDir =
|
|
1114
|
-
const skillsDir =
|
|
586
|
+
const commandsDir = join2(claudeDir, "commands");
|
|
587
|
+
const skillsDir = join2(claudeDir, "skills");
|
|
1115
588
|
const [commands, skills] = await Promise.all([
|
|
1116
589
|
listFilesRecursive(commandsDir, ".md"),
|
|
1117
590
|
listFilesRecursive(skillsDir, ".md")
|
|
@@ -1120,14 +593,14 @@ async function readSkills(claudeDir) {
|
|
|
1120
593
|
}
|
|
1121
594
|
async function listFilesRecursive(dir, ext) {
|
|
1122
595
|
try {
|
|
1123
|
-
await
|
|
596
|
+
await access(dir);
|
|
1124
597
|
} catch {
|
|
1125
598
|
return [];
|
|
1126
599
|
}
|
|
1127
600
|
const results = [];
|
|
1128
601
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
1129
602
|
for (const entry of entries) {
|
|
1130
|
-
const fullPath =
|
|
603
|
+
const fullPath = join2(dir, entry.name);
|
|
1131
604
|
if (entry.isDirectory()) {
|
|
1132
605
|
const nested = await listFilesRecursive(fullPath, ext);
|
|
1133
606
|
results.push(...nested);
|
|
@@ -1327,13 +800,13 @@ async function analyzeHooks(config) {
|
|
|
1327
800
|
}
|
|
1328
801
|
|
|
1329
802
|
// src/commands/doctor/analyzers/rules.ts
|
|
1330
|
-
import { readFile as
|
|
1331
|
-
import { basename
|
|
803
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
804
|
+
import { basename, join as join3, dirname } from "path";
|
|
1332
805
|
import { homedir as homedir2 } from "os";
|
|
1333
806
|
async function analyzeRules(config) {
|
|
1334
807
|
const issues = [];
|
|
1335
808
|
const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
|
|
1336
|
-
const hasBacklog = await fileExists(
|
|
809
|
+
const hasBacklog = await fileExists(join3(projectRoot, "BACKLOG.md"));
|
|
1337
810
|
if (!hasBacklog) {
|
|
1338
811
|
issues.push({
|
|
1339
812
|
analyzer: "Rules",
|
|
@@ -1342,7 +815,7 @@ async function analyzeRules(config) {
|
|
|
1342
815
|
fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
|
|
1343
816
|
});
|
|
1344
817
|
}
|
|
1345
|
-
const hasClaudeignore = await fileExists(
|
|
818
|
+
const hasClaudeignore = await fileExists(join3(projectRoot, ".claudeignore"));
|
|
1346
819
|
if (!hasClaudeignore) {
|
|
1347
820
|
issues.push({
|
|
1348
821
|
analyzer: "Rules",
|
|
@@ -1352,9 +825,9 @@ async function analyzeRules(config) {
|
|
|
1352
825
|
});
|
|
1353
826
|
}
|
|
1354
827
|
const hasSkillInProject = config.skills.some(
|
|
1355
|
-
(s) =>
|
|
828
|
+
(s) => basename(s) === "SKILL.md" && s.includes("lp-enhance") || basename(s) === "lp-enhance.md"
|
|
1356
829
|
);
|
|
1357
|
-
const hasSkillGlobal = await fileExists(
|
|
830
|
+
const hasSkillGlobal = await fileExists(join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")) || await fileExists(join3(homedir2(), ".claude", "commands", "lp-enhance.md"));
|
|
1358
831
|
if (!hasSkillInProject && !hasSkillGlobal) {
|
|
1359
832
|
issues.push({
|
|
1360
833
|
analyzer: "Rules",
|
|
@@ -1384,27 +857,27 @@ async function analyzeRules(config) {
|
|
|
1384
857
|
}
|
|
1385
858
|
for (const rulePath of config.rules) {
|
|
1386
859
|
try {
|
|
1387
|
-
const content = await
|
|
860
|
+
const content = await readFile2(rulePath, "utf-8");
|
|
1388
861
|
const trimmed = content.trim();
|
|
1389
862
|
if (trimmed.length === 0) {
|
|
1390
863
|
issues.push({
|
|
1391
864
|
analyzer: "Rules",
|
|
1392
865
|
severity: "low",
|
|
1393
|
-
message: `Empty rule file: ${
|
|
1394
|
-
fix: `Add content to ${
|
|
866
|
+
message: `Empty rule file: ${basename(rulePath)}`,
|
|
867
|
+
fix: `Add content to ${basename(rulePath)} or delete it`
|
|
1395
868
|
});
|
|
1396
869
|
} else if (trimmed.length < 20) {
|
|
1397
870
|
issues.push({
|
|
1398
871
|
analyzer: "Rules",
|
|
1399
872
|
severity: "info",
|
|
1400
|
-
message: `Very short rule file (${trimmed.length} chars): ${
|
|
873
|
+
message: `Very short rule file (${trimmed.length} chars): ${basename(rulePath)}`
|
|
1401
874
|
});
|
|
1402
875
|
}
|
|
1403
876
|
} catch {
|
|
1404
877
|
issues.push({
|
|
1405
878
|
analyzer: "Rules",
|
|
1406
879
|
severity: "low",
|
|
1407
|
-
message: `Could not read rule file: ${
|
|
880
|
+
message: `Could not read rule file: ${basename(rulePath)}`
|
|
1408
881
|
});
|
|
1409
882
|
}
|
|
1410
883
|
}
|
|
@@ -1413,12 +886,12 @@ async function analyzeRules(config) {
|
|
|
1413
886
|
}
|
|
1414
887
|
async function getSkillVersion(projectRoot) {
|
|
1415
888
|
const paths = [
|
|
1416
|
-
|
|
1417
|
-
|
|
889
|
+
join3(projectRoot, ".claude", "skills", "lp-enhance", "SKILL.md"),
|
|
890
|
+
join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")
|
|
1418
891
|
];
|
|
1419
892
|
for (const p of paths) {
|
|
1420
893
|
try {
|
|
1421
|
-
const content = await
|
|
894
|
+
const content = await readFile2(p, "utf-8");
|
|
1422
895
|
const match = content.match(/<!-- lp-enhance-version: (\d+) -->/);
|
|
1423
896
|
if (match) return parseInt(match[1], 10);
|
|
1424
897
|
return 0;
|
|
@@ -1527,7 +1000,7 @@ async function analyzePermissions(config) {
|
|
|
1527
1000
|
}
|
|
1528
1001
|
|
|
1529
1002
|
// src/commands/doctor/analyzers/mcp.ts
|
|
1530
|
-
import { access as
|
|
1003
|
+
import { access as access2 } from "fs/promises";
|
|
1531
1004
|
async function analyzeMcp(config) {
|
|
1532
1005
|
const issues = [];
|
|
1533
1006
|
const servers = config.mcpServers;
|
|
@@ -1560,7 +1033,7 @@ async function analyzeMcp(config) {
|
|
|
1560
1033
|
const executable = server.command.split(" ")[0];
|
|
1561
1034
|
if (executable.startsWith("/") || executable.startsWith("./")) {
|
|
1562
1035
|
try {
|
|
1563
|
-
await
|
|
1036
|
+
await access2(executable);
|
|
1564
1037
|
} catch {
|
|
1565
1038
|
issues.push({
|
|
1566
1039
|
analyzer: "MCP",
|
|
@@ -1801,420 +1274,9 @@ async function analyzeQuality(config) {
|
|
|
1801
1274
|
return { name: "CLAUDE.md Quality", issues, score };
|
|
1802
1275
|
}
|
|
1803
1276
|
|
|
1804
|
-
// src/commands/doctor/fixer.ts
|
|
1805
|
-
import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access4 } from "fs/promises";
|
|
1806
|
-
import { join as join6 } from "path";
|
|
1807
|
-
import { homedir as homedir3 } from "os";
|
|
1808
|
-
|
|
1809
|
-
// src/commands/doctor/fixer-memory.ts
|
|
1810
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1811
|
-
import { join as join5 } from "path";
|
|
1812
|
-
async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
|
|
1813
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1814
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1815
|
-
const settings = await read(root);
|
|
1816
|
-
const hooks = settings.hooks ?? {};
|
|
1817
|
-
const hookList = hooks[event] ?? [];
|
|
1818
|
-
const alreadyHas = hookList.some((g) => {
|
|
1819
|
-
const nested = g.hooks;
|
|
1820
|
-
return nested?.some((h) => String(h.command ?? "").includes(dedupKeyword));
|
|
1821
|
-
});
|
|
1822
|
-
if (alreadyHas) return false;
|
|
1823
|
-
hooks[event] = prepend ? [entry, ...hookList] : [...hookList, entry];
|
|
1824
|
-
settings.hooks = hooks;
|
|
1825
|
-
await write(root, settings);
|
|
1826
|
-
log.success(successMsg);
|
|
1827
|
-
return true;
|
|
1828
|
-
}
|
|
1829
|
-
async function disableAutoMemory(root, placement) {
|
|
1830
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1831
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1832
|
-
const settings = await read(root);
|
|
1833
|
-
if (settings.autoMemoryEnabled === false) return false;
|
|
1834
|
-
settings.autoMemoryEnabled = false;
|
|
1835
|
-
await write(root, settings);
|
|
1836
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1837
|
-
log.success(`Set autoMemoryEnabled: false in ${target}`);
|
|
1838
|
-
return true;
|
|
1839
|
-
}
|
|
1840
|
-
async function addMemoryToolPermissions(root, placement) {
|
|
1841
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1842
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1843
|
-
const settings = await read(root);
|
|
1844
|
-
const permissions = settings.permissions ?? {};
|
|
1845
|
-
const allow = permissions.allow ?? [];
|
|
1846
|
-
const tools = [
|
|
1847
|
-
"mcp__agentic-memory__memory_store",
|
|
1848
|
-
"mcp__agentic-memory__memory_search",
|
|
1849
|
-
"mcp__agentic-memory__memory_recent",
|
|
1850
|
-
"mcp__agentic-memory__memory_forget",
|
|
1851
|
-
"mcp__agentic-memory__memory_relate",
|
|
1852
|
-
"mcp__agentic-memory__memory_stats",
|
|
1853
|
-
"mcp__agentic-memory__memory_update"
|
|
1854
|
-
];
|
|
1855
|
-
const missing = tools.filter((t) => !allow.includes(t));
|
|
1856
|
-
if (missing.length === 0) return false;
|
|
1857
|
-
settings.permissions = { ...permissions, allow: [...allow, ...missing] };
|
|
1858
|
-
await write(root, settings);
|
|
1859
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1860
|
-
log.success(`Added agentic-memory MCP tool permissions to ${target}`);
|
|
1861
|
-
return true;
|
|
1862
|
-
}
|
|
1863
|
-
async function addSessionStartPullHook(root, placement) {
|
|
1864
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1865
|
-
return addPlacementHook(root, placement, "SessionStart", "memory pull", {
|
|
1866
|
-
matcher: "startup",
|
|
1867
|
-
hooks: [{ type: "command", command: "claude-launchpad memory pull -y 2>/dev/null; exit 0" }]
|
|
1868
|
-
}, true, `Added SessionStart hook for memory sync to ${target}`);
|
|
1869
|
-
}
|
|
1870
|
-
async function addSessionEndPushHook(root, placement) {
|
|
1871
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1872
|
-
return addPlacementHook(root, placement, "SessionEnd", "memory push", {
|
|
1873
|
-
hooks: [{ type: "command", command: "claude-launchpad memory push -y >/dev/null 2>&1 & exit 0" }]
|
|
1874
|
-
}, false, `Added SessionEnd hook for memory sync to ${target}`);
|
|
1875
|
-
}
|
|
1876
|
-
async function removeStaleStopHook(root) {
|
|
1877
|
-
const settings = await readSettingsJson(root);
|
|
1878
|
-
const hooks = settings.hooks;
|
|
1879
|
-
if (!hooks?.Stop) return false;
|
|
1880
|
-
const stopHooks = hooks.Stop;
|
|
1881
|
-
const filtered = stopHooks.filter((h) => {
|
|
1882
|
-
const innerHooks = h.hooks;
|
|
1883
|
-
return !innerHooks?.some(
|
|
1884
|
-
(ih) => typeof ih.command === "string" && ih.command.includes("memory extract")
|
|
1885
|
-
);
|
|
1886
|
-
});
|
|
1887
|
-
if (filtered.length === stopHooks.length) return false;
|
|
1888
|
-
const updated = filtered.length === 0 ? (({ Stop: _, ...rest }) => rest)(hooks) : { ...hooks, Stop: filtered };
|
|
1889
|
-
settings.hooks = updated;
|
|
1890
|
-
await writeSettingsJson(root, settings);
|
|
1891
|
-
log.success("Removed deprecated Stop hook (memory extract)");
|
|
1892
|
-
return true;
|
|
1893
|
-
}
|
|
1894
|
-
async function addAllowedMcpServers(root, placement) {
|
|
1895
|
-
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
1896
|
-
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
1897
|
-
const settings = await read(root);
|
|
1898
|
-
if (settings.allowedMcpServers) return false;
|
|
1899
|
-
const other = placement === "local" ? await readSettingsJson(root) : await readSettingsLocalJson(root);
|
|
1900
|
-
if (other.allowedMcpServers) return false;
|
|
1901
|
-
const serverNames = /* @__PURE__ */ new Set();
|
|
1902
|
-
const settingsServers = settings.mcpServers;
|
|
1903
|
-
if (settingsServers && typeof settingsServers === "object") {
|
|
1904
|
-
for (const name of Object.keys(settingsServers)) serverNames.add(name);
|
|
1905
|
-
}
|
|
1906
|
-
const mcpJsonPath = join5(root, ".mcp.json");
|
|
1907
|
-
try {
|
|
1908
|
-
const mcpJson = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
|
|
1909
|
-
const mcpServers = mcpJson.mcpServers;
|
|
1910
|
-
if (mcpServers && typeof mcpServers === "object") {
|
|
1911
|
-
for (const name of Object.keys(mcpServers)) serverNames.add(name);
|
|
1912
|
-
}
|
|
1913
|
-
} catch {
|
|
1914
|
-
}
|
|
1915
|
-
if (serverNames.size === 0) return false;
|
|
1916
|
-
settings.allowedMcpServers = [...serverNames].map(
|
|
1917
|
-
(name) => ({ serverName: name })
|
|
1918
|
-
);
|
|
1919
|
-
await write(root, settings);
|
|
1920
|
-
const target = placement === "local" ? "settings.local.json" : "settings.json";
|
|
1921
|
-
log.success(`Added allowedMcpServers from configured servers to ${target}`);
|
|
1922
|
-
return true;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
// src/commands/doctor/fixer.ts
|
|
1926
|
-
async function applyFixes(issues, projectRoot) {
|
|
1927
|
-
const detected = await detectProject(projectRoot);
|
|
1928
|
-
const hasMemoryIssues = issues.some((i) => i.analyzer === "Memory");
|
|
1929
|
-
const placement = hasMemoryIssues ? await getMemoryPlacement(projectRoot) : "shared";
|
|
1930
|
-
let fixed = 0;
|
|
1931
|
-
let skipped = 0;
|
|
1932
|
-
for (const issue of issues) {
|
|
1933
|
-
const applied = await tryFix(issue, projectRoot, detected, placement);
|
|
1934
|
-
if (applied) {
|
|
1935
|
-
fixed++;
|
|
1936
|
-
} else {
|
|
1937
|
-
skipped++;
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
return { fixed, skipped };
|
|
1941
|
-
}
|
|
1942
|
-
var FIX_TABLE = [
|
|
1943
|
-
{ analyzer: "Hooks", match: "No hooks configured", fix: async (root, detected) => {
|
|
1944
|
-
const a = await addEnvProtectionHook(root);
|
|
1945
|
-
const b = await addAutoFormatHook(root, detected);
|
|
1946
|
-
const c = await addForcePushProtection(root);
|
|
1947
|
-
const d = await addSessionStartHook(root);
|
|
1948
|
-
return a || b || c || d;
|
|
1949
|
-
} },
|
|
1950
|
-
{ analyzer: "Hooks", match: ".env file protection", fix: (root) => addEnvProtectionHook(root) },
|
|
1951
|
-
{ analyzer: "Hooks", match: "auto-format", fix: (root, detected) => addAutoFormatHook(root, detected) },
|
|
1952
|
-
{ analyzer: "Hooks", match: "No PreToolUse", fix: (root) => addEnvProtectionHook(root) },
|
|
1953
|
-
{ analyzer: "Quality", match: "Architecture", fix: (root) => addClaudeMdSection(root, "## Architecture", "<!-- TODO: Describe your codebase structure. Run `/lp-enhance` to auto-fill this. -->") },
|
|
1954
|
-
{ 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") },
|
|
1955
|
-
{ analyzer: "Quality", match: "Commands", fix: (root) => addClaudeMdSection(root, "## Commands", "<!-- TODO: Add your dev/build/test commands -->") },
|
|
1956
|
-
{ analyzer: "Quality", match: "Stack", fix: (root, detected) => {
|
|
1957
|
-
const content = detected.language ? `- **Language**: ${detected.language}${detected.framework ? `
|
|
1958
|
-
- **Framework**: ${detected.framework}` : ""}${detected.packageManager ? `
|
|
1959
|
-
- **Package Manager**: ${detected.packageManager}` : ""}` : "<!-- TODO: Define your tech stack -->";
|
|
1960
|
-
return addClaudeMdSection(root, "## Stack", content);
|
|
1961
|
-
} },
|
|
1962
|
-
{ 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") },
|
|
1963
|
-
{ 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") },
|
|
1964
|
-
{ analyzer: "Rules", match: "No BACKLOG.md", fix: (root) => createBacklogMd(root) },
|
|
1965
|
-
{ analyzer: "Rules", match: "No .claudeignore", fix: (root, detected) => createClaudeignore(root, detected) },
|
|
1966
|
-
{ analyzer: "Rules", match: "No .claude/rules/", fix: (root) => createStarterRules(root) },
|
|
1967
|
-
{ analyzer: "Hooks", match: "PostCompact", fix: (root) => addPostCompactHook(root) },
|
|
1968
|
-
{ analyzer: "Permissions", match: "force-push", fix: (root) => addForcePushProtection(root) },
|
|
1969
|
-
{ analyzer: "Permissions", match: "Credential files not blocked", fix: (root) => addCredentialDenyRules(root) },
|
|
1970
|
-
{ analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
|
|
1971
|
-
{ analyzer: "Permissions", match: "Sandbox not enabled", fix: (root) => addSandboxSettings(root) },
|
|
1972
|
-
{ analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
|
|
1973
|
-
{ analyzer: "Rules", match: "No /lp-enhance skill", fix: (root) => createEnhanceSkill(root) },
|
|
1974
|
-
{ analyzer: "Rules", match: "lp-enhance skill is outdated", fix: (root) => updateEnhanceSkill(root) },
|
|
1975
|
-
{ analyzer: "Settings", match: "Deprecated includeCoAuthoredBy", fix: (root) => migrateAttribution(root) },
|
|
1976
|
-
{ analyzer: "Hooks", match: "SessionStart", fix: (root) => addSessionStartHook(root) },
|
|
1977
|
-
{ analyzer: "Memory", match: "Deprecated Stop hook", fix: (root) => removeStaleStopHook(root) },
|
|
1978
|
-
{ analyzer: "Memory", match: "autoMemoryEnabled not disabled", fix: (root, _det, placement) => disableAutoMemory(root, placement) },
|
|
1979
|
-
{ analyzer: "Memory", match: "MCP tool permission", fix: (root, _det, placement) => addMemoryToolPermissions(root, placement) },
|
|
1980
|
-
{ analyzer: "MCP", match: "no allowedMcpServers", fix: (root, _det, placement) => addAllowedMcpServers(root, placement) },
|
|
1981
|
-
{ analyzer: "Memory", match: "SessionStart hook to auto-pull", fix: (root, _det, placement) => addSessionStartPullHook(root, placement) },
|
|
1982
|
-
{ analyzer: "Memory", match: "SessionEnd hook to auto-push", fix: (root, _det, placement) => addSessionEndPushHook(root, placement) },
|
|
1983
|
-
{ analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
|
|
1984
|
-
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";
|
|
1985
|
-
const target = placement === "local" ? join6(root, ".claude", "CLAUDE.md") : void 0;
|
|
1986
|
-
return addClaudeMdSection(root, "## Memory", content, target);
|
|
1987
|
-
} }
|
|
1988
|
-
];
|
|
1989
|
-
async function tryFix(issue, root, detected, placement) {
|
|
1990
|
-
const entry = FIX_TABLE.find(
|
|
1991
|
-
(e) => e.analyzer === issue.analyzer && issue.message.includes(e.match)
|
|
1992
|
-
);
|
|
1993
|
-
return entry ? entry.fix(root, detected, placement) : false;
|
|
1994
|
-
}
|
|
1995
|
-
async function addHook(root, event, dedupKeyword, entry, successMsg) {
|
|
1996
|
-
const settings = await readSettingsJson(root);
|
|
1997
|
-
const hooks = settings.hooks ?? {};
|
|
1998
|
-
const hookList = hooks[event] ?? [];
|
|
1999
|
-
const alreadyHas = hookList.some((g) => {
|
|
2000
|
-
const nested = g.hooks;
|
|
2001
|
-
return nested?.some((h) => String(h.command ?? "").includes(dedupKeyword));
|
|
2002
|
-
});
|
|
2003
|
-
if (alreadyHas) return false;
|
|
2004
|
-
hookList.push(entry);
|
|
2005
|
-
settings.hooks = { ...hooks, [event]: hookList };
|
|
2006
|
-
await writeSettingsJson(root, settings);
|
|
2007
|
-
log.success(successMsg);
|
|
2008
|
-
return true;
|
|
2009
|
-
}
|
|
2010
|
-
async function addEnvProtectionHook(root) {
|
|
2011
|
-
return addHook(root, "PreToolUse", ".env", {
|
|
2012
|
-
matcher: "Read|Write|Edit",
|
|
2013
|
-
hooks: [{
|
|
2014
|
-
type: "command",
|
|
2015
|
-
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`
|
|
2016
|
-
}]
|
|
2017
|
-
}, "Added .env file protection hook (PreToolUse)");
|
|
2018
|
-
}
|
|
2019
|
-
async function addAutoFormatHook(root, detected) {
|
|
2020
|
-
if (!detected.language) return false;
|
|
2021
|
-
const formatters = {
|
|
2022
|
-
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
2023
|
-
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
2024
|
-
Python: { extensions: ["py"], command: "ruff format" },
|
|
2025
|
-
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
2026
|
-
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
2027
|
-
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
2028
|
-
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
2029
|
-
};
|
|
2030
|
-
const config = formatters[detected.language];
|
|
2031
|
-
if (!config) return false;
|
|
2032
|
-
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
2033
|
-
return addHook(root, "PostToolUse", "format", {
|
|
2034
|
-
matcher: "Write|Edit",
|
|
2035
|
-
hooks: [{
|
|
2036
|
-
type: "command",
|
|
2037
|
-
command: `ext=\${TOOL_INPUT_FILE_PATH##*.}; (${extChecks}) && ${config.command} "$TOOL_INPUT_FILE_PATH" 2>/dev/null; exit 0`
|
|
2038
|
-
}]
|
|
2039
|
-
}, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
|
|
2040
|
-
}
|
|
2041
|
-
async function addForcePushProtection(root) {
|
|
2042
|
-
return addHook(root, "PreToolUse", "force", {
|
|
2043
|
-
matcher: "Bash",
|
|
2044
|
-
hooks: [{
|
|
2045
|
-
type: "command",
|
|
2046
|
-
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`
|
|
2047
|
-
}]
|
|
2048
|
-
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
2049
|
-
}
|
|
2050
|
-
async function addPostCompactHook(root) {
|
|
2051
|
-
return addHook(root, "PostCompact", "TASKS.md", {
|
|
2052
|
-
matcher: "",
|
|
2053
|
-
hooks: [{
|
|
2054
|
-
type: "command",
|
|
2055
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
2056
|
-
}]
|
|
2057
|
-
}, "Added PostCompact hook (re-injects TASKS.md after compaction)");
|
|
2058
|
-
}
|
|
2059
|
-
async function addSessionStartHook(root) {
|
|
2060
|
-
return addHook(root, "SessionStart", "TASKS.md", {
|
|
2061
|
-
matcher: "startup|resume",
|
|
2062
|
-
hooks: [{
|
|
2063
|
-
type: "command",
|
|
2064
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
2065
|
-
}]
|
|
2066
|
-
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
2067
|
-
}
|
|
2068
|
-
async function migrateAttribution(root) {
|
|
2069
|
-
const settings = await readSettingsJson(root);
|
|
2070
|
-
if (settings.includeCoAuthoredBy === void 0) return false;
|
|
2071
|
-
const { includeCoAuthoredBy: _, ...rest } = settings;
|
|
2072
|
-
const updated = { ...rest, attribution: { commit: "", pr: "" } };
|
|
2073
|
-
await writeSettingsJson(root, updated);
|
|
2074
|
-
log.success("Migrated includeCoAuthoredBy \u2192 attribution object");
|
|
2075
|
-
return true;
|
|
2076
|
-
}
|
|
2077
|
-
async function addCredentialDenyRules(root) {
|
|
2078
|
-
const settings = await readSettingsJson(root);
|
|
2079
|
-
const permissions = settings.permissions ?? {};
|
|
2080
|
-
const deny = permissions.deny ?? [];
|
|
2081
|
-
const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
|
|
2082
|
-
const missing = toAdd.filter((p) => !deny.includes(p));
|
|
2083
|
-
if (missing.length === 0) return false;
|
|
2084
|
-
settings.permissions = { ...permissions, deny: [...deny, ...missing] };
|
|
2085
|
-
await writeSettingsJson(root, settings);
|
|
2086
|
-
log.success("Added credential deny rules (SSH, AWS, npm)");
|
|
2087
|
-
return true;
|
|
2088
|
-
}
|
|
2089
|
-
async function addBypassDisable(root) {
|
|
2090
|
-
const settings = await readSettingsJson(root);
|
|
2091
|
-
if (settings.disableBypassPermissionsMode === "disable") return false;
|
|
2092
|
-
settings.disableBypassPermissionsMode = "disable";
|
|
2093
|
-
await writeSettingsJson(root, settings);
|
|
2094
|
-
log.success("Added disableBypassPermissionsMode: disable");
|
|
2095
|
-
return true;
|
|
2096
|
-
}
|
|
2097
|
-
async function addSandboxSettings(root) {
|
|
2098
|
-
const settings = await readSettingsJson(root);
|
|
2099
|
-
const sandbox = settings.sandbox;
|
|
2100
|
-
if (sandbox?.enabled === true) return false;
|
|
2101
|
-
settings.sandbox = { enabled: true, failIfUnavailable: true };
|
|
2102
|
-
await writeSettingsJson(root, settings);
|
|
2103
|
-
log.success("Enabled sandbox with failIfUnavailable");
|
|
2104
|
-
return true;
|
|
2105
|
-
}
|
|
2106
|
-
async function addEnvToClaudeignore(root) {
|
|
2107
|
-
const ignorePath = join6(root, ".claudeignore");
|
|
2108
|
-
let content;
|
|
2109
|
-
try {
|
|
2110
|
-
content = await readFile5(ignorePath, "utf-8");
|
|
2111
|
-
} catch {
|
|
2112
|
-
return false;
|
|
2113
|
-
}
|
|
2114
|
-
const lines = content.split("\n").map((l) => l.trim());
|
|
2115
|
-
if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
|
|
2116
|
-
await writeFile2(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
|
|
2117
|
-
log.success("Added .env to .claudeignore");
|
|
2118
|
-
return true;
|
|
2119
|
-
}
|
|
2120
|
-
async function addClaudeMdSection(root, heading, content, targetPath) {
|
|
2121
|
-
const claudeMdPath = targetPath ?? join6(root, "CLAUDE.md");
|
|
2122
|
-
let existing;
|
|
2123
|
-
try {
|
|
2124
|
-
existing = await readFile5(claudeMdPath, "utf-8");
|
|
2125
|
-
} catch {
|
|
2126
|
-
if (!targetPath) return false;
|
|
2127
|
-
await mkdir2(join6(root, ".claude"), { recursive: true });
|
|
2128
|
-
existing = "# Local Claude Config\n";
|
|
2129
|
-
}
|
|
2130
|
-
if (existing.includes(heading)) return false;
|
|
2131
|
-
const keyDecisionsIdx = existing.indexOf("## Key Decisions");
|
|
2132
|
-
const insertAt = keyDecisionsIdx > -1 ? keyDecisionsIdx : existing.length;
|
|
2133
|
-
const section = `
|
|
2134
|
-
${heading}
|
|
2135
|
-
${content}
|
|
2136
|
-
|
|
2137
|
-
`;
|
|
2138
|
-
const updated = existing.slice(0, insertAt) + section + existing.slice(insertAt);
|
|
2139
|
-
await writeFile2(claudeMdPath, updated);
|
|
2140
|
-
const label = targetPath ? ".claude/CLAUDE.md" : "CLAUDE.md";
|
|
2141
|
-
log.success(`Added "${heading}" section to ${label}`);
|
|
2142
|
-
return true;
|
|
2143
|
-
}
|
|
2144
|
-
async function createBacklogMd(root) {
|
|
2145
|
-
const backlogPath = join6(root, "BACKLOG.md");
|
|
2146
|
-
try {
|
|
2147
|
-
await access4(backlogPath);
|
|
2148
|
-
return false;
|
|
2149
|
-
} catch {
|
|
2150
|
-
}
|
|
2151
|
-
const name = root.split("/").pop() ?? "Project";
|
|
2152
|
-
await writeFile2(backlogPath, `# ${name} - Backlog
|
|
2153
|
-
|
|
2154
|
-
> Features discussed but deferred. Pick up when relevant.
|
|
2155
|
-
> Priority: P0 = next sprint, P1 = soon, P2 = when relevant.
|
|
2156
|
-
`);
|
|
2157
|
-
log.success("Generated BACKLOG.md");
|
|
2158
|
-
return true;
|
|
2159
|
-
}
|
|
2160
|
-
async function createClaudeignore(root, detected) {
|
|
2161
|
-
const ignorePath = join6(root, ".claudeignore");
|
|
2162
|
-
try {
|
|
2163
|
-
await access4(ignorePath);
|
|
2164
|
-
return false;
|
|
2165
|
-
} catch {
|
|
2166
|
-
}
|
|
2167
|
-
const content = generateClaudeignore(detected);
|
|
2168
|
-
await writeFile2(ignorePath, content);
|
|
2169
|
-
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
2170
|
-
return true;
|
|
2171
|
-
}
|
|
2172
|
-
async function createStarterRules(root) {
|
|
2173
|
-
const rulesDir = join6(root, ".claude", "rules");
|
|
2174
|
-
try {
|
|
2175
|
-
await access4(rulesDir);
|
|
2176
|
-
return false;
|
|
2177
|
-
} catch {
|
|
2178
|
-
}
|
|
2179
|
-
await mkdir2(rulesDir, { recursive: true });
|
|
2180
|
-
await writeFile2(
|
|
2181
|
-
join6(rulesDir, "conventions.md"),
|
|
2182
|
-
`# Project Conventions
|
|
2183
|
-
|
|
2184
|
-
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
2185
|
-
- Keep files under 400 lines, functions under 50 lines
|
|
2186
|
-
- Handle errors explicitly \u2014 no empty catch blocks
|
|
2187
|
-
- Validate input at system boundaries
|
|
2188
|
-
`
|
|
2189
|
-
);
|
|
2190
|
-
log.success("Created .claude/rules/conventions.md with starter rules");
|
|
2191
|
-
return true;
|
|
2192
|
-
}
|
|
2193
|
-
async function createEnhanceSkill(root) {
|
|
2194
|
-
const skillDir = join6(root, ".claude", "skills", "lp-enhance");
|
|
2195
|
-
const skillPath = join6(skillDir, "SKILL.md");
|
|
2196
|
-
const globalPath = join6(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2197
|
-
const legacyProject = join6(root, ".claude", "commands", "lp-enhance.md");
|
|
2198
|
-
const legacyGlobal = join6(homedir3(), ".claude", "commands", "lp-enhance.md");
|
|
2199
|
-
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
|
|
2200
|
-
await mkdir2(skillDir, { recursive: true });
|
|
2201
|
-
await writeFile2(skillPath, generateEnhanceSkill());
|
|
2202
|
-
log.success("Generated /lp-enhance skill (.claude/skills/lp-enhance/)");
|
|
2203
|
-
return true;
|
|
2204
|
-
}
|
|
2205
|
-
async function updateEnhanceSkill(root) {
|
|
2206
|
-
const projectPath = join6(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2207
|
-
const globalPath = join6(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
2208
|
-
const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
|
|
2209
|
-
if (!targetPath) return false;
|
|
2210
|
-
await writeFile2(targetPath, generateEnhanceSkill());
|
|
2211
|
-
log.success("Updated /lp-enhance skill to latest version");
|
|
2212
|
-
return true;
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
1277
|
// src/commands/doctor/watcher.ts
|
|
2216
1278
|
import { readdir as readdir2, stat } from "fs/promises";
|
|
2217
|
-
import { join as
|
|
1279
|
+
import { join as join4 } from "path";
|
|
2218
1280
|
async function watchConfig(projectRoot) {
|
|
2219
1281
|
await runAndDisplay(projectRoot);
|
|
2220
1282
|
log.blank();
|
|
@@ -2237,16 +1299,16 @@ async function watchConfig(projectRoot) {
|
|
|
2237
1299
|
}
|
|
2238
1300
|
async function getFileSnapshot(projectRoot) {
|
|
2239
1301
|
const files = [
|
|
2240
|
-
|
|
2241
|
-
|
|
1302
|
+
join4(projectRoot, "CLAUDE.md"),
|
|
1303
|
+
join4(projectRoot, ".claudeignore")
|
|
2242
1304
|
];
|
|
2243
|
-
const claudeDir =
|
|
1305
|
+
const claudeDir = join4(projectRoot, ".claude");
|
|
2244
1306
|
try {
|
|
2245
1307
|
const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
|
|
2246
1308
|
for (const entry of entries) {
|
|
2247
1309
|
if (entry.isFile()) {
|
|
2248
1310
|
const parentPath = entry.parentPath ?? claudeDir;
|
|
2249
|
-
files.push(
|
|
1311
|
+
files.push(join4(parentPath, entry.name));
|
|
2250
1312
|
}
|
|
2251
1313
|
}
|
|
2252
1314
|
} catch {
|
|
@@ -2390,12 +1452,12 @@ import { Command as Command3 } from "commander";
|
|
|
2390
1452
|
import { select as select2 } from "@inquirer/prompts";
|
|
2391
1453
|
import ora from "ora";
|
|
2392
1454
|
import chalk2 from "chalk";
|
|
2393
|
-
import { mkdir as
|
|
2394
|
-
import { join as
|
|
1455
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1456
|
+
import { join as join7 } from "path";
|
|
2395
1457
|
|
|
2396
1458
|
// src/commands/eval/loader.ts
|
|
2397
|
-
import { readFile as
|
|
2398
|
-
import { join as
|
|
1459
|
+
import { readFile as readFile3, readdir as readdir3, access as access3 } from "fs/promises";
|
|
1460
|
+
import { join as join5, resolve as resolve2, dirname as dirname2 } from "path";
|
|
2399
1461
|
import { fileURLToPath } from "url";
|
|
2400
1462
|
import { parse as parseYaml } from "yaml";
|
|
2401
1463
|
|
|
@@ -2509,7 +1571,7 @@ async function findScenariosDir() {
|
|
|
2509
1571
|
}
|
|
2510
1572
|
async function dirExists(path) {
|
|
2511
1573
|
try {
|
|
2512
|
-
await
|
|
1574
|
+
await access3(path);
|
|
2513
1575
|
return true;
|
|
2514
1576
|
} catch {
|
|
2515
1577
|
return false;
|
|
@@ -2518,14 +1580,14 @@ async function dirExists(path) {
|
|
|
2518
1580
|
async function loadScenarios(options) {
|
|
2519
1581
|
const { suite, customPath } = options;
|
|
2520
1582
|
const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
|
|
2521
|
-
const dirs = suite ? [
|
|
1583
|
+
const dirs = suite ? [join5(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
|
|
2522
1584
|
const allDirs = [scenarioDir, ...dirs];
|
|
2523
1585
|
const scenarios = [];
|
|
2524
1586
|
for (const dir of allDirs) {
|
|
2525
1587
|
const files = await listYamlFiles(dir);
|
|
2526
1588
|
for (const file of files) {
|
|
2527
1589
|
try {
|
|
2528
|
-
const content = await
|
|
1590
|
+
const content = await readFile3(file, "utf-8");
|
|
2529
1591
|
const raw = parseYaml(content);
|
|
2530
1592
|
const scenario = validateScenario(raw, file);
|
|
2531
1593
|
scenarios.push(scenario);
|
|
@@ -2540,7 +1602,7 @@ async function loadScenarios(options) {
|
|
|
2540
1602
|
async function getSubdirectories(dir) {
|
|
2541
1603
|
try {
|
|
2542
1604
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2543
|
-
return entries.filter((e) => e.isDirectory()).map((e) =>
|
|
1605
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join5(dir, e.name));
|
|
2544
1606
|
} catch {
|
|
2545
1607
|
return [];
|
|
2546
1608
|
}
|
|
@@ -2548,22 +1610,22 @@ async function getSubdirectories(dir) {
|
|
|
2548
1610
|
async function listYamlFiles(dir) {
|
|
2549
1611
|
try {
|
|
2550
1612
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2551
|
-
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) =>
|
|
1613
|
+
return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join5(dir, e.name));
|
|
2552
1614
|
} catch {
|
|
2553
1615
|
return [];
|
|
2554
1616
|
}
|
|
2555
1617
|
}
|
|
2556
1618
|
|
|
2557
1619
|
// src/commands/eval/runner.ts
|
|
2558
|
-
import { mkdir as
|
|
2559
|
-
import { join as
|
|
1620
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, readdir as readdir4, rm, cp } from "fs/promises";
|
|
1621
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
2560
1622
|
import { tmpdir } from "os";
|
|
2561
1623
|
import { randomUUID } from "crypto";
|
|
2562
1624
|
import { execFile } from "child_process";
|
|
2563
1625
|
import { promisify } from "util";
|
|
2564
1626
|
var exec = promisify(execFile);
|
|
2565
1627
|
async function runScenario(scenario, options) {
|
|
2566
|
-
const sandboxDir =
|
|
1628
|
+
const sandboxDir = join6(tmpdir(), `claude-eval-${randomUUID()}`);
|
|
2567
1629
|
try {
|
|
2568
1630
|
await setupSandbox(sandboxDir, scenario, options.projectRoot);
|
|
2569
1631
|
await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
|
|
@@ -2587,16 +1649,16 @@ async function runScenarioWithRetries(scenario, options) {
|
|
|
2587
1649
|
return sorted[Math.floor(sorted.length / 2)];
|
|
2588
1650
|
}
|
|
2589
1651
|
async function setupSandbox(sandboxDir, scenario, projectRoot) {
|
|
2590
|
-
await
|
|
1652
|
+
await mkdir2(sandboxDir, { recursive: true });
|
|
2591
1653
|
for (const file of scenario.setup.files) {
|
|
2592
|
-
const filePath =
|
|
2593
|
-
await
|
|
2594
|
-
await
|
|
1654
|
+
const filePath = join6(sandboxDir, file.path);
|
|
1655
|
+
await mkdir2(dirname3(filePath), { recursive: true });
|
|
1656
|
+
await writeFile2(filePath, file.content);
|
|
2595
1657
|
}
|
|
2596
1658
|
await copyProjectConfig(sandboxDir, projectRoot);
|
|
2597
1659
|
if (scenario.setup.instructions) {
|
|
2598
|
-
await
|
|
2599
|
-
|
|
1660
|
+
await writeFile2(
|
|
1661
|
+
join6(sandboxDir, "CLAUDE.md"),
|
|
2600
1662
|
`# Eval Scenario
|
|
2601
1663
|
|
|
2602
1664
|
${scenario.setup.instructions}
|
|
@@ -2617,20 +1679,20 @@ ${scenario.setup.instructions}
|
|
|
2617
1679
|
], { cwd: sandboxDir });
|
|
2618
1680
|
}
|
|
2619
1681
|
async function copyProjectConfig(sandboxDir, projectRoot) {
|
|
2620
|
-
const claudeDir =
|
|
2621
|
-
const sandboxClaudeDir =
|
|
2622
|
-
const settingsPath =
|
|
1682
|
+
const claudeDir = join6(projectRoot, ".claude");
|
|
1683
|
+
const sandboxClaudeDir = join6(sandboxDir, ".claude");
|
|
1684
|
+
const settingsPath = join6(claudeDir, "settings.json");
|
|
2623
1685
|
if (await fileExists(settingsPath)) {
|
|
2624
|
-
await
|
|
2625
|
-
await cp(settingsPath,
|
|
1686
|
+
await mkdir2(sandboxClaudeDir, { recursive: true });
|
|
1687
|
+
await cp(settingsPath, join6(sandboxClaudeDir, "settings.json"));
|
|
2626
1688
|
}
|
|
2627
|
-
const rulesDir =
|
|
1689
|
+
const rulesDir = join6(claudeDir, "rules");
|
|
2628
1690
|
if (await fileExists(rulesDir)) {
|
|
2629
|
-
await cp(rulesDir,
|
|
1691
|
+
await cp(rulesDir, join6(sandboxClaudeDir, "rules"), { recursive: true });
|
|
2630
1692
|
}
|
|
2631
|
-
const ignorePath =
|
|
1693
|
+
const ignorePath = join6(projectRoot, ".claudeignore");
|
|
2632
1694
|
if (await fileExists(ignorePath)) {
|
|
2633
|
-
await cp(ignorePath,
|
|
1695
|
+
await cp(ignorePath, join6(sandboxDir, ".claudeignore"));
|
|
2634
1696
|
}
|
|
2635
1697
|
}
|
|
2636
1698
|
async function runClaudeInSandbox(cwd, prompt, timeout, model) {
|
|
@@ -2724,7 +1786,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
|
|
|
2724
1786
|
async function checkGrep(check, sandboxDir) {
|
|
2725
1787
|
if (!check.pattern) return false;
|
|
2726
1788
|
try {
|
|
2727
|
-
const content = await
|
|
1789
|
+
const content = await readFile4(join6(sandboxDir, check.target), "utf-8");
|
|
2728
1790
|
let found;
|
|
2729
1791
|
try {
|
|
2730
1792
|
found = new RegExp(check.pattern).test(content);
|
|
@@ -2738,7 +1800,7 @@ async function checkGrep(check, sandboxDir) {
|
|
|
2738
1800
|
}
|
|
2739
1801
|
async function checkFilePresence(check, sandboxDir) {
|
|
2740
1802
|
try {
|
|
2741
|
-
await
|
|
1803
|
+
await readFile4(join6(sandboxDir, check.target));
|
|
2742
1804
|
return check.expect === "present";
|
|
2743
1805
|
} catch {
|
|
2744
1806
|
return check.expect === "absent";
|
|
@@ -2747,9 +1809,9 @@ async function checkFilePresence(check, sandboxDir) {
|
|
|
2747
1809
|
async function checkMaxLines(check, sandboxDir) {
|
|
2748
1810
|
const maxLines = parseInt(check.pattern ?? "800", 10);
|
|
2749
1811
|
try {
|
|
2750
|
-
const files = await listAllFiles(
|
|
1812
|
+
const files = await listAllFiles(join6(sandboxDir, check.target));
|
|
2751
1813
|
for (const file of files) {
|
|
2752
|
-
const content = await
|
|
1814
|
+
const content = await readFile4(file, "utf-8");
|
|
2753
1815
|
if (content.split("\n").length > maxLines) {
|
|
2754
1816
|
return check.expect === "absent";
|
|
2755
1817
|
}
|
|
@@ -2764,7 +1826,7 @@ async function listAllFiles(dir) {
|
|
|
2764
1826
|
try {
|
|
2765
1827
|
const entries = await readdir4(dir, { withFileTypes: true });
|
|
2766
1828
|
for (const entry of entries) {
|
|
2767
|
-
const fullPath =
|
|
1829
|
+
const fullPath = join6(dir, entry.name);
|
|
2768
1830
|
if (entry.isDirectory()) {
|
|
2769
1831
|
results.push(...await listAllFiles(fullPath));
|
|
2770
1832
|
} else {
|
|
@@ -2950,10 +2012,10 @@ async function saveEvalReport(results, projectRoot, suite, model) {
|
|
|
2950
2012
|
lines.push("");
|
|
2951
2013
|
}
|
|
2952
2014
|
}
|
|
2953
|
-
const evalDir =
|
|
2954
|
-
await
|
|
2015
|
+
const evalDir = join7(projectRoot, ".claude", "eval");
|
|
2016
|
+
await mkdir3(evalDir, { recursive: true });
|
|
2955
2017
|
const filename = `eval-${suite ?? "all"}-${timestamp}.md`;
|
|
2956
|
-
await
|
|
2018
|
+
await writeFile3(join7(evalDir, filename), lines.join("\n"));
|
|
2957
2019
|
log.success(`Report saved to .claude/eval/${filename}`);
|
|
2958
2020
|
}
|
|
2959
2021
|
async function checkClaudeCli() {
|
|
@@ -2970,12 +2032,19 @@ async function checkClaudeCli() {
|
|
|
2970
2032
|
|
|
2971
2033
|
// src/commands/memory/index.ts
|
|
2972
2034
|
import { readFileSync } from "fs";
|
|
2973
|
-
import { join as
|
|
2035
|
+
import { join as join8 } from "path";
|
|
2974
2036
|
import { Command as Command4 } from "commander";
|
|
2975
2037
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2038
|
+
async function handleSyncErrors(fn) {
|
|
2039
|
+
try {
|
|
2040
|
+
await fn();
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2976
2045
|
function isMemoryInstalled() {
|
|
2977
2046
|
const cwd = process.cwd();
|
|
2978
|
-
return hasMemoryHook(
|
|
2047
|
+
return hasMemoryHook(join8(cwd, ".claude", "settings.json")) || hasMemoryHook(join8(cwd, ".claude", "settings.local.json"));
|
|
2979
2048
|
}
|
|
2980
2049
|
function hasMemoryHook(path) {
|
|
2981
2050
|
try {
|
|
@@ -2992,20 +2061,20 @@ function hasMemoryHook(path) {
|
|
|
2992
2061
|
}
|
|
2993
2062
|
}
|
|
2994
2063
|
function createMemoryCommand() {
|
|
2995
|
-
const memory = new Command4("memory").description("
|
|
2064
|
+
const memory = new Command4("memory").description("Project-scoped memory with decay, sync, and a TUI dashboard").option("--dashboard", "Open the memory dashboard").action(async (opts) => {
|
|
2996
2065
|
if (opts.dashboard) {
|
|
2997
2066
|
if (!isMemoryInstalled()) {
|
|
2998
2067
|
log.error("Knowledge base not set up yet. Run `claude-launchpad memory` first.");
|
|
2999
2068
|
return;
|
|
3000
2069
|
}
|
|
3001
|
-
const { requireMemoryDeps } = await import("./require-deps-
|
|
2070
|
+
const { requireMemoryDeps } = await import("./require-deps-3GIE6TAG.js");
|
|
3002
2071
|
await requireMemoryDeps();
|
|
3003
|
-
const { startTui } = await import("./tui-
|
|
2072
|
+
const { startTui } = await import("./tui-YL5NWME5.js");
|
|
3004
2073
|
await startTui();
|
|
3005
2074
|
return;
|
|
3006
2075
|
}
|
|
3007
2076
|
if (!isMemoryInstalled()) {
|
|
3008
|
-
const { detectExistingSetup } = await import("./install-
|
|
2077
|
+
const { detectExistingSetup } = await import("./install-LS7DTMIE.js");
|
|
3009
2078
|
const existing = detectExistingSetup(process.cwd());
|
|
3010
2079
|
if (existing) {
|
|
3011
2080
|
const location = existing === "local" ? ".claude/CLAUDE.md + settings.local.json" : "CLAUDE.md + settings.json";
|
|
@@ -3031,18 +2100,18 @@ function createMemoryCommand() {
|
|
|
3031
2100
|
log.info("Skipped.");
|
|
3032
2101
|
return;
|
|
3033
2102
|
}
|
|
3034
|
-
const { runInstall } = await import("./install-
|
|
2103
|
+
const { runInstall } = await import("./install-LS7DTMIE.js");
|
|
3035
2104
|
await runInstall({});
|
|
3036
2105
|
} else {
|
|
3037
|
-
const { requireMemoryDeps } = await import("./require-deps-
|
|
2106
|
+
const { requireMemoryDeps } = await import("./require-deps-3GIE6TAG.js");
|
|
3038
2107
|
await requireMemoryDeps();
|
|
3039
|
-
const { runStats } = await import("./stats-
|
|
2108
|
+
const { runStats } = await import("./stats-NQ5NRUZC.js");
|
|
3040
2109
|
await runStats({});
|
|
3041
2110
|
}
|
|
3042
2111
|
});
|
|
3043
2112
|
memory.addCommand(
|
|
3044
2113
|
new Command4("context").description("Load session context (hook handler)").option("--json", "JSON output").action(async (opts) => {
|
|
3045
|
-
const { runContext } = await import("./context-
|
|
2114
|
+
const { runContext } = await import("./context-Q5ZQBY7O.js");
|
|
3046
2115
|
await runContext(opts);
|
|
3047
2116
|
}).helpCommand(false),
|
|
3048
2117
|
{ hidden: true }
|
|
@@ -3056,22 +2125,44 @@ function createMemoryCommand() {
|
|
|
3056
2125
|
);
|
|
3057
2126
|
memory.addCommand(
|
|
3058
2127
|
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) => {
|
|
3059
|
-
|
|
3060
|
-
|
|
2128
|
+
await handleSyncErrors(async () => {
|
|
2129
|
+
const { runPush } = await import("./push-BHYEETGP.js");
|
|
2130
|
+
await runPush(opts);
|
|
2131
|
+
});
|
|
3061
2132
|
})
|
|
3062
2133
|
);
|
|
3063
2134
|
memory.addCommand(
|
|
3064
2135
|
new Command4("pull").description("Pull current project's memories from GitHub Gist").option("--all", "Pull all projects").action(async (opts) => {
|
|
3065
|
-
|
|
3066
|
-
|
|
2136
|
+
await handleSyncErrors(async () => {
|
|
2137
|
+
const { runPull } = await import("./pull-4NRD4GQ4.js");
|
|
2138
|
+
await runPull(opts);
|
|
2139
|
+
});
|
|
2140
|
+
})
|
|
2141
|
+
);
|
|
2142
|
+
const sync = new Command4("sync").description("Manage memory sync");
|
|
2143
|
+
sync.addCommand(
|
|
2144
|
+
new Command4("status").description("Show local vs remote memory counts per project").action(async () => {
|
|
2145
|
+
await handleSyncErrors(async () => {
|
|
2146
|
+
const { runSyncStatus } = await import("./sync-status-ZMXMEBGC.js");
|
|
2147
|
+
await runSyncStatus();
|
|
2148
|
+
});
|
|
2149
|
+
})
|
|
2150
|
+
);
|
|
2151
|
+
sync.addCommand(
|
|
2152
|
+
new Command4("clean").description("Remove a project from the sync gist").argument("<project>", "Project slug to remove").option("-y, --yes", "Skip confirmation prompt").action(async (project, opts) => {
|
|
2153
|
+
await handleSyncErrors(async () => {
|
|
2154
|
+
const { runSyncClean } = await import("./sync-clean-QWEQVAYO.js");
|
|
2155
|
+
await runSyncClean(project, opts);
|
|
2156
|
+
});
|
|
3067
2157
|
})
|
|
3068
2158
|
);
|
|
2159
|
+
memory.addCommand(sync);
|
|
3069
2160
|
return memory;
|
|
3070
2161
|
}
|
|
3071
2162
|
|
|
3072
2163
|
// src/cli.ts
|
|
3073
|
-
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.
|
|
3074
|
-
const hasConfig = await fileExists(
|
|
2164
|
+
var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("1.0.0", "-v, --version").action(async () => {
|
|
2165
|
+
const hasConfig = await fileExists(join9(process.cwd(), "CLAUDE.md")) || await fileExists(join9(process.cwd(), ".claude", "settings.json"));
|
|
3075
2166
|
if (hasConfig) {
|
|
3076
2167
|
await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
|
|
3077
2168
|
} else {
|