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/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
|