claude-code-kit 0.7.0__py3-none-any.whl
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.
- claude_code_kit-0.7.0.dist-info/METADATA +384 -0
- claude_code_kit-0.7.0.dist-info/RECORD +209 -0
- claude_code_kit-0.7.0.dist-info/WHEEL +4 -0
- claude_code_kit-0.7.0.dist-info/entry_points.txt +4 -0
- claude_code_kit-0.7.0.dist-info/licenses/LICENSE +21 -0
- claude_kit/__init__.py +10 -0
- claude_kit/__main__.py +8 -0
- claude_kit/_payload/agents/acceptance-reviewer.md +60 -0
- claude_kit/_payload/agents/auditor.md +76 -0
- claude_kit/_payload/agents/dependency-scanner.md +84 -0
- claude_kit/_payload/agents/developer.md +187 -0
- claude_kit/_payload/agents/devils-advocate.md +62 -0
- claude_kit/_payload/agents/devops-engineer.md +134 -0
- claude_kit/_payload/agents/e2e-tester.md +152 -0
- claude_kit/_payload/agents/em-reviewer.md +105 -0
- claude_kit/_payload/agents/incident-responder.md +64 -0
- claude_kit/_payload/agents/merge-reviewer.md +194 -0
- claude_kit/_payload/agents/observability-engineer.md +94 -0
- claude_kit/_payload/agents/orchestrator.md +551 -0
- claude_kit/_payload/agents/owasp-reviewer.md +76 -0
- claude_kit/_payload/agents/policy-validator.md +63 -0
- claude_kit/_payload/agents/pr-raiser.md +138 -0
- claude_kit/_payload/agents/risk-classifier.md +50 -0
- claude_kit/_payload/agents/sdlc-code-reviewer.md +196 -0
- claude_kit/_payload/agents/secret-scanner.md +70 -0
- claude_kit/_payload/agents/security-reviewer.md +80 -0
- claude_kit/_payload/agents/senior-backend-dev.md +199 -0
- claude_kit/_payload/agents/senior-frontend-dev.md +181 -0
- claude_kit/_payload/agents/senior-tester.md +206 -0
- claude_kit/_payload/agents/spec-doc-writer.md +331 -0
- claude_kit/_payload/agents/story-planner.md +56 -0
- claude_kit/_payload/agents/technical-architect.md +139 -0
- claude_kit/_payload/agents/tester.md +193 -0
- claude_kit/_payload/agents/ui-designer.md +73 -0
- claude_kit/_payload/agents/unit-tester.md +119 -0
- claude_kit/_payload/catalog/mcp.yaml +54 -0
- claude_kit/_payload/catalog/org.yaml +145 -0
- claude_kit/_payload/catalog/profiles.yaml +96 -0
- claude_kit/_payload/catalog/stacks.yaml +96 -0
- claude_kit/_payload/commands/init.md +36 -0
- claude_kit/_payload/commands/sdlc.md +18 -0
- claude_kit/_payload/commands/status.md +20 -0
- claude_kit/_payload/hooks/hooks.json +58 -0
- claude_kit/_payload/hooks/scripts/audit-log.sh +18 -0
- claude_kit/_payload/hooks/scripts/guard-secrets.sh +26 -0
- claude_kit/_payload/hooks/scripts/lint-fix.sh +38 -0
- claude_kit/_payload/hooks/scripts/load-continuity.sh +32 -0
- claude_kit/_payload/hooks/scripts/load-learnings.sh +40 -0
- claude_kit/_payload/hooks/scripts/type-check.sh +23 -0
- claude_kit/_payload/hooks/scripts/validate-frontmatter.sh +34 -0
- claude_kit/_payload/hooks/scripts/validate-settings.sh +21 -0
- claude_kit/_payload/hooks/scripts/warn-large-edits.sh +24 -0
- claude_kit/_payload/hooks/scripts/warn-missing-tests.sh +24 -0
- claude_kit/_payload/hooks/scripts/warn-sensitive-files.sh +30 -0
- claude_kit/_payload/hooks/scripts/warn-shared-modules.sh +33 -0
- claude_kit/_payload/rules/agent-guardrails.md +83 -0
- claude_kit/_payload/rules/agent-memory.md +106 -0
- claude_kit/_payload/rules/agent-resilience.md +61 -0
- claude_kit/_payload/rules/autonomy-levels.md +30 -0
- claude_kit/_payload/rules/code-organization.md +312 -0
- claude_kit/_payload/rules/continuity.md +84 -0
- claude_kit/_payload/rules/design-patterns.md +422 -0
- claude_kit/_payload/rules/devops-observability.md +57 -0
- claude_kit/_payload/rules/documentation.md +326 -0
- claude_kit/_payload/rules/evals.md +62 -0
- claude_kit/_payload/rules/frontend-best-practices.md +157 -0
- claude_kit/_payload/rules/goal-setting-and-monitoring.md +72 -0
- claude_kit/_payload/rules/human-in-the-loop.md +64 -0
- claude_kit/_payload/rules/linting-and-formatting.md +220 -0
- claude_kit/_payload/rules/mandatory-workflow.md +309 -0
- claude_kit/_payload/rules/model-tiers.md +34 -0
- claude_kit/_payload/rules/quality-gates.md +107 -0
- claude_kit/_payload/rules/rarv-cycle.md +31 -0
- claude_kit/_payload/rules/reasoning-techniques.md +62 -0
- claude_kit/_payload/rules/responsive-and-accessibility.md +353 -0
- claude_kit/_payload/rules/risk-classification.md +36 -0
- claude_kit/_payload/rules/testing.md +417 -0
- claude_kit/_payload/rules/tool-design.md +66 -0
- claude_kit/_payload/skills/_references/accessibility-checklist.md +160 -0
- claude_kit/_payload/skills/_references/orchestration-patterns.md +405 -0
- claude_kit/_payload/skills/_references/performance-checklist.md +153 -0
- claude_kit/_payload/skills/_references/security-checklist.md +134 -0
- claude_kit/_payload/skills/_references/testing-patterns.md +236 -0
- claude_kit/_payload/skills/accessibility-review/SKILL.md +56 -0
- claude_kit/_payload/skills/api-and-interface-design/SKILL.md +294 -0
- claude_kit/_payload/skills/api-integration/SKILL.md +348 -0
- claude_kit/_payload/skills/archive-sprint/SKILL.md +31 -0
- claude_kit/_payload/skills/backlog/SKILL.md +41 -0
- claude_kit/_payload/skills/backlog/item-template.md +20 -0
- claude_kit/_payload/skills/browser-testing-with-devtools/SKILL.md +302 -0
- claude_kit/_payload/skills/ci-cd-and-automation/SKILL.md +402 -0
- claude_kit/_payload/skills/code-review-and-quality/SKILL.md +347 -0
- claude_kit/_payload/skills/code-simplification/SKILL.md +331 -0
- claude_kit/_payload/skills/component-design/SKILL.md +171 -0
- claude_kit/_payload/skills/consolidate-learnings/SKILL.md +55 -0
- claude_kit/_payload/skills/context-engineering/SKILL.md +321 -0
- claude_kit/_payload/skills/debugging-and-error-recovery/SKILL.md +300 -0
- claude_kit/_payload/skills/decision/SKILL.md +46 -0
- claude_kit/_payload/skills/decision/adr-template.md +36 -0
- claude_kit/_payload/skills/deprecation-and-migration/SKILL.md +207 -0
- claude_kit/_payload/skills/documentation-and-adrs/SKILL.md +299 -0
- claude_kit/_payload/skills/doubt-driven-development/SKILL.md +243 -0
- claude_kit/_payload/skills/execute/SKILL.md +27 -0
- claude_kit/_payload/skills/frontend-ui-engineering/SKILL.md +328 -0
- claude_kit/_payload/skills/git-workflow-and-versioning/SKILL.md +300 -0
- claude_kit/_payload/skills/idea-refine/SKILL.md +178 -0
- claude_kit/_payload/skills/idea-refine/examples.md +238 -0
- claude_kit/_payload/skills/idea-refine/frameworks.md +99 -0
- claude_kit/_payload/skills/idea-refine/refinement-criteria.md +113 -0
- claude_kit/_payload/skills/idea-refine/scripts/idea-refine.sh +15 -0
- claude_kit/_payload/skills/incident-postmortem/SKILL.md +74 -0
- claude_kit/_payload/skills/incremental-implementation/SKILL.md +245 -0
- claude_kit/_payload/skills/interview-me/SKILL.md +221 -0
- claude_kit/_payload/skills/load-testing/SKILL.md +83 -0
- claude_kit/_payload/skills/manual-test/SKILL.md +516 -0
- claude_kit/_payload/skills/performance-optimization/SKILL.md +277 -0
- claude_kit/_payload/skills/planning-and-task-breakdown/SKILL.md +223 -0
- claude_kit/_payload/skills/playwright-verification/SKILL.md +205 -0
- claude_kit/_payload/skills/refresh-docs/SKILL.md +63 -0
- claude_kit/_payload/skills/remember/SKILL.md +96 -0
- claude_kit/_payload/skills/scope/SKILL.md +52 -0
- claude_kit/_payload/skills/scope/scope-template.md +82 -0
- claude_kit/_payload/skills/sdlc/SKILL.md +83 -0
- claude_kit/_payload/skills/security-and-hardening/SKILL.md +368 -0
- claude_kit/_payload/skills/security-verification/SKILL.md +209 -0
- claude_kit/_payload/skills/shipping-and-launch/SKILL.md +309 -0
- claude_kit/_payload/skills/smoke-test/SKILL.md +78 -0
- claude_kit/_payload/skills/source-driven-development/SKILL.md +195 -0
- claude_kit/_payload/skills/spec-driven-development/SKILL.md +200 -0
- claude_kit/_payload/skills/sprint/SKILL.md +67 -0
- claude_kit/_payload/skills/sprint/sprint-template.md +90 -0
- claude_kit/_payload/skills/test-driven-development/SKILL.md +383 -0
- claude_kit/_payload/skills/threat-model/SKILL.md +60 -0
- claude_kit/_payload/skills/triage/SKILL.md +87 -0
- claude_kit/_payload/skills/ui-ux-design/SKILL.md +71 -0
- claude_kit/_payload/skills/unit-test/SKILL.md +237 -0
- claude_kit/_payload/skills/using-agent-skills/SKILL.md +180 -0
- claude_kit/_payload/templates/CLAUDE.md +238 -0
- claude_kit/_payload/templates/CLAUDE.stack.md.tmpl +53 -0
- claude_kit/_payload/templates/CONTINUITY.template.md +35 -0
- claude_kit/_payload/templates/README.claude-sdlc.md.tmpl +219 -0
- claude_kit/_payload/templates/agent-memory/MEMORY.md +30 -0
- claude_kit/_payload/templates/agent-memory/api/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/architecture/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/debugging/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/gotchas/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/patterns/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/performance/.gitkeep +0 -0
- claude_kit/_payload/templates/artifacts/adr.md +18 -0
- claude_kit/_payload/templates/artifacts/feature-spec.md +29 -0
- claude_kit/_payload/templates/artifacts/release-plan.md +23 -0
- claude_kit/_payload/templates/artifacts/runbook.md +24 -0
- claude_kit/_payload/templates/artifacts/security-review.md +23 -0
- claude_kit/_payload/templates/artifacts/test-plan.md +22 -0
- claude_kit/_payload/templates/org/README.md +53 -0
- claude_kit/_payload/templates/org/agents/data-workflow-agent.md +59 -0
- claude_kit/_payload/templates/org/agents/founder-prototype-agent.md +61 -0
- claude_kit/_payload/templates/org/agents/internal-tools-builder.md +63 -0
- claude_kit/_payload/templates/org/agents/pm-copilot.md +60 -0
- claude_kit/_payload/templates/org/agents/support-ticket-engineer.md +63 -0
- claude_kit/_payload/templates/org/packs/devops-and-release/README.md +46 -0
- claude_kit/_payload/templates/org/packs/devops-and-release/pack.yaml +32 -0
- claude_kit/_payload/templates/org/packs/engineering-core/README.md +46 -0
- claude_kit/_payload/templates/org/packs/engineering-core/pack.yaml +44 -0
- claude_kit/_payload/templates/org/packs/non-engineer-builder/README.md +53 -0
- claude_kit/_payload/templates/org/packs/non-engineer-builder/pack.yaml +39 -0
- claude_kit/_payload/templates/org/packs/onboarding-and-docs/README.md +49 -0
- claude_kit/_payload/templates/org/packs/onboarding-and-docs/pack.yaml +26 -0
- claude_kit/_payload/templates/org/packs/product-to-code/README.md +50 -0
- claude_kit/_payload/templates/org/packs/product-to-code/pack.yaml +34 -0
- claude_kit/_payload/templates/org/packs/quality-and-review/README.md +53 -0
- claude_kit/_payload/templates/org/packs/quality-and-review/pack.yaml +40 -0
- claude_kit/_payload/templates/org/packs/security-and-compliance/README.md +50 -0
- claude_kit/_payload/templates/org/packs/security-and-compliance/pack.yaml +36 -0
- claude_kit/_payload/templates/org/rules/ai-working-agreement.md +45 -0
- claude_kit/_payload/templates/org/rules/ambiguity-resolution.md +36 -0
- claude_kit/_payload/templates/org/rules/branch-and-pr-policy.md +41 -0
- claude_kit/_payload/templates/org/rules/compliance-policy.md +50 -0
- claude_kit/_payload/templates/org/rules/non-engineer-safe-coding.md +37 -0
- claude_kit/_payload/templates/org/rules/pii-policy.md +46 -0
- claude_kit/_payload/templates/org/rules/production-data-policy.md +35 -0
- claude_kit/_payload/templates/org/rules/prompt-to-task-conversion.md +30 -0
- claude_kit/_payload/templates/org/rules/prototype-boundaries.md +40 -0
- claude_kit/_payload/templates/org/rules/secrets-policy.md +34 -0
- claude_kit/_payload/templates/org/skills/customer-issue-to-fix/SKILL.md +61 -0
- claude_kit/_payload/templates/org/skills/feature-from-idea/SKILL.md +56 -0
- claude_kit/_payload/templates/org/skills/prompt-to-safe-task/SKILL.md +59 -0
- claude_kit/_payload/templates/org/skills/prototype-to-production/SKILL.md +61 -0
- claude_kit/_payload/templates/org/skills/repo-onboarding/SKILL.md +60 -0
- claude_kit/_payload/templates/settings.json +53 -0
- claude_kit/_payload/templates/stacks/backend/python/fastapi/rules/fastapi-patterns.md +64 -0
- claude_kit/_payload/templates/stacks/db/mongodb/agents/migration-specialist.md +61 -0
- claude_kit/_payload/templates/stacks/db/mongodb/agents/mongodb-specialist.md +59 -0
- claude_kit/_payload/templates/stacks/db/mongodb/rules/mongodb-patterns.md +39 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/db-performance-reviewer.md +66 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/migration-specialist.md +56 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/postgres-specialist.md +58 -0
- claude_kit/_payload/templates/stacks/db/postgres/rules/database-performance.md +64 -0
- claude_kit/_payload/templates/stacks/db/postgres/rules/postgres-patterns.md +43 -0
- claude_kit/_payload/templates/stacks/frontend/react/rules/react-patterns.md +63 -0
- claude_kit/catalog.py +476 -0
- claude_kit/cli.py +327 -0
- claude_kit/hooks.py +246 -0
- claude_kit/models.py +205 -0
- claude_kit/prompts.py +209 -0
- claude_kit/render.py +146 -0
- claude_kit/scaffold.py +492 -0
- claude_kit/upgrader.py +294 -0
- claude_kit/validator.py +197 -0
claude_kit/upgrader.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Diff and safe-upgrade of a scaffolded claude-kit configuration.
|
|
2
|
+
|
|
3
|
+
The strategy is **render-and-compare**: re-render a pristine reference install of the *recorded*
|
|
4
|
+
selection into a throwaway temp dir (reusing :func:`claude_kit.scaffold.install_sdlc`, so no install
|
|
5
|
+
logic is duplicated), then compare that reference tree against the live ``target`` tree. Each file's
|
|
6
|
+
recorded ``owner`` (kit / overlay / user-editable) plus whether it was modified since install (live
|
|
7
|
+
checksum vs. the checksum in ``.claude/config/init-options.json``) decides the action:
|
|
8
|
+
|
|
9
|
+
* **kit** / **overlay** files are refreshed to the new content (a user-modified one is backed up first).
|
|
10
|
+
* **user-editable** files (``CLAUDE.md``, ``settings.json``, ``.mcp.json``, ``CONTINUITY.md``,
|
|
11
|
+
``agent-memory/``) are *never* clobbered: if the user changed one, the new version is written
|
|
12
|
+
alongside as a ``.claude-kit`` sidecar so they can merge it (``--force`` overwrites instead).
|
|
13
|
+
* Files the current kit no longer ships (orphans) are backed up and removed — but only kit/overlay
|
|
14
|
+
ones; a user's own files are left untouched.
|
|
15
|
+
|
|
16
|
+
``diff`` previews these actions and writes nothing; ``upgrade`` applies them and then refreshes
|
|
17
|
+
``init-options.json`` with the new checksums and kit version. Both return the ``(ok, messages)``
|
|
18
|
+
contract shared by the other lifecycle commands.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import shutil
|
|
25
|
+
import tempfile
|
|
26
|
+
from contextlib import ExitStack
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from claude_kit import catalog, scaffold
|
|
31
|
+
from claude_kit.models import InitOptions
|
|
32
|
+
from claude_kit.validator import _load_init_options
|
|
33
|
+
|
|
34
|
+
#: Sidecar suffix for a new version of a user-modified, protected file.
|
|
35
|
+
_SIDECAR_SUFFIX = ".claude-kit"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class _Action:
|
|
40
|
+
"""One planned change to a single file, relative to the project root."""
|
|
41
|
+
|
|
42
|
+
rel: str
|
|
43
|
+
kind: str # "add" | "update" | "keep" | "remove"
|
|
44
|
+
owner: str # "kit" | "overlay" | "user-editable"
|
|
45
|
+
user_modified: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class _Comparison:
|
|
50
|
+
"""The result of comparing a freshly-rendered reference tree against the live install."""
|
|
51
|
+
|
|
52
|
+
target: Path
|
|
53
|
+
old: InitOptions
|
|
54
|
+
plan: object # ResolvedPlan
|
|
55
|
+
ref_root: Path
|
|
56
|
+
actions: list[_Action]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sha256(path: Path) -> str:
|
|
60
|
+
"""Return the hex SHA-256 of a file's bytes."""
|
|
61
|
+
h = hashlib.sha256()
|
|
62
|
+
h.update(path.read_bytes())
|
|
63
|
+
return h.hexdigest()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _compare(src: Path, target: Path) -> _Comparison | str:
|
|
67
|
+
"""Render a reference install and diff it against ``target``.
|
|
68
|
+
|
|
69
|
+
Returns a :class:`_Comparison`, or a short error string (``"not-installed"`` /
|
|
70
|
+
``"no-options"``) the callers turn into a ``FAIL`` message. The caller owns cleanup of
|
|
71
|
+
``ref_root`` (via :func:`_cleanup`).
|
|
72
|
+
"""
|
|
73
|
+
target = Path(target).expanduser().resolve()
|
|
74
|
+
claude = target / ".claude"
|
|
75
|
+
if not claude.is_dir():
|
|
76
|
+
return "not-installed"
|
|
77
|
+
old = _load_init_options(claude)
|
|
78
|
+
if old is None:
|
|
79
|
+
return "no-options"
|
|
80
|
+
|
|
81
|
+
plan = catalog.resolve(src, old.selection)
|
|
82
|
+
# Render the reference under the REAL project name so CLAUDE.md/README don't diff spuriously.
|
|
83
|
+
plan.context["project_name"] = target.name
|
|
84
|
+
ref_root = Path(tempfile.mkdtemp(prefix="claude-kit-ref-"))
|
|
85
|
+
scaffold.install_sdlc(src, ref_root, plan, force=True, log=[])
|
|
86
|
+
|
|
87
|
+
ref_opts = _load_init_options(ref_root / ".claude")
|
|
88
|
+
ref = {r.path: r for r in ref_opts.files} if ref_opts else {}
|
|
89
|
+
old_map = {r.path: r for r in old.files}
|
|
90
|
+
|
|
91
|
+
actions: list[_Action] = []
|
|
92
|
+
for rel, rrec in sorted(ref.items()):
|
|
93
|
+
live = target / rel
|
|
94
|
+
if not live.is_file():
|
|
95
|
+
actions.append(_Action(rel, "add", rrec.owner))
|
|
96
|
+
continue
|
|
97
|
+
if _sha256(live) == rrec.sha256:
|
|
98
|
+
continue # already identical to the new reference
|
|
99
|
+
old_sha = old_map[rel].sha256 if rel in old_map else None
|
|
100
|
+
user_modified = old_sha is not None and _sha256(live) != old_sha
|
|
101
|
+
if rrec.owner == "user-editable":
|
|
102
|
+
actions.append(
|
|
103
|
+
_Action(
|
|
104
|
+
rel,
|
|
105
|
+
"keep" if user_modified else "update",
|
|
106
|
+
rrec.owner,
|
|
107
|
+
user_modified,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
actions.append(_Action(rel, "update", rrec.owner, user_modified))
|
|
112
|
+
|
|
113
|
+
# Orphans: recorded kit/overlay files the current kit no longer ships for this selection.
|
|
114
|
+
for rel, orec in sorted(old_map.items()):
|
|
115
|
+
if rel in ref or orec.owner == "user-editable":
|
|
116
|
+
continue
|
|
117
|
+
if (target / rel).is_file():
|
|
118
|
+
actions.append(_Action(rel, "remove", orec.owner))
|
|
119
|
+
|
|
120
|
+
return _Comparison(
|
|
121
|
+
target=target, old=old, plan=plan, ref_root=ref_root, actions=actions
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cleanup(ref_root: Path) -> None:
|
|
126
|
+
"""Remove the throwaway reference render."""
|
|
127
|
+
shutil.rmtree(ref_root, ignore_errors=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _next_backup_dir(target: Path) -> Path:
|
|
131
|
+
"""Return a fresh, non-existing ``.claude-kit.bak-N/`` directory under ``target``."""
|
|
132
|
+
n = 1
|
|
133
|
+
while (target / f".claude-kit.bak-{n}").exists():
|
|
134
|
+
n += 1
|
|
135
|
+
return target / f".claude-kit.bak-{n}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_preview(cmp: _Comparison) -> list[str]:
|
|
139
|
+
"""Build the human-readable diff report from a comparison (no side effects)."""
|
|
140
|
+
from claude_kit import __version__
|
|
141
|
+
|
|
142
|
+
msgs: list[str] = []
|
|
143
|
+
if cmp.old.claude_kit_version != __version__:
|
|
144
|
+
msgs.append(f"INFO kit version {cmp.old.claude_kit_version} -> {__version__}")
|
|
145
|
+
else:
|
|
146
|
+
msgs.append(f"INFO kit version {__version__} (unchanged)")
|
|
147
|
+
|
|
148
|
+
if not cmp.actions:
|
|
149
|
+
msgs.append("OK everything up to date — nothing to upgrade")
|
|
150
|
+
return msgs
|
|
151
|
+
|
|
152
|
+
order = {"add": 0, "update": 1, "keep": 2, "remove": 3}
|
|
153
|
+
verbs = {
|
|
154
|
+
"add": "add",
|
|
155
|
+
"update": "update",
|
|
156
|
+
"keep": "keep (sidecar new version)",
|
|
157
|
+
"remove": "remove (orphan)",
|
|
158
|
+
}
|
|
159
|
+
for act in sorted(cmp.actions, key=lambda a: (order[a.kind], a.rel)):
|
|
160
|
+
note = ""
|
|
161
|
+
if act.kind == "update" and act.user_modified and act.owner != "user-editable":
|
|
162
|
+
note = " [local changes will be backed up]"
|
|
163
|
+
elif act.kind == "keep":
|
|
164
|
+
note = " [your edits kept; new version as .claude-kit]"
|
|
165
|
+
msgs.append(f" {verbs[act.kind]:<28} {act.rel} ({act.owner}){note}")
|
|
166
|
+
|
|
167
|
+
counts: dict[str, int] = {}
|
|
168
|
+
for act in cmp.actions:
|
|
169
|
+
counts[act.kind] = counts.get(act.kind, 0) + 1
|
|
170
|
+
summary = ", ".join(f"{counts[k]} {k}" for k in order if k in counts)
|
|
171
|
+
msgs.append(f"INFO {summary}")
|
|
172
|
+
return msgs
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def diff(target: str | Path) -> tuple[bool, list[str]]:
|
|
176
|
+
"""Preview what an upgrade would change (no writes). Returns ``(ok, messages)``."""
|
|
177
|
+
with ExitStack() as stack:
|
|
178
|
+
src = scaffold.payload_dir(stack)
|
|
179
|
+
result = _compare(src, target)
|
|
180
|
+
if isinstance(result, str):
|
|
181
|
+
return _explain_error(result, target)
|
|
182
|
+
try:
|
|
183
|
+
return True, _format_preview(result)
|
|
184
|
+
finally:
|
|
185
|
+
_cleanup(result.ref_root)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def upgrade(target: str | Path, *, force: bool = False) -> tuple[bool, list[str]]:
|
|
189
|
+
"""Apply the upgrade: refresh kit/overlay files, protect user edits, prune orphans.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
target: Project root to upgrade.
|
|
193
|
+
force: Overwrite user-modified *user-editable* files instead of writing sidecars.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
``(ok, messages)``.
|
|
197
|
+
"""
|
|
198
|
+
with ExitStack() as stack:
|
|
199
|
+
src = scaffold.payload_dir(stack)
|
|
200
|
+
result = _compare(src, target)
|
|
201
|
+
if isinstance(result, str):
|
|
202
|
+
return _explain_error(result, target)
|
|
203
|
+
try:
|
|
204
|
+
return _apply(result, force=force)
|
|
205
|
+
finally:
|
|
206
|
+
_cleanup(result.ref_root)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _apply(cmp: _Comparison, *, force: bool) -> tuple[bool, list[str]]:
|
|
210
|
+
"""Carry out the planned actions and refresh ``init-options.json``."""
|
|
211
|
+
msgs: list[str] = []
|
|
212
|
+
if not cmp.actions:
|
|
213
|
+
msgs.append("OK everything up to date — nothing to upgrade")
|
|
214
|
+
return True, msgs
|
|
215
|
+
|
|
216
|
+
target, ref_root = cmp.target, cmp.ref_root
|
|
217
|
+
backup_dir = _next_backup_dir(target)
|
|
218
|
+
backed_up = 0
|
|
219
|
+
|
|
220
|
+
def _backup(rel: str) -> None:
|
|
221
|
+
nonlocal backed_up
|
|
222
|
+
live = target / rel
|
|
223
|
+
if not live.is_file():
|
|
224
|
+
return
|
|
225
|
+
dest = backup_dir / rel
|
|
226
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
shutil.copy2(live, dest)
|
|
228
|
+
backed_up += 1
|
|
229
|
+
|
|
230
|
+
def _copy_ref(rel: str) -> None:
|
|
231
|
+
live = target / rel
|
|
232
|
+
live.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
shutil.copy2(ref_root / rel, live)
|
|
234
|
+
|
|
235
|
+
for act in cmp.actions:
|
|
236
|
+
live = target / act.rel
|
|
237
|
+
if act.kind == "add":
|
|
238
|
+
_copy_ref(act.rel)
|
|
239
|
+
msgs.append(f" + {act.rel}")
|
|
240
|
+
elif act.kind == "update":
|
|
241
|
+
if act.user_modified:
|
|
242
|
+
_backup(act.rel)
|
|
243
|
+
if act.owner == "user-editable" and act.user_modified and not force:
|
|
244
|
+
# Protect: keep the user's file, drop the new version beside it.
|
|
245
|
+
shutil.copy2(
|
|
246
|
+
ref_root / act.rel, live.with_name(live.name + _SIDECAR_SUFFIX)
|
|
247
|
+
)
|
|
248
|
+
msgs.append(
|
|
249
|
+
f" ~ {act.rel} (kept; new version -> {live.name}{_SIDECAR_SUFFIX})"
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
_copy_ref(act.rel)
|
|
253
|
+
msgs.append(f" ✓ {act.rel}")
|
|
254
|
+
elif act.kind == "keep":
|
|
255
|
+
shutil.copy2(
|
|
256
|
+
ref_root / act.rel, live.with_name(live.name + _SIDECAR_SUFFIX)
|
|
257
|
+
)
|
|
258
|
+
msgs.append(
|
|
259
|
+
f" ~ {act.rel} (kept; new version -> {live.name}{_SIDECAR_SUFFIX})"
|
|
260
|
+
)
|
|
261
|
+
elif act.kind == "remove":
|
|
262
|
+
_backup(act.rel)
|
|
263
|
+
live.unlink(missing_ok=True)
|
|
264
|
+
msgs.append(f" - {act.rel} (orphan removed)")
|
|
265
|
+
|
|
266
|
+
# Adopt the reference's config verbatim as the new baseline. Recording the kit's CANONICAL
|
|
267
|
+
# checksums (not the live ones) is what keeps a *kept* user-editable file detectable as
|
|
268
|
+
# user-modified on the next upgrade — re-recording its live sha would make the next run treat
|
|
269
|
+
# it as pristine and clobber the user's edits.
|
|
270
|
+
ref_config = ref_root / ".claude" / "config"
|
|
271
|
+
dst_config = target / ".claude" / "config"
|
|
272
|
+
dst_config.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
for name in ("init-options.json", "stack-catalog.snapshot.yaml"):
|
|
274
|
+
if (ref_config / name).is_file():
|
|
275
|
+
shutil.copy2(ref_config / name, dst_config / name)
|
|
276
|
+
|
|
277
|
+
if backed_up:
|
|
278
|
+
msgs.append(
|
|
279
|
+
f"INFO backed up {backed_up} modified/removed file(s) -> {backup_dir.name}/"
|
|
280
|
+
)
|
|
281
|
+
msgs.append("OK upgrade complete")
|
|
282
|
+
return True, msgs
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _explain_error(code: str, target: str | Path) -> tuple[bool, list[str]]:
|
|
286
|
+
"""Translate a ``_compare`` error code into a ``(False, [FAIL …])`` report."""
|
|
287
|
+
if code == "not-installed":
|
|
288
|
+
return False, [
|
|
289
|
+
f"FAIL no .claude/ at {Path(target).expanduser().resolve()} — run `claude-kit init` first"
|
|
290
|
+
]
|
|
291
|
+
return False, [
|
|
292
|
+
"FAIL no .claude/config/init-options.json — this install predates upgrade tracking; "
|
|
293
|
+
"re-run `claude-kit init --force` to start tracking"
|
|
294
|
+
]
|
claude_kit/validator.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Validation and health checks for a scaffolded claude-kit configuration.
|
|
2
|
+
|
|
3
|
+
``validate`` performs structural checks (files present, JSON parses, frontmatter complete,
|
|
4
|
+
referenced overlays installed). ``doctor`` adds environment checks (git/jq available, hook scripts
|
|
5
|
+
executable, runtime dirs gitignored). Both return ``(ok, messages)`` so the CLI can print a report
|
|
6
|
+
and choose an exit code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from claude_kit.models import InitOptions
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_frontmatter(text: str) -> dict[str, str] | None:
|
|
19
|
+
"""Return the frontmatter key/values at the top of a markdown file, or None if absent.
|
|
20
|
+
|
|
21
|
+
Uses lenient line-based parsing (``key: value`` at column 0), deliberately mirroring Claude
|
|
22
|
+
Code's own frontmatter reader rather than strict YAML. Real agent/skill files routinely carry
|
|
23
|
+
a colon inside a ``description`` ("Read-only: routes fixes…") or a bracketed ``argument-hint``
|
|
24
|
+
(``[optional: "x"]``); ``yaml.safe_load`` rejects both even though Claude Code accepts them, so
|
|
25
|
+
validating with strict YAML would fail on valid files. Indented continuation lines, blanks, and
|
|
26
|
+
comments are skipped — only the top-level scalar fields this module checks (``name``,
|
|
27
|
+
``description``) need to be recovered.
|
|
28
|
+
"""
|
|
29
|
+
if not text.startswith("---"):
|
|
30
|
+
return None
|
|
31
|
+
end = text.find("\n---", 3)
|
|
32
|
+
if end == -1:
|
|
33
|
+
return None
|
|
34
|
+
data: dict[str, str] = {}
|
|
35
|
+
for line in text[3:end].splitlines():
|
|
36
|
+
if not line.strip() or line[0] in (" ", "\t", "#"):
|
|
37
|
+
continue
|
|
38
|
+
key, sep, value = line.partition(":")
|
|
39
|
+
if sep:
|
|
40
|
+
data[key.strip()] = value.strip()
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_init_options(claude_dir: Path) -> InitOptions | None:
|
|
45
|
+
"""Load and parse ``.claude/config/init-options.json`` if present."""
|
|
46
|
+
path = claude_dir / "config" / "init-options.json"
|
|
47
|
+
if not path.is_file():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
return InitOptions.from_dict(json.loads(path.read_text(encoding="utf-8")))
|
|
51
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate(target: str | Path) -> tuple[bool, list[str]]:
|
|
56
|
+
"""Structurally validate the claude-kit config at ``target``.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
``(ok, messages)`` where each message is prefixed ``OK``/``WARN``/``FAIL`` and ``ok`` is
|
|
60
|
+
False if any ``FAIL`` was recorded.
|
|
61
|
+
"""
|
|
62
|
+
target = Path(target).expanduser().resolve()
|
|
63
|
+
claude = target / ".claude"
|
|
64
|
+
msgs: list[str] = []
|
|
65
|
+
ok = True
|
|
66
|
+
|
|
67
|
+
def fail(m: str) -> None:
|
|
68
|
+
nonlocal ok
|
|
69
|
+
ok = False
|
|
70
|
+
msgs.append(f"FAIL {m}")
|
|
71
|
+
|
|
72
|
+
def warn(m: str) -> None:
|
|
73
|
+
msgs.append(f"WARN {m}")
|
|
74
|
+
|
|
75
|
+
def good(m: str) -> None:
|
|
76
|
+
msgs.append(f"OK {m}")
|
|
77
|
+
|
|
78
|
+
if not claude.is_dir():
|
|
79
|
+
fail(f"no .claude/ directory in {target} — run `claude-kit init` here")
|
|
80
|
+
return ok, msgs
|
|
81
|
+
|
|
82
|
+
options = _load_init_options(claude)
|
|
83
|
+
if options is None:
|
|
84
|
+
warn(
|
|
85
|
+
"missing or unreadable .claude/config/init-options.json (validate/upgrade limited)"
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
good(
|
|
89
|
+
f"init-options.json (schema v{options.schema_version}, kit {options.claude_kit_version})"
|
|
90
|
+
)
|
|
91
|
+
for rec in options.files:
|
|
92
|
+
if not (target / rec.path).exists():
|
|
93
|
+
fail(f"recorded file missing: {rec.path}")
|
|
94
|
+
good(f"tracked files present ({len(options.files)} recorded)")
|
|
95
|
+
|
|
96
|
+
settings = claude / "settings.json"
|
|
97
|
+
if settings.is_file():
|
|
98
|
+
try:
|
|
99
|
+
json.loads(settings.read_text(encoding="utf-8"))
|
|
100
|
+
good("settings.json is valid JSON")
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
fail(f"settings.json is invalid JSON: {exc}")
|
|
103
|
+
else:
|
|
104
|
+
warn("no .claude/settings.json (hooks not configured)")
|
|
105
|
+
|
|
106
|
+
agents_dir = claude / "agents"
|
|
107
|
+
if agents_dir.is_dir():
|
|
108
|
+
bad = [
|
|
109
|
+
p.name
|
|
110
|
+
for p in agents_dir.glob("*.md")
|
|
111
|
+
if not (_parse_frontmatter(p.read_text(encoding="utf-8")) or {}).get("name")
|
|
112
|
+
or not (_parse_frontmatter(p.read_text(encoding="utf-8")) or {}).get(
|
|
113
|
+
"description"
|
|
114
|
+
)
|
|
115
|
+
]
|
|
116
|
+
if bad:
|
|
117
|
+
fail(
|
|
118
|
+
f"agents missing name/description frontmatter: {', '.join(sorted(bad))}"
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
good(
|
|
122
|
+
f"agents/ frontmatter complete ({sum(1 for _ in agents_dir.glob('*.md'))} agents)"
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
warn("no .claude/agents/")
|
|
126
|
+
|
|
127
|
+
skills_dir = claude / "skills"
|
|
128
|
+
if skills_dir.is_dir():
|
|
129
|
+
bad_skills = [
|
|
130
|
+
d.name
|
|
131
|
+
for d in skills_dir.iterdir()
|
|
132
|
+
if d.is_dir()
|
|
133
|
+
and (d / "SKILL.md").is_file()
|
|
134
|
+
and not (
|
|
135
|
+
_parse_frontmatter((d / "SKILL.md").read_text(encoding="utf-8")) or {}
|
|
136
|
+
).get("description")
|
|
137
|
+
]
|
|
138
|
+
if bad_skills:
|
|
139
|
+
fail(f"skills missing description: {', '.join(sorted(bad_skills))}")
|
|
140
|
+
else:
|
|
141
|
+
good(
|
|
142
|
+
f"skills/ descriptions present "
|
|
143
|
+
f"({sum(1 for d in skills_dir.iterdir() if d.is_dir() and (d / 'SKILL.md').is_file())} skills)"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
rules_dir = claude / "rules"
|
|
147
|
+
if not rules_dir.is_dir() or not any(rules_dir.glob("*.md")):
|
|
148
|
+
fail("no .claude/rules/ content")
|
|
149
|
+
else:
|
|
150
|
+
good(f"rules/ present ({sum(1 for _ in rules_dir.glob('*.md'))} rules)")
|
|
151
|
+
|
|
152
|
+
return ok, msgs
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def doctor(target: str | Path) -> tuple[bool, list[str]]:
|
|
156
|
+
"""Run :func:`validate` plus environment/health checks.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
``(ok, messages)``; environment issues are warnings (do not fail) unless they break config.
|
|
160
|
+
"""
|
|
161
|
+
ok, msgs = validate(target)
|
|
162
|
+
target = Path(target).expanduser().resolve()
|
|
163
|
+
claude = target / ".claude"
|
|
164
|
+
|
|
165
|
+
for tool, why in (
|
|
166
|
+
("git", "version control"),
|
|
167
|
+
("jq", "command hooks parse tool input with jq"),
|
|
168
|
+
):
|
|
169
|
+
if shutil.which(tool):
|
|
170
|
+
msgs.append(f"OK {tool} found ({why})")
|
|
171
|
+
else:
|
|
172
|
+
msgs.append(f"WARN {tool} not on PATH — {why}")
|
|
173
|
+
|
|
174
|
+
hooks_dir = claude / "hooks"
|
|
175
|
+
if hooks_dir.is_dir():
|
|
176
|
+
nonexec = [
|
|
177
|
+
p.name for p in hooks_dir.glob("*.sh") if not (p.stat().st_mode & 0o111)
|
|
178
|
+
]
|
|
179
|
+
if nonexec:
|
|
180
|
+
msgs.append(
|
|
181
|
+
f"WARN hook scripts not executable: {', '.join(sorted(nonexec))} "
|
|
182
|
+
f"(run: chmod +x .claude/hooks/*.sh)"
|
|
183
|
+
)
|
|
184
|
+
elif any(hooks_dir.glob("*.sh")):
|
|
185
|
+
msgs.append("OK hook scripts are executable")
|
|
186
|
+
|
|
187
|
+
gitignore = target / ".gitignore"
|
|
188
|
+
gi = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
|
|
189
|
+
for entry in (".claude/state/", ".claude/tmp/"):
|
|
190
|
+
if entry in gi:
|
|
191
|
+
msgs.append(f"OK {entry} is gitignored")
|
|
192
|
+
else:
|
|
193
|
+
msgs.append(
|
|
194
|
+
f"WARN {entry} not gitignored (runtime artifacts may be committed)"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return ok, msgs
|