claude-launchpad 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MDR73QT2.js → chunk-72VWDNAE.js} +2 -2
- package/dist/{chunk-NJQ2JFGF.js → chunk-DYHPVA6O.js} +2 -2
- package/dist/{chunk-GY3SK66Y.js → chunk-GA3IUQUM.js} +3 -3
- package/dist/{chunk-MIHOPQP4.js → chunk-I4S4Q2IV.js} +2 -2
- package/dist/{chunk-7QPZN27I.js → chunk-RCYLZUU6.js} +367 -197
- package/dist/chunk-RCYLZUU6.js.map +1 -0
- package/dist/cli.js +144 -35
- package/dist/cli.js.map +1 -1
- package/dist/commands/memory/server.js +3 -3
- package/dist/{context-XTPBYGKW.js → context-X7UP2ODK.js} +5 -5
- package/dist/{install-MQDN7TLA.js → install-XBCEI5QK.js} +37 -44
- package/dist/install-XBCEI5QK.js.map +1 -0
- package/dist/{pull-ELZGXQPJ.js → pull-VA62U3OP.js} +7 -7
- package/dist/{push-HJXEC7XC.js → push-C3M6Q4V7.js} +7 -7
- package/dist/{require-deps-3B3NWAEP.js → require-deps-UBU5CYM5.js} +3 -3
- package/dist/{stats-S3GNMUDF.js → stats-R4TWCPHW.js} +6 -6
- package/dist/{sync-clean-XNMLP3FZ.js → sync-clean-P4S7V2JS.js} +3 -3
- package/dist/{sync-status-4AGU776A.js → sync-status-TPYUF43G.js} +7 -7
- package/dist/{tui-CGXGXU5U.js → tui-FFLCUR7E.js} +4 -4
- package/package.json +1 -1
- package/dist/chunk-7QPZN27I.js.map +0 -1
- package/dist/install-MQDN7TLA.js.map +0 -1
- /package/dist/{chunk-MDR73QT2.js.map → chunk-72VWDNAE.js.map} +0 -0
- /package/dist/{chunk-NJQ2JFGF.js.map → chunk-DYHPVA6O.js.map} +0 -0
- /package/dist/{chunk-GY3SK66Y.js.map → chunk-GA3IUQUM.js.map} +0 -0
- /package/dist/{chunk-MIHOPQP4.js.map → chunk-I4S4Q2IV.js.map} +0 -0
- /package/dist/{context-XTPBYGKW.js.map → context-X7UP2ODK.js.map} +0 -0
- /package/dist/{pull-ELZGXQPJ.js.map → pull-VA62U3OP.js.map} +0 -0
- /package/dist/{push-HJXEC7XC.js.map → push-C3M6Q4V7.js.map} +0 -0
- /package/dist/{require-deps-3B3NWAEP.js.map → require-deps-UBU5CYM5.js.map} +0 -0
- /package/dist/{stats-S3GNMUDF.js.map → stats-R4TWCPHW.js.map} +0 -0
- /package/dist/{sync-clean-XNMLP3FZ.js.map → sync-clean-P4S7V2JS.js.map} +0 -0
- /package/dist/{sync-status-4AGU776A.js.map → sync-status-TPYUF43G.js.map} +0 -0
- /package/dist/{tui-CGXGXU5U.js.map → tui-FFLCUR7E.js.map} +0 -0
|
@@ -32,77 +32,15 @@ async function readJsonOrNull(path) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/lib/settings.ts
|
|
35
|
-
import { readFile as
|
|
36
|
-
import { join } from "path";
|
|
37
|
-
async function readSettingsJson(root) {
|
|
38
|
-
const path = join(root, ".claude", "settings.json");
|
|
39
|
-
try {
|
|
40
|
-
const content = await readFile2(path, "utf-8");
|
|
41
|
-
return JSON.parse(content);
|
|
42
|
-
} catch {
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async function writeSettingsJson(root, settings) {
|
|
47
|
-
const dir = join(root, ".claude");
|
|
48
|
-
await mkdir(dir, { recursive: true });
|
|
49
|
-
await writeFile(join(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
50
|
-
}
|
|
51
|
-
async function readSettingsLocalJson(root) {
|
|
52
|
-
const path = join(root, ".claude", "settings.local.json");
|
|
53
|
-
try {
|
|
54
|
-
const content = await readFile2(path, "utf-8");
|
|
55
|
-
return JSON.parse(content);
|
|
56
|
-
} catch {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function writeSettingsLocalJson(root, settings) {
|
|
61
|
-
const dir = join(root, ".claude");
|
|
62
|
-
await mkdir(dir, { recursive: true });
|
|
63
|
-
await writeFile(join(dir, "settings.local.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/lib/memory-placement.ts
|
|
67
|
-
import { select } from "@inquirer/prompts";
|
|
68
|
-
function hasMemoryPermissions(settings) {
|
|
69
|
-
const permissions = settings.permissions;
|
|
70
|
-
const allow = permissions?.allow ?? [];
|
|
71
|
-
return allow.some((p) => p.includes("agentic-memory"));
|
|
72
|
-
}
|
|
73
|
-
async function getMemoryPlacement(root, skipPrompt = false) {
|
|
74
|
-
const local = await readSettingsLocalJson(root);
|
|
75
|
-
const persisted = local.memoryPlacement;
|
|
76
|
-
if (persisted === "shared" || persisted === "local") {
|
|
77
|
-
return persisted;
|
|
78
|
-
}
|
|
79
|
-
if (hasMemoryPermissions(local)) {
|
|
80
|
-
await writeSettingsLocalJson(root, { ...local, memoryPlacement: "local" });
|
|
81
|
-
return "local";
|
|
82
|
-
}
|
|
83
|
-
const shared = await readSettingsJson(root);
|
|
84
|
-
if (hasMemoryPermissions(shared)) {
|
|
85
|
-
await writeSettingsLocalJson(root, { ...local, memoryPlacement: "shared" });
|
|
86
|
-
return "shared";
|
|
87
|
-
}
|
|
88
|
-
if (skipPrompt) return "shared";
|
|
89
|
-
const choice = await select({
|
|
90
|
-
message: "Where should memory config go?",
|
|
91
|
-
choices: [
|
|
92
|
-
{ value: "shared", name: "Shared (team sees it) \u2014 CLAUDE.md + settings.json" },
|
|
93
|
-
{ value: "local", name: "Local (only you) \u2014 .claude/CLAUDE.md + settings.local.json" }
|
|
94
|
-
]
|
|
95
|
-
});
|
|
96
|
-
await writeSettingsLocalJson(root, { ...local, memoryPlacement: choice });
|
|
97
|
-
return choice;
|
|
98
|
-
}
|
|
35
|
+
import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
36
|
+
import { join as join6 } from "path";
|
|
99
37
|
|
|
100
38
|
// src/lib/output.ts
|
|
101
39
|
import chalk from "chalk";
|
|
102
40
|
|
|
103
41
|
// src/commands/doctor/fixer.ts
|
|
104
|
-
import { readFile as
|
|
105
|
-
import { join as
|
|
42
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2, access as access2 } from "fs/promises";
|
|
43
|
+
import { join as join5 } from "path";
|
|
106
44
|
import { homedir } from "os";
|
|
107
45
|
|
|
108
46
|
// src/lib/sections.ts
|
|
@@ -113,22 +51,22 @@ var OFF_LIMITS_CONTENT = "- Never hardcode secrets \u2014 use environment variab
|
|
|
113
51
|
var SKILL_AUTHORING_CONTENT = 'When creating Claude Code skills (.claude/skills/*/SKILL.md):\n\n- Keep SKILL.md under 500 lines \u2014 move reference material to supporting files in the same directory\n- Front-load description (first 250 chars shown in listings) with TRIGGER when / DO NOT TRIGGER when clauses\n- Add allowed-tools in frontmatter to restrict tool access (e.g. Read, Glob, Grep for read-only skills)\n- Add argument-hint in frontmatter showing the expected input format (use $ARGUMENTS or $0, $1 for dynamic input)\n- Set disable-model-invocation: true for skills with side effects (deploy, send messages)\n- Structure as phases: Research, Plan, Execute, Verify with "Done when:" success criteria per phase\n- Handle edge cases and preconditions before execution';
|
|
114
52
|
|
|
115
53
|
// src/lib/detect.ts
|
|
116
|
-
import { join
|
|
54
|
+
import { join, basename } from "path";
|
|
117
55
|
async function detectProject(root) {
|
|
118
56
|
const name = basename(root);
|
|
119
57
|
const [pkgJson, goMod, pyProject, gemfile, cargo, pubspec, composerJson, pomXml, buildGradleGroovy, buildGradleKts, packageSwift, mixExs, csproj, lockfiles] = await Promise.all([
|
|
120
|
-
readJsonOrNull(
|
|
121
|
-
fileExists(
|
|
122
|
-
readFileOrNull(
|
|
123
|
-
fileExists(
|
|
124
|
-
fileExists(
|
|
125
|
-
fileExists(
|
|
126
|
-
readJsonOrNull(
|
|
127
|
-
fileExists(
|
|
128
|
-
fileExists(
|
|
129
|
-
fileExists(
|
|
130
|
-
fileExists(
|
|
131
|
-
fileExists(
|
|
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")),
|
|
132
70
|
globExists(root, "*.csproj"),
|
|
133
71
|
detectLockfiles(root)
|
|
134
72
|
]);
|
|
@@ -209,10 +147,10 @@ function detectFramework(m) {
|
|
|
209
147
|
}
|
|
210
148
|
async function detectLockfiles(root) {
|
|
211
149
|
const [pnpmLock, yarnLock, bunLock, npmLock] = await Promise.all([
|
|
212
|
-
fileExists(
|
|
213
|
-
fileExists(
|
|
214
|
-
fileExists(
|
|
215
|
-
fileExists(
|
|
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"))
|
|
216
154
|
]);
|
|
217
155
|
return { pnpmLock, yarnLock, bunLock, npmLock };
|
|
218
156
|
}
|
|
@@ -619,6 +557,40 @@ function generateEnhanceSkill() {
|
|
|
619
557
|
].join("\n");
|
|
620
558
|
}
|
|
621
559
|
|
|
560
|
+
// src/lib/memory-placement.ts
|
|
561
|
+
import { select } from "@inquirer/prompts";
|
|
562
|
+
function hasMemoryPermissions(settings) {
|
|
563
|
+
const permissions = settings.permissions;
|
|
564
|
+
const allow = permissions?.allow ?? [];
|
|
565
|
+
return allow.some((p) => p.includes("agentic-memory"));
|
|
566
|
+
}
|
|
567
|
+
async function getMemoryPlacement(root, skipPrompt = false) {
|
|
568
|
+
const local = await readSettingsLocalJson(root) ?? {};
|
|
569
|
+
const persisted = local.memoryPlacement;
|
|
570
|
+
if (persisted === "shared" || persisted === "local") {
|
|
571
|
+
return persisted;
|
|
572
|
+
}
|
|
573
|
+
if (hasMemoryPermissions(local)) {
|
|
574
|
+
await writeSettingsLocalJson(root, { ...local, memoryPlacement: "local" });
|
|
575
|
+
return "local";
|
|
576
|
+
}
|
|
577
|
+
const shared = await readSettingsJson(root) ?? {};
|
|
578
|
+
if (hasMemoryPermissions(shared)) {
|
|
579
|
+
await writeSettingsLocalJson(root, { ...local, memoryPlacement: "shared" });
|
|
580
|
+
return "shared";
|
|
581
|
+
}
|
|
582
|
+
if (skipPrompt) return "shared";
|
|
583
|
+
const choice = await select({
|
|
584
|
+
message: "Where should memory config go?",
|
|
585
|
+
choices: [
|
|
586
|
+
{ value: "shared", name: "Shared (team sees it) \u2014 CLAUDE.md + settings.json" },
|
|
587
|
+
{ value: "local", name: "Local (only you) \u2014 .claude/CLAUDE.md + settings.local.json" }
|
|
588
|
+
]
|
|
589
|
+
});
|
|
590
|
+
await writeSettingsLocalJson(root, { ...local, memoryPlacement: choice });
|
|
591
|
+
return choice;
|
|
592
|
+
}
|
|
593
|
+
|
|
622
594
|
// src/lib/stub-marker.ts
|
|
623
595
|
var LP_STUB_OPEN = "<!-- LP-STUB: ai-recommended -->";
|
|
624
596
|
var LP_STUB_CLOSE = "<!-- /LP-STUB -->";
|
|
@@ -628,23 +600,236 @@ ${content}
|
|
|
628
600
|
${LP_STUB_CLOSE}`;
|
|
629
601
|
}
|
|
630
602
|
|
|
631
|
-
// src/commands/doctor/fixer-
|
|
632
|
-
import {
|
|
603
|
+
// src/commands/doctor/fixer-sprint.ts
|
|
604
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
633
605
|
import { join as join3 } from "path";
|
|
606
|
+
|
|
607
|
+
// src/lib/hook-builder.ts
|
|
608
|
+
function addOrUpdateHook(existingHooks, options) {
|
|
609
|
+
const hookList = existingHooks?.[options.event] ?? [];
|
|
610
|
+
const alreadyHas = hookList.some((group) => {
|
|
611
|
+
const nested = group.hooks;
|
|
612
|
+
return nested?.some((h) => String(h.command ?? "").includes(options.dedupKeyword));
|
|
613
|
+
});
|
|
614
|
+
if (alreadyHas) {
|
|
615
|
+
return { hooks: existingHooks ?? {}, added: false };
|
|
616
|
+
}
|
|
617
|
+
const newEntry = options.entry;
|
|
618
|
+
const updated = options.prepend ? [newEntry, ...hookList] : [...hookList, newEntry];
|
|
619
|
+
return {
|
|
620
|
+
hooks: { ...existingHooks ?? {}, [options.event]: updated },
|
|
621
|
+
added: true
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function addHookToSettings(root, event, dedupKeyword, entry, successMsg) {
|
|
625
|
+
const settings = await readSettingsJson(root);
|
|
626
|
+
if (settings === null) return false;
|
|
627
|
+
const existingHooks = settings.hooks;
|
|
628
|
+
const result = addOrUpdateHook(existingHooks, { event, dedupKeyword, entry });
|
|
629
|
+
if (!result.added) return false;
|
|
630
|
+
await writeSettingsJson(root, { ...settings, hooks: result.hooks });
|
|
631
|
+
log.success(successMsg);
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/lib/hook-scripts.ts
|
|
636
|
+
import { writeFile, mkdir, chmod } from "fs/promises";
|
|
637
|
+
import { join as join2 } from "path";
|
|
638
|
+
var SPRINT_SIZE_CHECK = `#!/usr/bin/env bash
|
|
639
|
+
# Warns when the current sprint is too small (<3) or too large (>7).
|
|
640
|
+
# Sweet spot is 3-6 work packages per sprint. Non-blocking (always exits 0).
|
|
641
|
+
|
|
642
|
+
set -u
|
|
643
|
+
tasks="\${1:-TASKS.md}"
|
|
644
|
+
[ -f "$tasks" ] || exit 0
|
|
645
|
+
|
|
646
|
+
section=$(sed -n '/^## Current/,/^## /p' "$tasks" 2>/dev/null)
|
|
647
|
+
[ -z "$section" ] && exit 0
|
|
648
|
+
|
|
649
|
+
unchecked=$(echo "$section" | grep -cF -e '- [ ]' || true)
|
|
650
|
+
checked=$(echo "$section" | grep -cF -e '- [x]' || true)
|
|
651
|
+
total=$((unchecked + checked))
|
|
652
|
+
|
|
653
|
+
if [ "$total" -eq 0 ]; then
|
|
654
|
+
echo "NOTE: Current sprint has no work packages yet. Pull 3-6 from BACKLOG.md to start."
|
|
655
|
+
exit 0
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
if [ "$unchecked" -eq 0 ]; then exit 0; fi
|
|
659
|
+
|
|
660
|
+
if [ "$unchecked" -lt 3 ]; then
|
|
661
|
+
echo "NOTE: Current sprint has $unchecked open work package(s) \u2014 that's a microsprint. Pull from BACKLOG.md (aim 3-6)."
|
|
662
|
+
exit 0
|
|
663
|
+
fi
|
|
664
|
+
|
|
665
|
+
if [ "$unchecked" -gt 7 ]; then
|
|
666
|
+
echo "NOTE: Current sprint has $unchecked open work packages \u2014 oversized. Move some back to BACKLOG.md (aim 3-6)."
|
|
667
|
+
exit 0
|
|
668
|
+
fi
|
|
669
|
+
|
|
670
|
+
exit 0
|
|
671
|
+
`;
|
|
672
|
+
var SPRINT_OPEN_CHECK = `#!/usr/bin/env bash
|
|
673
|
+
# Warns when TASKS.md opens a new sprint block but BACKLOG.md has no staged
|
|
674
|
+
# deletions, i.e. the "remove pulled WPs from BACKLOG in the same edit" rule
|
|
675
|
+
# from CLAUDE.md was skipped. Non-blocking (always exits 0).
|
|
676
|
+
|
|
677
|
+
set -u
|
|
678
|
+
cmd="\${TOOL_INPUT_COMMAND:-}"
|
|
679
|
+
|
|
680
|
+
# Only act on \`git commit\`, word-boundary match.
|
|
681
|
+
echo "$cmd" | grep -qE '(^|[^a-zA-Z0-9_-])git[[:space:]]+commit([[:space:]]|$)' || exit 0
|
|
682
|
+
|
|
683
|
+
# Nothing staged
|
|
684
|
+
git diff --cached --quiet 2>/dev/null && exit 0
|
|
685
|
+
|
|
686
|
+
# TASKS.md not staged
|
|
687
|
+
git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -q '^TASKS\\.md$' || exit 0
|
|
688
|
+
|
|
689
|
+
# Does the staged TASKS.md diff ADD a new \`## Current\` block?
|
|
690
|
+
new_sprint=$(git diff --cached TASKS.md 2>/dev/null | grep -cE '^\\+## Current')
|
|
691
|
+
[ "$new_sprint" -eq 0 ] && exit 0
|
|
692
|
+
|
|
693
|
+
# If a new sprint was opened, BACKLOG.md should have net deletions.
|
|
694
|
+
backlog_deletions=$(git diff --cached BACKLOG.md 2>/dev/null | grep -cE '^-[^-]')
|
|
695
|
+
if [ "$backlog_deletions" -eq 0 ]; then
|
|
696
|
+
echo ""
|
|
697
|
+
echo "WARNING: sprint-open hygiene"
|
|
698
|
+
echo ""
|
|
699
|
+
echo "TASKS.md stages a new '## Current' block, but BACKLOG.md has no"
|
|
700
|
+
echo "staged deletions. When a WP is pulled from BACKLOG.md into a sprint,"
|
|
701
|
+
echo "remove it from BACKLOG.md in the same edit. Overlap = drift."
|
|
702
|
+
echo ""
|
|
703
|
+
echo "If you opened a fresh-scope sprint with no BACKLOG pulls, ignore"
|
|
704
|
+
echo "this. Otherwise scrub BACKLOG.md before committing."
|
|
705
|
+
echo ""
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
exit 0
|
|
709
|
+
`;
|
|
710
|
+
async function writeSprintHygieneScripts(root) {
|
|
711
|
+
const hooksDir = join2(root, ".claude", "hooks");
|
|
712
|
+
await mkdir(hooksDir, { recursive: true });
|
|
713
|
+
const sizePath = join2(hooksDir, "sprint-size-check.sh");
|
|
714
|
+
const openPath = join2(hooksDir, "sprint-open-check.sh");
|
|
715
|
+
await writeFile(sizePath, SPRINT_SIZE_CHECK);
|
|
716
|
+
await writeFile(openPath, SPRINT_OPEN_CHECK);
|
|
717
|
+
await chmod(sizePath, 493);
|
|
718
|
+
await chmod(openPath, 493);
|
|
719
|
+
return { sizePath, openPath };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/commands/doctor/fixer-sprint.ts
|
|
723
|
+
var WORKTREE_INCLUDE_TEMPLATE = `# Files copied into git worktrees that Claude Code creates for subagents.
|
|
724
|
+
# Listed files must be gitignored \u2014 that's the point: keep secrets out of
|
|
725
|
+
# commits while letting worktree subagents inherit local env so dev servers,
|
|
726
|
+
# tests, and integration runs work the same as the main tree.
|
|
727
|
+
# Anything needed by \`pnpm dev\`, \`pnpm test\`, etc. that's NOT committed
|
|
728
|
+
# should land here.
|
|
729
|
+
|
|
730
|
+
.env.local
|
|
731
|
+
.env
|
|
732
|
+
`;
|
|
733
|
+
async function createWorktreeInclude(root) {
|
|
734
|
+
await writeFile2(join3(root, ".worktreeinclude"), WORKTREE_INCLUDE_TEMPLATE);
|
|
735
|
+
log.success("Generated .worktreeinclude (worktree subagents inherit .env.local / .env)");
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
async function addSprintSizeHook(root) {
|
|
739
|
+
await writeSprintHygieneScripts(root);
|
|
740
|
+
return addHookToSettings(root, "SessionStart", "sprint-size-check.sh", {
|
|
741
|
+
matcher: "startup|resume",
|
|
742
|
+
hooks: [{ type: "command", command: "bash .claude/hooks/sprint-size-check.sh TASKS.md 2>/dev/null; exit 0" }]
|
|
743
|
+
}, "Added sprint-size-check hook (warns on microsprint/oversized sprints)");
|
|
744
|
+
}
|
|
745
|
+
async function addSprintOpenHook(root) {
|
|
746
|
+
await writeSprintHygieneScripts(root);
|
|
747
|
+
return addHookToSettings(root, "PreToolUse", "sprint-open-check.sh", {
|
|
748
|
+
matcher: "Bash",
|
|
749
|
+
hooks: [{ type: "command", command: "bash .claude/hooks/sprint-open-check.sh 2>/dev/null; exit 0" }]
|
|
750
|
+
}, "Added sprint-open-check hook (warns on new sprint without BACKLOG cleanup)");
|
|
751
|
+
}
|
|
752
|
+
async function addSprintCompleteNudge(root) {
|
|
753
|
+
return addHookToSettings(root, "PostToolUse", "Sprint complete", {
|
|
754
|
+
matcher: "Edit|Write",
|
|
755
|
+
hooks: [{
|
|
756
|
+
type: "command",
|
|
757
|
+
command: `echo "$TOOL_INPUT_FILE_PATH" | grep -q TASKS.md || exit 0; section=$(sed -n '/^## Current/,/^## /p' TASKS.md 2>/dev/null); [ -z "$section" ] && exit 0; unchecked=$(echo "$section" | grep -cF '- [ ]' || true); checked=$(echo "$section" | grep -cF '- [x]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && echo 'Sprint complete \u2014 all current tasks done. Consider a quick quality check before committing: scan for dead code, debug artifacts, TODO hacks, and convention violations. Run tests if available. Skip if trivial.'; exit 0`
|
|
758
|
+
}]
|
|
759
|
+
}, "Added sprint-complete nudge hook");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/commands/doctor/fixer-hooks.ts
|
|
763
|
+
var FORMATTERS = {
|
|
764
|
+
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
765
|
+
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
766
|
+
Python: { extensions: ["py"], command: "ruff format" },
|
|
767
|
+
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
768
|
+
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
769
|
+
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
770
|
+
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
771
|
+
};
|
|
772
|
+
async function addEnvProtectionHook(root) {
|
|
773
|
+
return addHookToSettings(root, "PreToolUse", ".env", {
|
|
774
|
+
matcher: "Read|Write|Edit",
|
|
775
|
+
hooks: [{
|
|
776
|
+
type: "command",
|
|
777
|
+
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`
|
|
778
|
+
}]
|
|
779
|
+
}, "Added .env file protection hook (PreToolUse)");
|
|
780
|
+
}
|
|
781
|
+
async function addAutoFormatHook(root, detected) {
|
|
782
|
+
if (!detected.language) return false;
|
|
783
|
+
const config = detected.language ? FORMATTERS[detected.language] : null;
|
|
784
|
+
if (!config) return false;
|
|
785
|
+
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
786
|
+
return addHookToSettings(root, "PostToolUse", "format", {
|
|
787
|
+
matcher: "Write|Edit",
|
|
788
|
+
hooks: [{
|
|
789
|
+
type: "command",
|
|
790
|
+
command: `ext=\${TOOL_INPUT_FILE_PATH##*.}; (${extChecks}) && ${config.command} "$TOOL_INPUT_FILE_PATH" 2>/dev/null; exit 0`
|
|
791
|
+
}]
|
|
792
|
+
}, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
|
|
793
|
+
}
|
|
794
|
+
async function addForcePushProtection(root) {
|
|
795
|
+
return addHookToSettings(root, "PreToolUse", "force", {
|
|
796
|
+
matcher: "Bash",
|
|
797
|
+
hooks: [{
|
|
798
|
+
type: "command",
|
|
799
|
+
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`
|
|
800
|
+
}]
|
|
801
|
+
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
802
|
+
}
|
|
803
|
+
async function addPostCompactHook(root) {
|
|
804
|
+
return addHookToSettings(root, "PostCompact", "TASKS.md", {
|
|
805
|
+
matcher: "",
|
|
806
|
+
hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
|
|
807
|
+
}, "Added PostCompact hook (re-injects TASKS.md after compaction)");
|
|
808
|
+
}
|
|
809
|
+
async function addSessionStartHook(root) {
|
|
810
|
+
return addHookToSettings(root, "SessionStart", "TASKS.md", {
|
|
811
|
+
matcher: "startup|resume",
|
|
812
|
+
hooks: [{ type: "command", command: "cat TASKS.md 2>/dev/null; exit 0" }]
|
|
813
|
+
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/commands/doctor/fixer-memory.ts
|
|
817
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
818
|
+
import { join as join4 } from "path";
|
|
634
819
|
async function addPlacementHook(root, placement, event, dedupKeyword, entry, prepend, successMsg) {
|
|
635
820
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
636
821
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
637
822
|
const settings = await read(root);
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
823
|
+
if (settings === null) return false;
|
|
824
|
+
const existingHooks = settings.hooks;
|
|
825
|
+
const result = addOrUpdateHook(existingHooks, {
|
|
826
|
+
event,
|
|
827
|
+
dedupKeyword,
|
|
828
|
+
entry,
|
|
829
|
+
prepend
|
|
643
830
|
});
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
const updatedSettings = { ...settings, hooks: { ...hooks, [event]: updatedList } };
|
|
647
|
-
await write(root, updatedSettings);
|
|
831
|
+
if (!result.added) return false;
|
|
832
|
+
await write(root, { ...settings, hooks: result.hooks });
|
|
648
833
|
log.success(successMsg);
|
|
649
834
|
return true;
|
|
650
835
|
}
|
|
@@ -652,6 +837,7 @@ async function disableAutoMemory(root, placement) {
|
|
|
652
837
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
653
838
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
654
839
|
const settings = await read(root);
|
|
840
|
+
if (settings === null) return false;
|
|
655
841
|
if (settings.autoMemoryEnabled === false) return false;
|
|
656
842
|
const updated = { ...settings, autoMemoryEnabled: false };
|
|
657
843
|
await write(root, updated);
|
|
@@ -663,6 +849,7 @@ async function addMemoryToolPermissions(root, placement) {
|
|
|
663
849
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
664
850
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
665
851
|
const settings = await read(root);
|
|
852
|
+
if (settings === null) return false;
|
|
666
853
|
const permissions = settings.permissions ?? {};
|
|
667
854
|
const allow = permissions.allow ?? [];
|
|
668
855
|
const tools = [
|
|
@@ -701,6 +888,7 @@ async function upgradeStaleSessionEndPushHook(root) {
|
|
|
701
888
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
702
889
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
703
890
|
const settings = await read(root);
|
|
891
|
+
if (settings === null) continue;
|
|
704
892
|
const hooks = settings.hooks;
|
|
705
893
|
const sessionEnd = hooks?.SessionEnd;
|
|
706
894
|
if (!sessionEnd) continue;
|
|
@@ -727,6 +915,7 @@ async function upgradeStaleSessionEndPushHook(root) {
|
|
|
727
915
|
}
|
|
728
916
|
async function removeStaleStopHook(root) {
|
|
729
917
|
const settings = await readSettingsJson(root);
|
|
918
|
+
if (settings === null) return false;
|
|
730
919
|
const hooks = settings.hooks;
|
|
731
920
|
if (!hooks?.Stop) return false;
|
|
732
921
|
const stopHooks = hooks.Stop;
|
|
@@ -749,6 +938,7 @@ async function addMemoryToAllowedMcpServers(root) {
|
|
|
749
938
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
750
939
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
751
940
|
const settings = await read(root);
|
|
941
|
+
if (settings === null) continue;
|
|
752
942
|
const existing = settings.allowedMcpServers;
|
|
753
943
|
if (!Array.isArray(existing)) continue;
|
|
754
944
|
const list = existing;
|
|
@@ -769,17 +959,19 @@ async function addAllowedMcpServers(root, placement) {
|
|
|
769
959
|
const read = placement === "local" ? readSettingsLocalJson : readSettingsJson;
|
|
770
960
|
const write = placement === "local" ? writeSettingsLocalJson : writeSettingsJson;
|
|
771
961
|
const settings = await read(root);
|
|
962
|
+
if (settings === null) return false;
|
|
772
963
|
if (settings.allowedMcpServers) return false;
|
|
773
964
|
const other = placement === "local" ? await readSettingsJson(root) : await readSettingsLocalJson(root);
|
|
965
|
+
if (other === null) return false;
|
|
774
966
|
if (other.allowedMcpServers) return false;
|
|
775
967
|
const serverNames = /* @__PURE__ */ new Set();
|
|
776
968
|
const settingsServers = settings.mcpServers;
|
|
777
969
|
if (settingsServers && typeof settingsServers === "object") {
|
|
778
970
|
for (const name of Object.keys(settingsServers)) serverNames.add(name);
|
|
779
971
|
}
|
|
780
|
-
const mcpJsonPath =
|
|
972
|
+
const mcpJsonPath = join4(root, ".mcp.json");
|
|
781
973
|
try {
|
|
782
|
-
const mcpJson = JSON.parse(await
|
|
974
|
+
const mcpJson = JSON.parse(await readFile2(mcpJsonPath, "utf-8"));
|
|
783
975
|
const mcpServers = mcpJson.mcpServers;
|
|
784
976
|
if (mcpServers && typeof mcpServers === "object") {
|
|
785
977
|
for (const name of Object.keys(mcpServers)) serverNames.add(name);
|
|
@@ -849,6 +1041,10 @@ var FIX_TABLE = [
|
|
|
849
1041
|
{ analyzer: "Permissions", match: "Bypass permissions mode", fix: (root) => addBypassDisable(root) },
|
|
850
1042
|
{ analyzer: "Permissions", match: "Filesystem sandbox enabled", fix: (root) => removeSandboxSettings(root) },
|
|
851
1043
|
{ analyzer: "Permissions", match: ".env is protected by hooks but not in .claudeignore", fix: (root) => addEnvToClaudeignore(root) },
|
|
1044
|
+
{ analyzer: "Permissions", match: ".worktreeinclude is missing or empty", fix: (root) => createWorktreeInclude(root) },
|
|
1045
|
+
{ analyzer: "Hooks", match: "sprint-size-check", fix: (root) => addSprintSizeHook(root) },
|
|
1046
|
+
{ analyzer: "Hooks", match: "sprint-open-check", fix: (root) => addSprintOpenHook(root) },
|
|
1047
|
+
{ analyzer: "Hooks", match: "sprint-complete nudge", fix: (root) => addSprintCompleteNudge(root) },
|
|
852
1048
|
{ analyzer: "Rules", match: "No skill authoring conventions", fix: (root) => addSkillAuthoringConventions(root) },
|
|
853
1049
|
{ analyzer: "Rules", match: "No /lp-enhance skill", fix: (root) => createEnhanceSkill(root) },
|
|
854
1050
|
{ analyzer: "Rules", match: "lp-enhance skill is outdated", fix: (root) => updateEnhanceSkill(root) },
|
|
@@ -864,7 +1060,7 @@ var FIX_TABLE = [
|
|
|
864
1060
|
{ analyzer: "Memory", match: "SessionEnd push hook is not nohup-wrapped", fix: (root) => upgradeStaleSessionEndPushHook(root) },
|
|
865
1061
|
{ analyzer: "Memory", match: "CLAUDE.md missing memory guidance", fix: (root, _det, placement) => {
|
|
866
1062
|
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";
|
|
867
|
-
const target = placement === "local" ?
|
|
1063
|
+
const target = placement === "local" ? join5(root, ".claude", "CLAUDE.md") : void 0;
|
|
868
1064
|
return addClaudeMdSection(root, "## Memory", wrapStub(content), target);
|
|
869
1065
|
} }
|
|
870
1066
|
];
|
|
@@ -879,81 +1075,9 @@ async function tryFix(issue, root, detected, placement) {
|
|
|
879
1075
|
);
|
|
880
1076
|
return entry ? entry.fix(root, detected, placement) : false;
|
|
881
1077
|
}
|
|
882
|
-
async function addHook(root, event, dedupKeyword, entry, successMsg) {
|
|
883
|
-
const settings = await readSettingsJson(root);
|
|
884
|
-
const hooks = settings.hooks ?? {};
|
|
885
|
-
const hookList = hooks[event] ?? [];
|
|
886
|
-
const alreadyHas = hookList.some((g) => {
|
|
887
|
-
const nested = g.hooks;
|
|
888
|
-
return nested?.some((h) => String(h.command ?? "").includes(dedupKeyword));
|
|
889
|
-
});
|
|
890
|
-
if (alreadyHas) return false;
|
|
891
|
-
const updated = [...hookList, entry];
|
|
892
|
-
const updatedSettings = { ...settings, hooks: { ...hooks, [event]: updated } };
|
|
893
|
-
await writeSettingsJson(root, updatedSettings);
|
|
894
|
-
log.success(successMsg);
|
|
895
|
-
return true;
|
|
896
|
-
}
|
|
897
|
-
async function addEnvProtectionHook(root) {
|
|
898
|
-
return addHook(root, "PreToolUse", ".env", {
|
|
899
|
-
matcher: "Read|Write|Edit",
|
|
900
|
-
hooks: [{
|
|
901
|
-
type: "command",
|
|
902
|
-
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`
|
|
903
|
-
}]
|
|
904
|
-
}, "Added .env file protection hook (PreToolUse)");
|
|
905
|
-
}
|
|
906
|
-
async function addAutoFormatHook(root, detected) {
|
|
907
|
-
if (!detected.language) return false;
|
|
908
|
-
const formatters = {
|
|
909
|
-
TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
|
|
910
|
-
JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
|
|
911
|
-
Python: { extensions: ["py"], command: "ruff format" },
|
|
912
|
-
Go: { extensions: ["go"], command: "gofmt -w" },
|
|
913
|
-
Rust: { extensions: ["rs"], command: "rustfmt" },
|
|
914
|
-
Ruby: { extensions: ["rb"], command: "rubocop -A" },
|
|
915
|
-
PHP: { extensions: ["php"], command: "vendor/bin/pint" }
|
|
916
|
-
};
|
|
917
|
-
const config = formatters[detected.language];
|
|
918
|
-
if (!config) return false;
|
|
919
|
-
const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
|
|
920
|
-
return addHook(root, "PostToolUse", "format", {
|
|
921
|
-
matcher: "Write|Edit",
|
|
922
|
-
hooks: [{
|
|
923
|
-
type: "command",
|
|
924
|
-
command: `ext=\${TOOL_INPUT_FILE_PATH##*.}; (${extChecks}) && ${config.command} "$TOOL_INPUT_FILE_PATH" 2>/dev/null; exit 0`
|
|
925
|
-
}]
|
|
926
|
-
}, `Added auto-format hook (PostToolUse \u2192 ${config.command})`);
|
|
927
|
-
}
|
|
928
|
-
async function addForcePushProtection(root) {
|
|
929
|
-
return addHook(root, "PreToolUse", "force", {
|
|
930
|
-
matcher: "Bash",
|
|
931
|
-
hooks: [{
|
|
932
|
-
type: "command",
|
|
933
|
-
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`
|
|
934
|
-
}]
|
|
935
|
-
}, "Added force-push protection hook (PreToolUse \u2192 Bash)");
|
|
936
|
-
}
|
|
937
|
-
async function addPostCompactHook(root) {
|
|
938
|
-
return addHook(root, "PostCompact", "TASKS.md", {
|
|
939
|
-
matcher: "",
|
|
940
|
-
hooks: [{
|
|
941
|
-
type: "command",
|
|
942
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
943
|
-
}]
|
|
944
|
-
}, "Added PostCompact hook (re-injects TASKS.md after compaction)");
|
|
945
|
-
}
|
|
946
|
-
async function addSessionStartHook(root) {
|
|
947
|
-
return addHook(root, "SessionStart", "TASKS.md", {
|
|
948
|
-
matcher: "startup|resume",
|
|
949
|
-
hooks: [{
|
|
950
|
-
type: "command",
|
|
951
|
-
command: "cat TASKS.md 2>/dev/null; exit 0"
|
|
952
|
-
}]
|
|
953
|
-
}, "Added SessionStart hook (injects TASKS.md at startup)");
|
|
954
|
-
}
|
|
955
1078
|
async function migrateAttribution(root) {
|
|
956
1079
|
const settings = await readSettingsJson(root);
|
|
1080
|
+
if (settings === null) return false;
|
|
957
1081
|
if (settings.includeCoAuthoredBy === void 0) return false;
|
|
958
1082
|
const { includeCoAuthoredBy: _, ...rest } = settings;
|
|
959
1083
|
const updated = { ...rest, attribution: { commit: "", pr: "" } };
|
|
@@ -963,6 +1087,7 @@ async function migrateAttribution(root) {
|
|
|
963
1087
|
}
|
|
964
1088
|
async function addCredentialDenyRules(root) {
|
|
965
1089
|
const settings = await readSettingsJson(root);
|
|
1090
|
+
if (settings === null) return false;
|
|
966
1091
|
const permissions = settings.permissions ?? {};
|
|
967
1092
|
const deny = permissions.deny ?? [];
|
|
968
1093
|
const toAdd = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
|
|
@@ -975,6 +1100,7 @@ async function addCredentialDenyRules(root) {
|
|
|
975
1100
|
}
|
|
976
1101
|
async function addBypassDisable(root) {
|
|
977
1102
|
const settings = await readSettingsJson(root);
|
|
1103
|
+
if (settings === null) return false;
|
|
978
1104
|
if (settings.disableBypassPermissionsMode === "disable") return false;
|
|
979
1105
|
const updated = { ...settings, disableBypassPermissionsMode: "disable" };
|
|
980
1106
|
await writeSettingsJson(root, updated);
|
|
@@ -983,6 +1109,7 @@ async function addBypassDisable(root) {
|
|
|
983
1109
|
}
|
|
984
1110
|
async function removeSandboxSettings(root) {
|
|
985
1111
|
const settings = await readSettingsJson(root);
|
|
1112
|
+
if (settings === null) return false;
|
|
986
1113
|
if (settings.sandbox === void 0) return false;
|
|
987
1114
|
const { sandbox: _sandbox, ...rest } = settings;
|
|
988
1115
|
await writeSettingsJson(root, rest);
|
|
@@ -990,27 +1117,27 @@ async function removeSandboxSettings(root) {
|
|
|
990
1117
|
return true;
|
|
991
1118
|
}
|
|
992
1119
|
async function addEnvToClaudeignore(root) {
|
|
993
|
-
const ignorePath =
|
|
1120
|
+
const ignorePath = join5(root, ".claudeignore");
|
|
994
1121
|
let content;
|
|
995
1122
|
try {
|
|
996
|
-
content = await
|
|
1123
|
+
content = await readFile3(ignorePath, "utf-8");
|
|
997
1124
|
} catch {
|
|
998
1125
|
return false;
|
|
999
1126
|
}
|
|
1000
1127
|
const lines = content.split("\n").map((l) => l.trim());
|
|
1001
1128
|
if (lines.some((l) => l === ".env" || l === ".env.*" || l === ".env*")) return false;
|
|
1002
|
-
await
|
|
1129
|
+
await writeFile3(ignorePath, content.trimEnd() + "\n.env\n.env.*\n");
|
|
1003
1130
|
log.success("Added .env to .claudeignore");
|
|
1004
1131
|
return true;
|
|
1005
1132
|
}
|
|
1006
1133
|
async function addClaudeMdSection(root, heading, content, targetPath) {
|
|
1007
|
-
const claudeMdPath = targetPath ??
|
|
1134
|
+
const claudeMdPath = targetPath ?? join5(root, "CLAUDE.md");
|
|
1008
1135
|
let existing;
|
|
1009
1136
|
try {
|
|
1010
|
-
existing = await
|
|
1137
|
+
existing = await readFile3(claudeMdPath, "utf-8");
|
|
1011
1138
|
} catch {
|
|
1012
1139
|
if (!targetPath) return false;
|
|
1013
|
-
await mkdir2(
|
|
1140
|
+
await mkdir2(join5(root, ".claude"), { recursive: true });
|
|
1014
1141
|
existing = "# Local Claude Config\n";
|
|
1015
1142
|
}
|
|
1016
1143
|
if (existing.includes(heading)) return false;
|
|
@@ -1022,20 +1149,20 @@ ${content}
|
|
|
1022
1149
|
|
|
1023
1150
|
`;
|
|
1024
1151
|
const updated = existing.slice(0, insertAt) + section + existing.slice(insertAt);
|
|
1025
|
-
await
|
|
1152
|
+
await writeFile3(claudeMdPath, updated);
|
|
1026
1153
|
const label = targetPath ? ".claude/CLAUDE.md" : "CLAUDE.md";
|
|
1027
1154
|
log.success(`Added "${heading}" section to ${label}`);
|
|
1028
1155
|
return true;
|
|
1029
1156
|
}
|
|
1030
1157
|
async function createBacklogMd(root) {
|
|
1031
|
-
const backlogPath =
|
|
1158
|
+
const backlogPath = join5(root, "BACKLOG.md");
|
|
1032
1159
|
try {
|
|
1033
1160
|
await access2(backlogPath);
|
|
1034
1161
|
return false;
|
|
1035
1162
|
} catch {
|
|
1036
1163
|
}
|
|
1037
1164
|
const name = root.split("/").pop() ?? "Project";
|
|
1038
|
-
await
|
|
1165
|
+
await writeFile3(backlogPath, `# ${name} - Backlog
|
|
1039
1166
|
|
|
1040
1167
|
> Features discussed but deferred. Pick up when relevant.
|
|
1041
1168
|
> Priority: P0 = next sprint, P1 = soon, P2 = when relevant.
|
|
@@ -1044,14 +1171,14 @@ async function createBacklogMd(root) {
|
|
|
1044
1171
|
return true;
|
|
1045
1172
|
}
|
|
1046
1173
|
async function createClaudeignore(root, detected) {
|
|
1047
|
-
const ignorePath =
|
|
1174
|
+
const ignorePath = join5(root, ".claudeignore");
|
|
1048
1175
|
try {
|
|
1049
1176
|
await access2(ignorePath);
|
|
1050
1177
|
return false;
|
|
1051
1178
|
} catch {
|
|
1052
1179
|
}
|
|
1053
1180
|
const content = generateClaudeignore(detected);
|
|
1054
|
-
await
|
|
1181
|
+
await writeFile3(ignorePath, content);
|
|
1055
1182
|
log.success("Generated .claudeignore with language-specific ignore patterns");
|
|
1056
1183
|
return true;
|
|
1057
1184
|
}
|
|
@@ -1061,15 +1188,15 @@ var SKILL_AUTHORING_SECTION = `
|
|
|
1061
1188
|
${SKILL_AUTHORING_CONTENT}
|
|
1062
1189
|
`;
|
|
1063
1190
|
async function createStarterRules(root) {
|
|
1064
|
-
const rulesDir =
|
|
1191
|
+
const rulesDir = join5(root, ".claude", "rules");
|
|
1065
1192
|
try {
|
|
1066
1193
|
await access2(rulesDir);
|
|
1067
1194
|
return false;
|
|
1068
1195
|
} catch {
|
|
1069
1196
|
}
|
|
1070
1197
|
await mkdir2(rulesDir, { recursive: true });
|
|
1071
|
-
await
|
|
1072
|
-
|
|
1198
|
+
await writeFile3(
|
|
1199
|
+
join5(rulesDir, "conventions.md"),
|
|
1073
1200
|
`# Project Conventions
|
|
1074
1201
|
|
|
1075
1202
|
- Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
|
|
@@ -1082,36 +1209,36 @@ ${SKILL_AUTHORING_SECTION}`
|
|
|
1082
1209
|
return true;
|
|
1083
1210
|
}
|
|
1084
1211
|
async function addSkillAuthoringConventions(root) {
|
|
1085
|
-
const conventionsPath =
|
|
1212
|
+
const conventionsPath = join5(root, ".claude", "rules", "conventions.md");
|
|
1086
1213
|
let content;
|
|
1087
1214
|
try {
|
|
1088
|
-
content = await
|
|
1215
|
+
content = await readFile3(conventionsPath, "utf-8");
|
|
1089
1216
|
} catch {
|
|
1090
1217
|
return false;
|
|
1091
1218
|
}
|
|
1092
1219
|
if (/^##\s+Skill\s+Authoring/im.test(content)) return false;
|
|
1093
|
-
await
|
|
1220
|
+
await writeFile3(conventionsPath, content.trimEnd() + "\n" + SKILL_AUTHORING_SECTION);
|
|
1094
1221
|
log.success("Added Skill Authoring section to .claude/rules/conventions.md");
|
|
1095
1222
|
return true;
|
|
1096
1223
|
}
|
|
1097
1224
|
async function createEnhanceSkill(root) {
|
|
1098
|
-
const skillDir =
|
|
1099
|
-
const skillPath =
|
|
1100
|
-
const globalPath =
|
|
1101
|
-
const legacyProject =
|
|
1102
|
-
const legacyGlobal =
|
|
1225
|
+
const skillDir = join5(root, ".claude", "skills", "lp-enhance");
|
|
1226
|
+
const skillPath = join5(skillDir, "SKILL.md");
|
|
1227
|
+
const globalPath = join5(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
1228
|
+
const legacyProject = join5(root, ".claude", "commands", "lp-enhance.md");
|
|
1229
|
+
const legacyGlobal = join5(homedir(), ".claude", "commands", "lp-enhance.md");
|
|
1103
1230
|
if (await fileExists(skillPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return false;
|
|
1104
1231
|
await mkdir2(skillDir, { recursive: true });
|
|
1105
|
-
await
|
|
1232
|
+
await writeFile3(skillPath, generateEnhanceSkill());
|
|
1106
1233
|
log.success("Generated /lp-enhance skill (.claude/skills/lp-enhance/)");
|
|
1107
1234
|
return true;
|
|
1108
1235
|
}
|
|
1109
1236
|
async function updateEnhanceSkill(root) {
|
|
1110
|
-
const projectPath =
|
|
1111
|
-
const globalPath =
|
|
1237
|
+
const projectPath = join5(root, ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
1238
|
+
const globalPath = join5(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
|
|
1112
1239
|
const targetPath = await fileExists(projectPath) ? projectPath : await fileExists(globalPath) ? globalPath : null;
|
|
1113
1240
|
if (!targetPath) return false;
|
|
1114
|
-
await
|
|
1241
|
+
await writeFile3(targetPath, generateEnhanceSkill());
|
|
1115
1242
|
log.success("Updated /lp-enhance skill to latest version");
|
|
1116
1243
|
return true;
|
|
1117
1244
|
}
|
|
@@ -1140,10 +1267,16 @@ var colors = {
|
|
|
1140
1267
|
return map[sev](` ${sev.toUpperCase()} `);
|
|
1141
1268
|
}
|
|
1142
1269
|
};
|
|
1270
|
+
var warnedKeys = /* @__PURE__ */ new Set();
|
|
1143
1271
|
var log = {
|
|
1144
1272
|
success: (msg) => console.log(` ${chalk.green("\u2713")} ${msg}`),
|
|
1145
1273
|
error: (msg) => console.log(` ${chalk.red("\u2717")} ${msg}`),
|
|
1146
1274
|
warn: (msg) => console.log(` ${chalk.yellow("!")} ${msg}`),
|
|
1275
|
+
warnOnce: (key, msg) => {
|
|
1276
|
+
if (warnedKeys.has(key)) return;
|
|
1277
|
+
warnedKeys.add(key);
|
|
1278
|
+
console.log(` ${chalk.yellow("!")} ${msg}`);
|
|
1279
|
+
},
|
|
1147
1280
|
step: (msg) => console.log(` ${chalk.cyan("\u2192")} ${msg}`),
|
|
1148
1281
|
info: (msg) => console.log(` ${chalk.dim("\xB7")} ${msg}`),
|
|
1149
1282
|
blank: () => console.log()
|
|
@@ -1212,6 +1345,41 @@ function renderDoctorReport(results, options) {
|
|
|
1212
1345
|
return { overallScore, actionableCount: actionable.length };
|
|
1213
1346
|
}
|
|
1214
1347
|
|
|
1348
|
+
// src/lib/settings.ts
|
|
1349
|
+
async function readJsonFile(path) {
|
|
1350
|
+
let raw;
|
|
1351
|
+
try {
|
|
1352
|
+
raw = await readFile4(path, "utf-8");
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
const code = err.code;
|
|
1355
|
+
if (code === "ENOENT") return {};
|
|
1356
|
+
log.warnOnce(`read:${path}`, `Could not read ${path}: ${err.message}`);
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
return JSON.parse(raw);
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
log.warnOnce(`parse:${path}`, `${path} is not valid JSON: ${err.message}. Treating as unreadable to avoid clobbering it.`);
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async function readSettingsJson(root) {
|
|
1367
|
+
return readJsonFile(join6(root, ".claude", "settings.json"));
|
|
1368
|
+
}
|
|
1369
|
+
async function writeSettingsJson(root, settings) {
|
|
1370
|
+
const dir = join6(root, ".claude");
|
|
1371
|
+
await mkdir3(dir, { recursive: true });
|
|
1372
|
+
await writeFile4(join6(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1373
|
+
}
|
|
1374
|
+
async function readSettingsLocalJson(root) {
|
|
1375
|
+
return readJsonFile(join6(root, ".claude", "settings.local.json"));
|
|
1376
|
+
}
|
|
1377
|
+
async function writeSettingsLocalJson(root, settings) {
|
|
1378
|
+
const dir = join6(root, ".claude");
|
|
1379
|
+
await mkdir3(dir, { recursive: true });
|
|
1380
|
+
await writeFile4(join6(dir, "settings.local.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1215
1383
|
export {
|
|
1216
1384
|
__export,
|
|
1217
1385
|
SESSION_START_CONTENT,
|
|
@@ -1231,10 +1399,12 @@ export {
|
|
|
1231
1399
|
writeSettingsLocalJson,
|
|
1232
1400
|
getMemoryPlacement,
|
|
1233
1401
|
LP_STUB_OPEN,
|
|
1402
|
+
addOrUpdateHook,
|
|
1403
|
+
writeSprintHygieneScripts,
|
|
1234
1404
|
applyFixes,
|
|
1235
1405
|
log,
|
|
1236
1406
|
printBanner,
|
|
1237
1407
|
printScoreCard,
|
|
1238
1408
|
renderDoctorReport
|
|
1239
1409
|
};
|
|
1240
|
-
//# sourceMappingURL=chunk-
|
|
1410
|
+
//# sourceMappingURL=chunk-RCYLZUU6.js.map
|