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.
Files changed (209) hide show
  1. claude_code_kit-0.7.0.dist-info/METADATA +384 -0
  2. claude_code_kit-0.7.0.dist-info/RECORD +209 -0
  3. claude_code_kit-0.7.0.dist-info/WHEEL +4 -0
  4. claude_code_kit-0.7.0.dist-info/entry_points.txt +4 -0
  5. claude_code_kit-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. claude_kit/__init__.py +10 -0
  7. claude_kit/__main__.py +8 -0
  8. claude_kit/_payload/agents/acceptance-reviewer.md +60 -0
  9. claude_kit/_payload/agents/auditor.md +76 -0
  10. claude_kit/_payload/agents/dependency-scanner.md +84 -0
  11. claude_kit/_payload/agents/developer.md +187 -0
  12. claude_kit/_payload/agents/devils-advocate.md +62 -0
  13. claude_kit/_payload/agents/devops-engineer.md +134 -0
  14. claude_kit/_payload/agents/e2e-tester.md +152 -0
  15. claude_kit/_payload/agents/em-reviewer.md +105 -0
  16. claude_kit/_payload/agents/incident-responder.md +64 -0
  17. claude_kit/_payload/agents/merge-reviewer.md +194 -0
  18. claude_kit/_payload/agents/observability-engineer.md +94 -0
  19. claude_kit/_payload/agents/orchestrator.md +551 -0
  20. claude_kit/_payload/agents/owasp-reviewer.md +76 -0
  21. claude_kit/_payload/agents/policy-validator.md +63 -0
  22. claude_kit/_payload/agents/pr-raiser.md +138 -0
  23. claude_kit/_payload/agents/risk-classifier.md +50 -0
  24. claude_kit/_payload/agents/sdlc-code-reviewer.md +196 -0
  25. claude_kit/_payload/agents/secret-scanner.md +70 -0
  26. claude_kit/_payload/agents/security-reviewer.md +80 -0
  27. claude_kit/_payload/agents/senior-backend-dev.md +199 -0
  28. claude_kit/_payload/agents/senior-frontend-dev.md +181 -0
  29. claude_kit/_payload/agents/senior-tester.md +206 -0
  30. claude_kit/_payload/agents/spec-doc-writer.md +331 -0
  31. claude_kit/_payload/agents/story-planner.md +56 -0
  32. claude_kit/_payload/agents/technical-architect.md +139 -0
  33. claude_kit/_payload/agents/tester.md +193 -0
  34. claude_kit/_payload/agents/ui-designer.md +73 -0
  35. claude_kit/_payload/agents/unit-tester.md +119 -0
  36. claude_kit/_payload/catalog/mcp.yaml +54 -0
  37. claude_kit/_payload/catalog/org.yaml +145 -0
  38. claude_kit/_payload/catalog/profiles.yaml +96 -0
  39. claude_kit/_payload/catalog/stacks.yaml +96 -0
  40. claude_kit/_payload/commands/init.md +36 -0
  41. claude_kit/_payload/commands/sdlc.md +18 -0
  42. claude_kit/_payload/commands/status.md +20 -0
  43. claude_kit/_payload/hooks/hooks.json +58 -0
  44. claude_kit/_payload/hooks/scripts/audit-log.sh +18 -0
  45. claude_kit/_payload/hooks/scripts/guard-secrets.sh +26 -0
  46. claude_kit/_payload/hooks/scripts/lint-fix.sh +38 -0
  47. claude_kit/_payload/hooks/scripts/load-continuity.sh +32 -0
  48. claude_kit/_payload/hooks/scripts/load-learnings.sh +40 -0
  49. claude_kit/_payload/hooks/scripts/type-check.sh +23 -0
  50. claude_kit/_payload/hooks/scripts/validate-frontmatter.sh +34 -0
  51. claude_kit/_payload/hooks/scripts/validate-settings.sh +21 -0
  52. claude_kit/_payload/hooks/scripts/warn-large-edits.sh +24 -0
  53. claude_kit/_payload/hooks/scripts/warn-missing-tests.sh +24 -0
  54. claude_kit/_payload/hooks/scripts/warn-sensitive-files.sh +30 -0
  55. claude_kit/_payload/hooks/scripts/warn-shared-modules.sh +33 -0
  56. claude_kit/_payload/rules/agent-guardrails.md +83 -0
  57. claude_kit/_payload/rules/agent-memory.md +106 -0
  58. claude_kit/_payload/rules/agent-resilience.md +61 -0
  59. claude_kit/_payload/rules/autonomy-levels.md +30 -0
  60. claude_kit/_payload/rules/code-organization.md +312 -0
  61. claude_kit/_payload/rules/continuity.md +84 -0
  62. claude_kit/_payload/rules/design-patterns.md +422 -0
  63. claude_kit/_payload/rules/devops-observability.md +57 -0
  64. claude_kit/_payload/rules/documentation.md +326 -0
  65. claude_kit/_payload/rules/evals.md +62 -0
  66. claude_kit/_payload/rules/frontend-best-practices.md +157 -0
  67. claude_kit/_payload/rules/goal-setting-and-monitoring.md +72 -0
  68. claude_kit/_payload/rules/human-in-the-loop.md +64 -0
  69. claude_kit/_payload/rules/linting-and-formatting.md +220 -0
  70. claude_kit/_payload/rules/mandatory-workflow.md +309 -0
  71. claude_kit/_payload/rules/model-tiers.md +34 -0
  72. claude_kit/_payload/rules/quality-gates.md +107 -0
  73. claude_kit/_payload/rules/rarv-cycle.md +31 -0
  74. claude_kit/_payload/rules/reasoning-techniques.md +62 -0
  75. claude_kit/_payload/rules/responsive-and-accessibility.md +353 -0
  76. claude_kit/_payload/rules/risk-classification.md +36 -0
  77. claude_kit/_payload/rules/testing.md +417 -0
  78. claude_kit/_payload/rules/tool-design.md +66 -0
  79. claude_kit/_payload/skills/_references/accessibility-checklist.md +160 -0
  80. claude_kit/_payload/skills/_references/orchestration-patterns.md +405 -0
  81. claude_kit/_payload/skills/_references/performance-checklist.md +153 -0
  82. claude_kit/_payload/skills/_references/security-checklist.md +134 -0
  83. claude_kit/_payload/skills/_references/testing-patterns.md +236 -0
  84. claude_kit/_payload/skills/accessibility-review/SKILL.md +56 -0
  85. claude_kit/_payload/skills/api-and-interface-design/SKILL.md +294 -0
  86. claude_kit/_payload/skills/api-integration/SKILL.md +348 -0
  87. claude_kit/_payload/skills/archive-sprint/SKILL.md +31 -0
  88. claude_kit/_payload/skills/backlog/SKILL.md +41 -0
  89. claude_kit/_payload/skills/backlog/item-template.md +20 -0
  90. claude_kit/_payload/skills/browser-testing-with-devtools/SKILL.md +302 -0
  91. claude_kit/_payload/skills/ci-cd-and-automation/SKILL.md +402 -0
  92. claude_kit/_payload/skills/code-review-and-quality/SKILL.md +347 -0
  93. claude_kit/_payload/skills/code-simplification/SKILL.md +331 -0
  94. claude_kit/_payload/skills/component-design/SKILL.md +171 -0
  95. claude_kit/_payload/skills/consolidate-learnings/SKILL.md +55 -0
  96. claude_kit/_payload/skills/context-engineering/SKILL.md +321 -0
  97. claude_kit/_payload/skills/debugging-and-error-recovery/SKILL.md +300 -0
  98. claude_kit/_payload/skills/decision/SKILL.md +46 -0
  99. claude_kit/_payload/skills/decision/adr-template.md +36 -0
  100. claude_kit/_payload/skills/deprecation-and-migration/SKILL.md +207 -0
  101. claude_kit/_payload/skills/documentation-and-adrs/SKILL.md +299 -0
  102. claude_kit/_payload/skills/doubt-driven-development/SKILL.md +243 -0
  103. claude_kit/_payload/skills/execute/SKILL.md +27 -0
  104. claude_kit/_payload/skills/frontend-ui-engineering/SKILL.md +328 -0
  105. claude_kit/_payload/skills/git-workflow-and-versioning/SKILL.md +300 -0
  106. claude_kit/_payload/skills/idea-refine/SKILL.md +178 -0
  107. claude_kit/_payload/skills/idea-refine/examples.md +238 -0
  108. claude_kit/_payload/skills/idea-refine/frameworks.md +99 -0
  109. claude_kit/_payload/skills/idea-refine/refinement-criteria.md +113 -0
  110. claude_kit/_payload/skills/idea-refine/scripts/idea-refine.sh +15 -0
  111. claude_kit/_payload/skills/incident-postmortem/SKILL.md +74 -0
  112. claude_kit/_payload/skills/incremental-implementation/SKILL.md +245 -0
  113. claude_kit/_payload/skills/interview-me/SKILL.md +221 -0
  114. claude_kit/_payload/skills/load-testing/SKILL.md +83 -0
  115. claude_kit/_payload/skills/manual-test/SKILL.md +516 -0
  116. claude_kit/_payload/skills/performance-optimization/SKILL.md +277 -0
  117. claude_kit/_payload/skills/planning-and-task-breakdown/SKILL.md +223 -0
  118. claude_kit/_payload/skills/playwright-verification/SKILL.md +205 -0
  119. claude_kit/_payload/skills/refresh-docs/SKILL.md +63 -0
  120. claude_kit/_payload/skills/remember/SKILL.md +96 -0
  121. claude_kit/_payload/skills/scope/SKILL.md +52 -0
  122. claude_kit/_payload/skills/scope/scope-template.md +82 -0
  123. claude_kit/_payload/skills/sdlc/SKILL.md +83 -0
  124. claude_kit/_payload/skills/security-and-hardening/SKILL.md +368 -0
  125. claude_kit/_payload/skills/security-verification/SKILL.md +209 -0
  126. claude_kit/_payload/skills/shipping-and-launch/SKILL.md +309 -0
  127. claude_kit/_payload/skills/smoke-test/SKILL.md +78 -0
  128. claude_kit/_payload/skills/source-driven-development/SKILL.md +195 -0
  129. claude_kit/_payload/skills/spec-driven-development/SKILL.md +200 -0
  130. claude_kit/_payload/skills/sprint/SKILL.md +67 -0
  131. claude_kit/_payload/skills/sprint/sprint-template.md +90 -0
  132. claude_kit/_payload/skills/test-driven-development/SKILL.md +383 -0
  133. claude_kit/_payload/skills/threat-model/SKILL.md +60 -0
  134. claude_kit/_payload/skills/triage/SKILL.md +87 -0
  135. claude_kit/_payload/skills/ui-ux-design/SKILL.md +71 -0
  136. claude_kit/_payload/skills/unit-test/SKILL.md +237 -0
  137. claude_kit/_payload/skills/using-agent-skills/SKILL.md +180 -0
  138. claude_kit/_payload/templates/CLAUDE.md +238 -0
  139. claude_kit/_payload/templates/CLAUDE.stack.md.tmpl +53 -0
  140. claude_kit/_payload/templates/CONTINUITY.template.md +35 -0
  141. claude_kit/_payload/templates/README.claude-sdlc.md.tmpl +219 -0
  142. claude_kit/_payload/templates/agent-memory/MEMORY.md +30 -0
  143. claude_kit/_payload/templates/agent-memory/api/.gitkeep +0 -0
  144. claude_kit/_payload/templates/agent-memory/architecture/.gitkeep +0 -0
  145. claude_kit/_payload/templates/agent-memory/debugging/.gitkeep +0 -0
  146. claude_kit/_payload/templates/agent-memory/gotchas/.gitkeep +0 -0
  147. claude_kit/_payload/templates/agent-memory/patterns/.gitkeep +0 -0
  148. claude_kit/_payload/templates/agent-memory/performance/.gitkeep +0 -0
  149. claude_kit/_payload/templates/artifacts/adr.md +18 -0
  150. claude_kit/_payload/templates/artifacts/feature-spec.md +29 -0
  151. claude_kit/_payload/templates/artifacts/release-plan.md +23 -0
  152. claude_kit/_payload/templates/artifacts/runbook.md +24 -0
  153. claude_kit/_payload/templates/artifacts/security-review.md +23 -0
  154. claude_kit/_payload/templates/artifacts/test-plan.md +22 -0
  155. claude_kit/_payload/templates/org/README.md +53 -0
  156. claude_kit/_payload/templates/org/agents/data-workflow-agent.md +59 -0
  157. claude_kit/_payload/templates/org/agents/founder-prototype-agent.md +61 -0
  158. claude_kit/_payload/templates/org/agents/internal-tools-builder.md +63 -0
  159. claude_kit/_payload/templates/org/agents/pm-copilot.md +60 -0
  160. claude_kit/_payload/templates/org/agents/support-ticket-engineer.md +63 -0
  161. claude_kit/_payload/templates/org/packs/devops-and-release/README.md +46 -0
  162. claude_kit/_payload/templates/org/packs/devops-and-release/pack.yaml +32 -0
  163. claude_kit/_payload/templates/org/packs/engineering-core/README.md +46 -0
  164. claude_kit/_payload/templates/org/packs/engineering-core/pack.yaml +44 -0
  165. claude_kit/_payload/templates/org/packs/non-engineer-builder/README.md +53 -0
  166. claude_kit/_payload/templates/org/packs/non-engineer-builder/pack.yaml +39 -0
  167. claude_kit/_payload/templates/org/packs/onboarding-and-docs/README.md +49 -0
  168. claude_kit/_payload/templates/org/packs/onboarding-and-docs/pack.yaml +26 -0
  169. claude_kit/_payload/templates/org/packs/product-to-code/README.md +50 -0
  170. claude_kit/_payload/templates/org/packs/product-to-code/pack.yaml +34 -0
  171. claude_kit/_payload/templates/org/packs/quality-and-review/README.md +53 -0
  172. claude_kit/_payload/templates/org/packs/quality-and-review/pack.yaml +40 -0
  173. claude_kit/_payload/templates/org/packs/security-and-compliance/README.md +50 -0
  174. claude_kit/_payload/templates/org/packs/security-and-compliance/pack.yaml +36 -0
  175. claude_kit/_payload/templates/org/rules/ai-working-agreement.md +45 -0
  176. claude_kit/_payload/templates/org/rules/ambiguity-resolution.md +36 -0
  177. claude_kit/_payload/templates/org/rules/branch-and-pr-policy.md +41 -0
  178. claude_kit/_payload/templates/org/rules/compliance-policy.md +50 -0
  179. claude_kit/_payload/templates/org/rules/non-engineer-safe-coding.md +37 -0
  180. claude_kit/_payload/templates/org/rules/pii-policy.md +46 -0
  181. claude_kit/_payload/templates/org/rules/production-data-policy.md +35 -0
  182. claude_kit/_payload/templates/org/rules/prompt-to-task-conversion.md +30 -0
  183. claude_kit/_payload/templates/org/rules/prototype-boundaries.md +40 -0
  184. claude_kit/_payload/templates/org/rules/secrets-policy.md +34 -0
  185. claude_kit/_payload/templates/org/skills/customer-issue-to-fix/SKILL.md +61 -0
  186. claude_kit/_payload/templates/org/skills/feature-from-idea/SKILL.md +56 -0
  187. claude_kit/_payload/templates/org/skills/prompt-to-safe-task/SKILL.md +59 -0
  188. claude_kit/_payload/templates/org/skills/prototype-to-production/SKILL.md +61 -0
  189. claude_kit/_payload/templates/org/skills/repo-onboarding/SKILL.md +60 -0
  190. claude_kit/_payload/templates/settings.json +53 -0
  191. claude_kit/_payload/templates/stacks/backend/python/fastapi/rules/fastapi-patterns.md +64 -0
  192. claude_kit/_payload/templates/stacks/db/mongodb/agents/migration-specialist.md +61 -0
  193. claude_kit/_payload/templates/stacks/db/mongodb/agents/mongodb-specialist.md +59 -0
  194. claude_kit/_payload/templates/stacks/db/mongodb/rules/mongodb-patterns.md +39 -0
  195. claude_kit/_payload/templates/stacks/db/postgres/agents/db-performance-reviewer.md +66 -0
  196. claude_kit/_payload/templates/stacks/db/postgres/agents/migration-specialist.md +56 -0
  197. claude_kit/_payload/templates/stacks/db/postgres/agents/postgres-specialist.md +58 -0
  198. claude_kit/_payload/templates/stacks/db/postgres/rules/database-performance.md +64 -0
  199. claude_kit/_payload/templates/stacks/db/postgres/rules/postgres-patterns.md +43 -0
  200. claude_kit/_payload/templates/stacks/frontend/react/rules/react-patterns.md +63 -0
  201. claude_kit/catalog.py +476 -0
  202. claude_kit/cli.py +327 -0
  203. claude_kit/hooks.py +246 -0
  204. claude_kit/models.py +205 -0
  205. claude_kit/prompts.py +209 -0
  206. claude_kit/render.py +146 -0
  207. claude_kit/scaffold.py +492 -0
  208. claude_kit/upgrader.py +294 -0
  209. claude_kit/validator.py +197 -0
claude_kit/models.py ADDED
@@ -0,0 +1,205 @@
1
+ """Typed data structures for claude-kit's catalog-driven scaffolder.
2
+
3
+ These dataclasses are the contract between the prompt layer (:mod:`claude_kit.prompts`), the
4
+ catalog resolver (:mod:`claude_kit.catalog`), and the installer (:mod:`claude_kit.scaffold`).
5
+ Using explicit types (rather than loose dicts) honours the kit's own "no bare container types"
6
+ documentation rule and keeps ``init-options.json`` round-trippable for ``validate``/``upgrade``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Any
13
+
14
+ #: Schema version of the persisted ``.claude/config/init-options.json`` document.
15
+ INIT_OPTIONS_SCHEMA = 1
16
+
17
+
18
+ @dataclass
19
+ class Selection:
20
+ """A fully-resolved set of user choices from ``init`` (prompts, ``--defaults``, or ``--config``).
21
+
22
+ Attributes:
23
+ frontend_framework: Frontend framework id (e.g. ``"react"``).
24
+ frontend_language: Frontend language id (e.g. ``"typescript"``).
25
+ backend_language: Backend language id (e.g. ``"python"``).
26
+ backend_framework: Backend framework id (e.g. ``"fastapi"``).
27
+ database: Database id (``"postgres"`` or ``"mongodb"``).
28
+ profile: SDLC profile id (``"lean"``/``"standard"``/``"enterprise"``).
29
+ mcp: Selected MCP server ids (empty means no ``.mcp.json`` is written).
30
+ scope: Usage scope (``"individual"``/``"team"``/``"organization"``). Only ``organization``
31
+ installs the org capability layer (packs, persona agents, org rules, autonomy hooks).
32
+ teams: Teams adopting the config (organization scope only; personalises the generated README).
33
+ autonomy: Autonomy level (``advisory``/``assisted``/``autonomous-local``/``autonomous-pr``/
34
+ ``enterprise-controlled``); higher levels enable more guardrail hooks. Prompted only in
35
+ organization scope; defaults to ``assisted`` everywhere else.
36
+ review_strictness: Review strictness (``light``/``standard``/``regulated``); ``regulated``
37
+ adds extra gates/hooks. Prompted only in organization scope.
38
+ org_packs: Whether to generate the reusable org capability packs (organization scope only).
39
+ """
40
+
41
+ frontend_framework: str
42
+ frontend_language: str
43
+ backend_language: str
44
+ backend_framework: str
45
+ database: str
46
+ profile: str
47
+ mcp: list[str] = field(default_factory=list)
48
+ scope: str = "team"
49
+ teams: list[str] = field(default_factory=list)
50
+ autonomy: str = "assisted"
51
+ review_strictness: str = "standard"
52
+ org_packs: bool = True
53
+
54
+ def to_dict(self) -> dict[str, Any]:
55
+ """Return a JSON/YAML-serialisable mapping of this selection."""
56
+ return asdict(self)
57
+
58
+ @classmethod
59
+ def from_dict(cls, data: dict[str, Any]) -> Selection:
60
+ """Build a :class:`Selection` from a mapping, ignoring unknown keys.
61
+
62
+ Args:
63
+ data: A mapping with the selection fields (e.g. parsed from ``--config``). Org fields
64
+ may be absent in older documents; their dataclass defaults apply (back-compatible).
65
+
66
+ Returns:
67
+ A populated :class:`Selection`.
68
+ """
69
+ known = {f for f in cls.__dataclass_fields__} # type: ignore[attr-defined]
70
+ kwargs = {k: v for k, v in data.items() if k in known}
71
+ kwargs.setdefault("mcp", [])
72
+ return cls(**kwargs)
73
+
74
+
75
+ @dataclass
76
+ class OrgPlan:
77
+ """The resolved organization capability layer (only present when ``scope == organization``).
78
+
79
+ Produced by :func:`claude_kit.catalog.resolve` from ``catalog/org.yaml`` and consumed by
80
+ :func:`claude_kit.scaffold.install_sdlc` (its ``_install_org`` step). The new skills/agents/rules
81
+ install into the standard auto-discovered ``.claude/{skills,agents,rules}`` dirs; the packs install
82
+ as manifests under ``.claude/org-packs/``. Autonomy hooks are merged into :attr:`ResolvedPlan.hooks`
83
+ so they flow through the normal settings assembly.
84
+
85
+ Attributes:
86
+ scope: The usage scope (always ``"organization"`` here).
87
+ teams: Teams adopting the config (personalises the generated README).
88
+ autonomy: The chosen autonomy level id.
89
+ autonomy_policy: One-line human-readable policy for the chosen autonomy level.
90
+ review_strictness: The chosen review-strictness id.
91
+ packs: Org-pack ids whose manifests install under ``.claude/org-packs/``.
92
+ org_skills: New skill dir names to copy from ``templates/org/skills/`` into ``.claude/skills/``.
93
+ org_agents: New persona agent names to copy from ``templates/org/agents/`` into ``.claude/agents/``.
94
+ org_rules: New rule filenames to copy from ``templates/org/rules/`` into ``.claude/rules/``.
95
+ added_hooks: Hook ids added by the autonomy level / strictness (merged into the plan's hooks).
96
+ added_agents: Core agent names the org layer activates regardless of profile (e.g.
97
+ ``risk-classifier``; installed via the normal core-agent path, so merged into the plan's
98
+ ``agents``).
99
+ extra_gates: Quality-gate ids added by the chosen review strictness (merged into the plan's
100
+ ``gates``).
101
+ """
102
+
103
+ scope: str
104
+ teams: list[str]
105
+ autonomy: str
106
+ autonomy_policy: str
107
+ review_strictness: str
108
+ packs: list[str]
109
+ org_skills: list[str]
110
+ org_agents: list[str]
111
+ org_rules: list[str]
112
+ added_hooks: list[str]
113
+ added_agents: list[str] = field(default_factory=list)
114
+ extra_gates: list[str] = field(default_factory=list)
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ """Return a JSON/YAML-serialisable mapping of this org plan."""
118
+ return asdict(self)
119
+
120
+
121
+ @dataclass
122
+ class ResolvedPlan:
123
+ """The concrete install plan produced by :func:`claude_kit.catalog.resolve`.
124
+
125
+ Attributes:
126
+ selection: The originating :class:`Selection`.
127
+ agents: Core agent names to install (profile subset, ∪ org core agents in organization scope).
128
+ skills: Skill directory names to install (profile subset ∪ stack-suggested).
129
+ overlay_rules: Overlay rule filenames to copy from the selected stacks.
130
+ overlay_agents: Overlay agent names to copy from the selected stacks.
131
+ hooks: Hook ids to enable (drives copied scripts + assembled ``settings.json``).
132
+ gates: Quality-gate ids active for the chosen profile (∪ strictness gates in org scope).
133
+ mcp_servers: Mapping of selected MCP server id to its ``.mcp.json`` config fragment.
134
+ context: Flat string context for rendering ``CLAUDE.md`` / ``README`` (labels + commands).
135
+ stack_dirs: Mapping of selected stack kind to its ``templates/stacks`` subdir.
136
+ org: The resolved org capability layer, or ``None`` for individual/team scope.
137
+ """
138
+
139
+ selection: Selection
140
+ agents: list[str]
141
+ skills: list[str]
142
+ overlay_rules: list[str]
143
+ overlay_agents: list[str]
144
+ hooks: list[str]
145
+ gates: list[str]
146
+ mcp_servers: dict[str, dict[str, Any]]
147
+ context: dict[str, str]
148
+ stack_dirs: dict[str, str]
149
+ org: OrgPlan | None = None
150
+
151
+
152
+ @dataclass
153
+ class FileRecord:
154
+ """A single installed file tracked in ``init-options.json`` for safe upgrades.
155
+
156
+ Attributes:
157
+ path: Path relative to the project root (POSIX separators).
158
+ sha256: Hex SHA-256 of the file contents at install time.
159
+ owner: One of ``"kit"`` (refreshed on upgrade), ``"overlay"`` (follows the selection),
160
+ or ``"user-editable"`` (never clobbered).
161
+ """
162
+
163
+ path: str
164
+ sha256: str
165
+ owner: str
166
+
167
+ def to_dict(self) -> dict[str, str]:
168
+ """Return a JSON-serialisable mapping of this record."""
169
+ return asdict(self)
170
+
171
+
172
+ @dataclass
173
+ class InitOptions:
174
+ """The persisted ``.claude/config/init-options.json`` document.
175
+
176
+ Attributes:
177
+ claude_kit_version: Kit version that produced the install.
178
+ selection: The user's resolved choices.
179
+ files: Per-file checksum + ownership records (drives ``diff``/``upgrade``).
180
+ schema_version: Document schema version (:data:`INIT_OPTIONS_SCHEMA`).
181
+ """
182
+
183
+ claude_kit_version: str
184
+ selection: Selection
185
+ files: list[FileRecord]
186
+ schema_version: int = INIT_OPTIONS_SCHEMA
187
+
188
+ def to_dict(self) -> dict[str, Any]:
189
+ """Return a JSON-serialisable mapping (checksums excluded from no field)."""
190
+ return {
191
+ "schema_version": self.schema_version,
192
+ "claude_kit_version": self.claude_kit_version,
193
+ "selection": self.selection.to_dict(),
194
+ "files": [r.to_dict() for r in self.files],
195
+ }
196
+
197
+ @classmethod
198
+ def from_dict(cls, data: dict[str, Any]) -> InitOptions:
199
+ """Reconstruct :class:`InitOptions` from a parsed ``init-options.json`` mapping."""
200
+ return cls(
201
+ claude_kit_version=str(data.get("claude_kit_version", "")),
202
+ selection=Selection.from_dict(data.get("selection", {})),
203
+ files=[FileRecord(**r) for r in data.get("files", [])],
204
+ schema_version=int(data.get("schema_version", INIT_OPTIONS_SCHEMA)),
205
+ )
claude_kit/prompts.py ADDED
@@ -0,0 +1,209 @@
1
+ """Interactive (and non-interactive) selection of an init configuration.
2
+
3
+ Produces a :class:`~claude_kit.models.Selection` three ways: ordered interactive prompts, the
4
+ catalog defaults (``--defaults``), or a YAML config file (``--config``). The prompt order matches
5
+ the spec: frontend framework → frontend language → backend language → backend framework → database →
6
+ SDLC profile → MCP integrations. (The target path is handled by the CLI before prompting.)
7
+
8
+ I/O uses ``input``/``print`` so it is trivially testable via a Typer ``CliRunner(input=...)`` or by
9
+ monkeypatching ``builtins.input``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import yaml
18
+
19
+ from claude_kit import catalog
20
+ from claude_kit.models import Selection
21
+
22
+
23
+ def _ask(prompt: str, default: str) -> str:
24
+ """Prompt for a single value with a default, tolerant of EOF (non-interactive) input."""
25
+ try:
26
+ resp = input(f"{prompt} [{default}]: ").strip()
27
+ except EOFError:
28
+ return default
29
+ return resp or default
30
+
31
+
32
+ def _choose_one(title: str, options: list[dict[str, Any]], default: str) -> str:
33
+ """Render a numbered menu of live options (planned ones shown but not selectable).
34
+
35
+ Args:
36
+ title: Section heading.
37
+ options: Each dict has ``id``, ``label`` and may have ``status`` (``"planned"`` = disabled).
38
+ default: The default option id (must be live).
39
+
40
+ Returns:
41
+ The chosen option id.
42
+ """
43
+ live = [o for o in options if o.get("status", "live") != "planned"]
44
+ print(f"\n{title}")
45
+ for n, o in enumerate(live, 1):
46
+ mark = " (default)" if o["id"] == default else ""
47
+ print(f" {n}) {o['label']}{mark}")
48
+ for o in options:
49
+ if o.get("status") == "planned":
50
+ print(f" -) {o['label']} (coming soon)")
51
+ valid = {o["id"] for o in live}
52
+ while True:
53
+ resp = _ask(" choose", default)
54
+ if resp in valid:
55
+ return resp
56
+ if resp.isdigit() and 1 <= int(resp) <= len(live):
57
+ return live[int(resp) - 1]["id"]
58
+ print(" please enter one of the listed ids or numbers")
59
+
60
+
61
+ def _ask_bool(prompt: str, default: bool) -> bool:
62
+ """Prompt for a yes/no answer with a default, tolerant of EOF (non-interactive) input."""
63
+ resp = _ask(f"{prompt} [y/n]", "y" if default else "n").strip().lower()
64
+ if resp in ("y", "yes", "true", "1"):
65
+ return True
66
+ if resp in ("n", "no", "false", "0"):
67
+ return False
68
+ return default
69
+
70
+
71
+ def _choose_many(title: str, options: list[dict[str, Any]]) -> list[str]:
72
+ """Render a menu and read a comma/space-separated multi-selection (empty = none)."""
73
+ print(f"\n{title} (comma-separated ids or numbers; empty = none)")
74
+ for n, o in enumerate(options, 1):
75
+ print(f" {n}) {o['id']} — {o['label']}")
76
+ resp = _ask(" select", "none")
77
+ if resp.lower() in ("", "none"):
78
+ return []
79
+ chosen: list[str] = []
80
+ by_id = {o["id"]: o["id"] for o in options}
81
+ for tok in resp.replace(",", " ").split():
82
+ if tok in by_id:
83
+ chosen.append(tok)
84
+ elif tok.isdigit() and 1 <= int(tok) <= len(options):
85
+ chosen.append(options[int(tok) - 1]["id"])
86
+ else:
87
+ print(f" (ignoring unknown selection: {tok})")
88
+ # de-dup, preserve order
89
+ seen: set[str] = set()
90
+ return [c for c in chosen if not (c in seen or seen.add(c))]
91
+
92
+
93
+ def interactive(payload_root: str | Path) -> Selection:
94
+ """Run the ordered prompts and return the chosen :class:`Selection`."""
95
+ opts = catalog.list_options(payload_root)
96
+ dflt = catalog.defaults(payload_root)
97
+
98
+ fe = _choose_one("Frontend framework", opts["frontend"], dflt.frontend_framework)
99
+ fe_entry = next(o for o in opts["frontend"] if o["id"] == fe)
100
+ langs = fe_entry.get("languages", []) or ["typescript"]
101
+ lang_options = [{"id": lang_id, "label": lang_id} for lang_id in langs]
102
+ fe_lang = _choose_one(
103
+ "Frontend language",
104
+ lang_options,
105
+ fe_entry.get("default_language", "typescript"),
106
+ )
107
+
108
+ be = _choose_one("Backend language", opts["backend"], dflt.backend_language)
109
+ be_entry = next(o for o in opts["backend"] if o["id"] == be)
110
+ be_fw = _choose_one(
111
+ "Backend framework",
112
+ be_entry.get("frameworks", []),
113
+ be_entry.get("default_framework", ""),
114
+ )
115
+
116
+ db = _choose_one("Database", opts["database"], dflt.database)
117
+ profile = _choose_one("SDLC profile", opts["profiles"], dflt.profile)
118
+ mcp = _choose_many("Optional MCP integrations", opts["mcp"])
119
+
120
+ # Usage scope — and, for organizations, the capability-layer questions.
121
+ org = catalog.org_options(payload_root)
122
+ scope = _choose_one("Usage scope", org["scopes"], org["defaults"]["scope"])
123
+ teams: list[str] = []
124
+ autonomy = org["defaults"]["autonomy"]
125
+ review_strictness = org["defaults"]["strictness"]
126
+ org_packs = True
127
+ if scope == "organization":
128
+ teams = _choose_many("Which teams will use this?", org["teams"])
129
+ autonomy = _choose_one("Autonomy level", org["autonomy"], autonomy)
130
+ review_strictness = _choose_one(
131
+ "Review strictness", org["strictness"], review_strictness
132
+ )
133
+ org_packs = _ask_bool("Generate reusable org capability packs?", True)
134
+
135
+ return Selection(
136
+ frontend_framework=fe,
137
+ frontend_language=fe_lang,
138
+ backend_language=be,
139
+ backend_framework=be_fw,
140
+ database=db,
141
+ profile=profile,
142
+ mcp=mcp,
143
+ scope=scope,
144
+ teams=teams,
145
+ autonomy=autonomy,
146
+ review_strictness=review_strictness,
147
+ org_packs=org_packs,
148
+ )
149
+
150
+
151
+ def from_config(config_path: str | Path, payload_root: str | Path) -> Selection:
152
+ """Build a :class:`Selection` from a YAML config file (``--config``).
153
+
154
+ Accepts either flat keys (matching :class:`Selection` fields) or a friendly nested form::
155
+
156
+ frontend: { framework: react, language: typescript }
157
+ backend: { language: python, framework: fastapi }
158
+ database: postgres
159
+ profile: standard
160
+ mcp: [github]
161
+ scope: organization
162
+ org: { teams: [engineering, product], autonomy: autonomous-pr,
163
+ review_strictness: regulated, packs: true }
164
+
165
+ Org fields may also be given flat (``scope``/``teams``/``autonomy``/``review_strictness``/
166
+ ``org_packs``). Missing keys fall back to the catalog defaults.
167
+ """
168
+ data = yaml.safe_load(Path(config_path).read_text(encoding="utf-8")) or {}
169
+ if not isinstance(data, dict):
170
+ raise ValueError("config file did not parse to a mapping")
171
+ dflt = catalog.defaults(payload_root)
172
+ org_defaults = catalog.org_options(payload_root)["defaults"]
173
+
174
+ fe = data.get("frontend", {})
175
+ be = data.get("backend", {})
176
+ org = data.get("org", {})
177
+ if not isinstance(org, dict):
178
+ org = {}
179
+ flat = {
180
+ "frontend_framework": data.get("frontend_framework")
181
+ or (fe.get("framework") if isinstance(fe, dict) else fe)
182
+ or dflt.frontend_framework,
183
+ "frontend_language": data.get("frontend_language")
184
+ or (fe.get("language") if isinstance(fe, dict) else None)
185
+ or dflt.frontend_language,
186
+ "backend_language": data.get("backend_language")
187
+ or (be.get("language") if isinstance(be, dict) else be)
188
+ or dflt.backend_language,
189
+ "backend_framework": data.get("backend_framework")
190
+ or (be.get("framework") if isinstance(be, dict) else None)
191
+ or dflt.backend_framework,
192
+ "database": data.get("database") or dflt.database,
193
+ "profile": data.get("profile") or dflt.profile,
194
+ "mcp": data.get("mcp") or [],
195
+ "scope": data.get("scope") or org_defaults["scope"],
196
+ "teams": data.get("teams") or org.get("teams") or [],
197
+ "autonomy": data.get("autonomy")
198
+ or org.get("autonomy")
199
+ or org_defaults["autonomy"],
200
+ "review_strictness": data.get("review_strictness")
201
+ or org.get("review_strictness")
202
+ or org_defaults["strictness"],
203
+ }
204
+ # org_packs / org.packs: accept an explicit bool, else default True.
205
+ packs = data.get("org_packs")
206
+ if packs is None:
207
+ packs = org.get("packs")
208
+ flat["org_packs"] = True if packs is None else bool(packs)
209
+ return Selection.from_dict(flat)
claude_kit/render.py ADDED
@@ -0,0 +1,146 @@
1
+ """Jinja2-backed template renderer for claude-kit.
2
+
3
+ Design goals (unchanged from the original stdlib renderer, now powered by Jinja2):
4
+
5
+ * **Never corrupt literal braces.** Only files whose name ends in ``.tmpl`` are rendered; every
6
+ other file is copied byte-for-byte. So any literal ``{{``/``{%`` in non-template files (JSON
7
+ examples, shell, etc.) is left exactly as written.
8
+ * **Ship dotfiles reliably.** A path segment named ``dot__foo`` is written as ``.foo``
9
+ (``dot__gitignore`` -> ``.gitignore``). This keeps real dotfiles out of the template tree (where
10
+ some packaging tools silently drop them) and greppable in the repo.
11
+ * **Fail loud on a missing value.** The Jinja environment uses ``StrictUndefined``; an undefined
12
+ placeholder raises (surfaced as :class:`KeyError`) rather than rendering a blank.
13
+
14
+ Substitution syntax is Jinja2 (``{{ name }}``), so existing ``.tmpl`` files work unchanged.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import shutil
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from jinja2 import Environment, StrictUndefined
24
+ from jinja2.exceptions import UndefinedError
25
+
26
+ #: Suffix marking a file as a template (stripped from the rendered output name).
27
+ TEMPLATE_SUFFIX = ".tmpl"
28
+
29
+ #: Prefix marking a path segment that should become a dotfile/dotdir on output.
30
+ DOTFILE_PREFIX = "dot__"
31
+
32
+ #: Shared Jinja environment. ``keep_trailing_newline`` preserves files' final newline; autoescape is
33
+ #: off because we render Markdown/JSON/text, never HTML.
34
+ _ENV = Environment(
35
+ undefined=StrictUndefined,
36
+ keep_trailing_newline=True,
37
+ autoescape=False,
38
+ trim_blocks=False,
39
+ lstrip_blocks=False,
40
+ )
41
+
42
+ #: Directory names never copied from a template tree (build droppings / VCS / vendored deps).
43
+ _IGNORE_DIRS = frozenset(
44
+ {
45
+ "__pycache__",
46
+ ".pytest_cache",
47
+ ".ruff_cache",
48
+ ".mypy_cache",
49
+ ".git",
50
+ ".venv",
51
+ "venv",
52
+ "node_modules",
53
+ "dist",
54
+ "coverage",
55
+ ".DS_Store",
56
+ }
57
+ )
58
+
59
+ #: File names/suffixes never copied from a template tree.
60
+ _IGNORE_FILE_SUFFIXES = (".pyc", ".pyo")
61
+ _IGNORE_FILE_NAMES = frozenset({".DS_Store"})
62
+
63
+
64
+ def _is_ignored(rel: Path) -> bool:
65
+ """Return True if ``rel`` is build/VCS junk that must never be rendered."""
66
+ if _IGNORE_DIRS & set(rel.parts):
67
+ return True
68
+ name = rel.name
69
+ return name in _IGNORE_FILE_NAMES or name.endswith(_IGNORE_FILE_SUFFIXES)
70
+
71
+
72
+ def render_text(text: str, context: dict[str, Any]) -> str:
73
+ """Render Jinja2 ``text`` against ``context``.
74
+
75
+ Args:
76
+ text: Template text (Jinja2 syntax, e.g. ``{{ name }}``).
77
+ context: Mapping of placeholder name to replacement value.
78
+
79
+ Returns:
80
+ The rendered text.
81
+
82
+ Raises:
83
+ KeyError: If the template references a name absent from ``context`` (fail-loud parity
84
+ with the previous renderer).
85
+ """
86
+ try:
87
+ return _ENV.from_string(text).render(**context)
88
+ except UndefinedError as exc: # surface as KeyError to keep the existing contract
89
+ raise KeyError(str(exc)) from exc
90
+
91
+
92
+ def _resolve_name(name: str) -> str:
93
+ """Map a single template path segment to its output name (strip ``.tmpl``, ``dot__`` -> ``.``)."""
94
+ if name.endswith(TEMPLATE_SUFFIX):
95
+ name = name[: -len(TEMPLATE_SUFFIX)]
96
+ if name.startswith(DOTFILE_PREFIX):
97
+ name = "." + name[len(DOTFILE_PREFIX) :]
98
+ return name
99
+
100
+
101
+ def _resolve_relpath(rel: Path) -> Path:
102
+ """Apply :func:`_resolve_name` to every segment of a relative path."""
103
+ return Path(*[_resolve_name(part) for part in rel.parts])
104
+
105
+
106
+ def render_tree(src: Path, dest: Path, context: dict[str, Any]) -> list[Path]:
107
+ """Render the template directory ``src`` into ``dest``.
108
+
109
+ The *contents* of ``src`` are written into ``dest``. ``*.tmpl`` files are rendered with Jinja2
110
+ and written without the suffix; all other files are copied verbatim. ``dot__``-prefixed segments
111
+ become dotfiles/dotdirs. Existing destination files are overwritten; parents are created.
112
+
113
+ Args:
114
+ src: Source template directory.
115
+ dest: Destination directory (created if missing).
116
+ context: Substitution values for ``.tmpl`` files.
117
+
118
+ Returns:
119
+ The written destination file paths (directories excluded).
120
+
121
+ Raises:
122
+ FileNotFoundError: If ``src`` is not an existing directory.
123
+ KeyError: If a ``.tmpl`` file references an unknown placeholder.
124
+ """
125
+ if not src.is_dir():
126
+ raise FileNotFoundError(f"template source not found: {src}")
127
+
128
+ written: list[Path] = []
129
+ for entry in sorted(src.rglob("*")):
130
+ rel = entry.relative_to(src)
131
+ if _is_ignored(rel):
132
+ continue
133
+ target = dest / _resolve_relpath(rel)
134
+ if entry.is_dir():
135
+ target.mkdir(parents=True, exist_ok=True)
136
+ continue
137
+ target.parent.mkdir(parents=True, exist_ok=True)
138
+ if entry.name.endswith(TEMPLATE_SUFFIX):
139
+ target.write_text(
140
+ render_text(entry.read_text(encoding="utf-8"), context),
141
+ encoding="utf-8",
142
+ )
143
+ else:
144
+ shutil.copy2(entry, target)
145
+ written.append(target)
146
+ return written