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/cli.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Command-line interface for claude-kit (``claude-kit`` · aliases ``ckit`` / ``claude-sdlc``).
|
|
2
|
+
|
|
3
|
+
A Cookiecutter-style scaffolder for a Claude Code **configuration** (no application code, no Docker):
|
|
4
|
+
``init`` asks ordered questions and lays down ``CLAUDE.md`` + ``.claude/`` (rules, the profile's
|
|
5
|
+
agents/skills, hooks, artifact templates, config) + an optional ``.mcp.json`` and a README. Lifecycle
|
|
6
|
+
commands — ``validate``, ``doctor``, ``diff``, ``upgrade``, ``list-options``, ``status`` — manage it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from contextlib import ExitStack
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from claude_kit import __version__, catalog, prompts, scaffold, upgrader, validator
|
|
18
|
+
|
|
19
|
+
BANNER = r"""
|
|
20
|
+
___ _ _ _ ___ ___ _ _____ _____
|
|
21
|
+
/ __| | /_\ | | | \| __| | |/ /_ _|_ _|
|
|
22
|
+
| (__| |__ / _ \| |_| | |) | _| | ' < | | | |
|
|
23
|
+
\___|____/_/ \_\\___/|___/|___| |_|\_\___| |_| autonomous SDLC config for Claude Code
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
add_completion=False,
|
|
28
|
+
no_args_is_help=False,
|
|
29
|
+
help="Scaffold and manage a Claude Code autonomous-SDLC configuration.",
|
|
30
|
+
)
|
|
31
|
+
research_app = typer.Typer(
|
|
32
|
+
no_args_is_help=True, help="Research helpers (license-respecting)."
|
|
33
|
+
)
|
|
34
|
+
app.add_typer(research_app, name="research")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _version_callback(value: bool) -> None:
|
|
38
|
+
if value:
|
|
39
|
+
typer.echo(f"claude-kit {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback(invoke_without_command=True)
|
|
44
|
+
def _root(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
version: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"-V",
|
|
49
|
+
"--version",
|
|
50
|
+
callback=_version_callback,
|
|
51
|
+
is_eager=True,
|
|
52
|
+
help="print the version",
|
|
53
|
+
),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Show the banner + help when invoked with no subcommand."""
|
|
56
|
+
if ctx.invoked_subcommand is None:
|
|
57
|
+
typer.echo(BANNER)
|
|
58
|
+
typer.echo(ctx.get_help())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _print_report(ok: bool, messages: list[str]) -> None:
|
|
62
|
+
"""Print a check report and exit non-zero on failure."""
|
|
63
|
+
for line in messages:
|
|
64
|
+
typer.echo(line)
|
|
65
|
+
if not ok:
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def init(
|
|
71
|
+
path: Optional[str] = typer.Argument(
|
|
72
|
+
None, help="target project dir (prompted if omitted; default: current dir)"
|
|
73
|
+
),
|
|
74
|
+
defaults: bool = typer.Option(
|
|
75
|
+
False, "--defaults", help="non-interactive; use catalog defaults"
|
|
76
|
+
),
|
|
77
|
+
config: Optional[str] = typer.Option(
|
|
78
|
+
None, "--config", help="non-interactive; read the selection from a YAML file"
|
|
79
|
+
),
|
|
80
|
+
force: bool = typer.Option(
|
|
81
|
+
False,
|
|
82
|
+
"--force",
|
|
83
|
+
help="overwrite existing CLAUDE.md / settings.json / .mcp.json",
|
|
84
|
+
),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Scaffold a Claude Code SDLC configuration into a project."""
|
|
87
|
+
non_interactive = defaults or config is not None
|
|
88
|
+
with ExitStack() as stack:
|
|
89
|
+
src = scaffold.payload_dir(stack)
|
|
90
|
+
|
|
91
|
+
# 1) Target path.
|
|
92
|
+
if path is None:
|
|
93
|
+
raw = "." if non_interactive else input("Target path [.]: ").strip() or "."
|
|
94
|
+
else:
|
|
95
|
+
raw = path
|
|
96
|
+
target = Path(raw).expanduser().resolve()
|
|
97
|
+
if not target.exists():
|
|
98
|
+
if not non_interactive and not typer.confirm(
|
|
99
|
+
f"Create {target}?", default=True
|
|
100
|
+
):
|
|
101
|
+
typer.echo("aborted.")
|
|
102
|
+
raise typer.Exit(0)
|
|
103
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
# 2) Existing .claude handling: merge / overwrite / backup / abort.
|
|
106
|
+
overwrite = force
|
|
107
|
+
if (target / ".claude").exists():
|
|
108
|
+
if force:
|
|
109
|
+
mode = "overwrite"
|
|
110
|
+
elif non_interactive:
|
|
111
|
+
mode = "merge"
|
|
112
|
+
else:
|
|
113
|
+
mode = (
|
|
114
|
+
typer.prompt(
|
|
115
|
+
".claude already exists — [merge/overwrite/backup/abort]",
|
|
116
|
+
default="merge",
|
|
117
|
+
)
|
|
118
|
+
.strip()
|
|
119
|
+
.lower()
|
|
120
|
+
)
|
|
121
|
+
if mode == "abort":
|
|
122
|
+
typer.echo("aborted — nothing changed.")
|
|
123
|
+
raise typer.Exit(0)
|
|
124
|
+
if mode == "overwrite":
|
|
125
|
+
overwrite = True
|
|
126
|
+
if mode == "backup":
|
|
127
|
+
n = 1
|
|
128
|
+
while (target / f".claude.bak-{n}").exists():
|
|
129
|
+
n += 1
|
|
130
|
+
(target / ".claude").rename(target / f".claude.bak-{n}")
|
|
131
|
+
typer.echo(f" • backed up existing .claude/ -> .claude.bak-{n}")
|
|
132
|
+
|
|
133
|
+
# 3) Resolve the selection.
|
|
134
|
+
try:
|
|
135
|
+
if config is not None:
|
|
136
|
+
selection = prompts.from_config(config, src)
|
|
137
|
+
elif defaults:
|
|
138
|
+
selection = catalog.defaults(src)
|
|
139
|
+
else:
|
|
140
|
+
selection = prompts.interactive(src)
|
|
141
|
+
plan = catalog.resolve(src, selection)
|
|
142
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
143
|
+
typer.echo(f"error: {exc}", err=True)
|
|
144
|
+
raise typer.Exit(2) from exc
|
|
145
|
+
|
|
146
|
+
# 4) Install.
|
|
147
|
+
typer.echo(f"\nclaude-kit: installing into {target}")
|
|
148
|
+
for line in scaffold.install_sdlc(src, target, plan, force=overwrite):
|
|
149
|
+
typer.echo(line)
|
|
150
|
+
|
|
151
|
+
typer.echo(
|
|
152
|
+
"\nDone. Open the project in Claude Code and run `/sdlc <your task>` to start the pipeline."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def validate(
|
|
158
|
+
path: str = typer.Argument(".", help="target project dir (default: .)"),
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Structurally validate a scaffolded .claude/ configuration."""
|
|
161
|
+
_print_report(*validator.validate(path))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command()
|
|
165
|
+
def doctor(
|
|
166
|
+
path: str = typer.Argument(".", help="target project dir (default: .)"),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Run validation plus environment/health checks with fix hints."""
|
|
169
|
+
_print_report(*validator.doctor(path))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def diff(
|
|
174
|
+
path: str = typer.Argument(".", help="target project dir (default: .)"),
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Preview what an upgrade would change (no writes)."""
|
|
177
|
+
_print_report(*upgrader.diff(path))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
def upgrade(
|
|
182
|
+
path: str = typer.Argument(".", help="target project dir (default: .)"),
|
|
183
|
+
force: bool = typer.Option(
|
|
184
|
+
False, "--force", help="overwrite user-modified kit files"
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Refresh kit-owned files, backing up user-modified ones."""
|
|
188
|
+
_print_report(*upgrader.upgrade(path, force=force))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command("list-options")
|
|
192
|
+
def list_options() -> None:
|
|
193
|
+
"""List the available frontend/backend/database/profile/MCP options from the catalog."""
|
|
194
|
+
with ExitStack() as stack:
|
|
195
|
+
src = scaffold.payload_dir(stack)
|
|
196
|
+
opts = catalog.list_options(src)
|
|
197
|
+
|
|
198
|
+
def _badge(entry: dict) -> str:
|
|
199
|
+
return "" if entry.get("status", "live") == "live" else " (coming soon)"
|
|
200
|
+
|
|
201
|
+
typer.echo("\nFrontend frameworks:")
|
|
202
|
+
for fe in opts["frontend"]:
|
|
203
|
+
langs = ", ".join(fe.get("languages", [])) or "—"
|
|
204
|
+
typer.echo(f" • {fe['id']}: {fe['label']}{_badge(fe)} [languages: {langs}]")
|
|
205
|
+
typer.echo("\nBackend languages & frameworks:")
|
|
206
|
+
for be in opts["backend"]:
|
|
207
|
+
typer.echo(f" • {be['id']}: {be['label']}{_badge(be)}")
|
|
208
|
+
for fw in be["frameworks"]:
|
|
209
|
+
typer.echo(f" - {fw['id']}: {fw['label']}{_badge(fw)}")
|
|
210
|
+
typer.echo("\nDatabases:")
|
|
211
|
+
for db in opts["database"]:
|
|
212
|
+
typer.echo(f" • {db['id']}: {db['label']}")
|
|
213
|
+
typer.echo("\nSDLC profiles:")
|
|
214
|
+
for pr in opts["profiles"]:
|
|
215
|
+
typer.echo(f" • {pr['id']}: {pr['label']}")
|
|
216
|
+
typer.echo("\nMCP integrations (optional):")
|
|
217
|
+
for mc in opts["mcp"]:
|
|
218
|
+
typer.echo(f" • {mc['id']}: {mc['label']}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command()
|
|
222
|
+
def status(
|
|
223
|
+
path: str = typer.Argument(".", help="target project dir (default: .)"),
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Show what's installed and the current working memory."""
|
|
226
|
+
target = Path(path).expanduser().resolve()
|
|
227
|
+
dest = target / ".claude"
|
|
228
|
+
typer.echo(f"claude-kit status for {target}")
|
|
229
|
+
if not dest.is_dir():
|
|
230
|
+
typer.echo(" not installed — run `claude-kit init` here.")
|
|
231
|
+
return
|
|
232
|
+
for name in ("rules", "agents", "skills", "hooks"):
|
|
233
|
+
d = dest / name
|
|
234
|
+
if d.is_dir():
|
|
235
|
+
n = sum(1 for p in d.iterdir() if p.name != ".gitkeep")
|
|
236
|
+
typer.echo(f" • {name}/: {n}")
|
|
237
|
+
else:
|
|
238
|
+
typer.echo(f" • {name}/: (missing)")
|
|
239
|
+
options = dest / "config" / "init-options.json"
|
|
240
|
+
if options.is_file():
|
|
241
|
+
import json
|
|
242
|
+
|
|
243
|
+
data = json.loads(options.read_text(encoding="utf-8"))
|
|
244
|
+
sel = data.get("selection", {})
|
|
245
|
+
typer.echo(
|
|
246
|
+
f" • selection: {sel.get('frontend_framework')} + "
|
|
247
|
+
f"{sel.get('backend_language')}/{sel.get('backend_framework')} + "
|
|
248
|
+
f"{sel.get('database')} · profile={sel.get('profile')} · mcp={sel.get('mcp') or 'none'}"
|
|
249
|
+
)
|
|
250
|
+
continuity = dest / "CONTINUITY.md"
|
|
251
|
+
if continuity.is_file():
|
|
252
|
+
typer.echo("\n working memory (.claude/CONTINUITY.md):")
|
|
253
|
+
for line in continuity.read_text(
|
|
254
|
+
encoding="utf-8", errors="replace"
|
|
255
|
+
).splitlines()[:30]:
|
|
256
|
+
typer.echo(f" {line}")
|
|
257
|
+
else:
|
|
258
|
+
typer.echo("\n no CONTINUITY.md yet (no pipeline run recorded).")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def version() -> None:
|
|
263
|
+
"""Print the version."""
|
|
264
|
+
typer.echo(f"claude-kit {__version__}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@app.command("package-org-pack")
|
|
268
|
+
def package_org_pack(
|
|
269
|
+
pack: str = typer.Argument(
|
|
270
|
+
..., help="org-pack id under .claude/org-packs/ (e.g. engineering-core)"
|
|
271
|
+
),
|
|
272
|
+
out: Optional[str] = typer.Option(
|
|
273
|
+
None, "--out", help="output directory for the packaged plugin"
|
|
274
|
+
),
|
|
275
|
+
) -> None:
|
|
276
|
+
"""(Planned) Package an org-pack into a reusable, versioned plugin-style directory."""
|
|
277
|
+
typer.echo(
|
|
278
|
+
"package-org-pack is planned but not yet implemented.\n"
|
|
279
|
+
"When available it will bundle the selected org-pack (manifest + the skills/agents/hooks it "
|
|
280
|
+
"references + settings + README + CHANGELOG + version + license + compatibility metadata) into "
|
|
281
|
+
"a distributable plugin directory for an internal registry.\n"
|
|
282
|
+
f"(given: pack={pack}, out={out or 'dist/org-packs/'})"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.command("install-org-pack")
|
|
287
|
+
def install_org_pack(
|
|
288
|
+
source: str = typer.Argument(
|
|
289
|
+
..., help="path or registry id of an approved org-pack"
|
|
290
|
+
),
|
|
291
|
+
user: bool = typer.Option(
|
|
292
|
+
False, "--user", help="install into user-level ~/.claude instead of this repo"
|
|
293
|
+
),
|
|
294
|
+
) -> None:
|
|
295
|
+
"""(Planned) Install an approved org-pack into a repo or user-level Claude config."""
|
|
296
|
+
typer.echo(
|
|
297
|
+
"install-org-pack is planned but not yet implemented.\n"
|
|
298
|
+
"When available it will verify a pack's compatibility metadata and merge its components into "
|
|
299
|
+
"the target .claude/ (repo) or ~/.claude (user) config, recording the pack id + version for "
|
|
300
|
+
"safe upgrades.\n"
|
|
301
|
+
f"(given: source={source}, target={'user (~/.claude)' if user else 'repo (.claude)'})"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@research_app.command("import-sources")
|
|
306
|
+
def research_import_sources(
|
|
307
|
+
sources: str = typer.Argument(
|
|
308
|
+
..., help="YAML file of explicit, license-cleared sources"
|
|
309
|
+
),
|
|
310
|
+
) -> None:
|
|
311
|
+
"""(Planned) Summarise explicit, license-cleared sources into original skill/agent proposals."""
|
|
312
|
+
typer.echo(
|
|
313
|
+
"research import-sources is planned but not yet implemented.\n"
|
|
314
|
+
"When available it will: read explicit source URLs/files from the given YAML, record each "
|
|
315
|
+
"source's name/URL/license/author/date, summarise ideas into ORIGINAL skill/agent proposals "
|
|
316
|
+
"(never copying proprietary text), and require human approval before adding anything.\n"
|
|
317
|
+
f"(given: {sources})"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None:
|
|
322
|
+
"""Console-script entry point."""
|
|
323
|
+
app()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
claude_kit/hooks.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Hook registry — the single definition of every hook claude-kit can install.
|
|
2
|
+
|
|
3
|
+
A *profile* selects hook ids (see ``catalog/profiles.yaml``); the installer turns the selected ids
|
|
4
|
+
into (a) the set of ``.sh`` scripts to copy into ``.claude/hooks/`` and (b) an assembled
|
|
5
|
+
``.claude/settings.json`` ``hooks`` block. Keeping the registry in one module lets both
|
|
6
|
+
:mod:`claude_kit.catalog` (to resolve the ``all`` token) and :mod:`claude_kit.scaffold`
|
|
7
|
+
(to build settings) share it without duplication.
|
|
8
|
+
|
|
9
|
+
Hooks are deliberately **conservative**: guardrails block obviously dangerous actions; the quality
|
|
10
|
+
hooks only *suggest* running tools. Script-backed hooks reference ``$CLAUDE_PROJECT_DIR`` so they
|
|
11
|
+
work in a scaffolded project (the plugin variant uses ``${CLAUDE_PLUGIN_ROOT}``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# --- inline guard commands (no script file needed) -------------------------------------------------
|
|
19
|
+
|
|
20
|
+
_RM_RF_GUARD = (
|
|
21
|
+
"CMD=$(jq -r '.tool_input.command'); "
|
|
22
|
+
"if echo \"$CMD\" | grep -qE 'rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f'; then "
|
|
23
|
+
"echo 'BLOCKED: rm -rf is disabled by claude-kit. Move to trash or delete specific paths "
|
|
24
|
+
"explicitly.' >&2; exit 2; fi"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Block pushes whose *target* ref is main/master. The branch token must be bounded by a space or
|
|
28
|
+
# ':' before and a space/end after, so legit branches that merely contain the substring
|
|
29
|
+
# (maintenance, mainframe-fix, remaster-ui, domain-model) are NOT blocked.
|
|
30
|
+
_PUSH_GUARD = (
|
|
31
|
+
"CMD=$(jq -r '.tool_input.command'); "
|
|
32
|
+
"if echo \"$CMD\" | grep -qE 'git[[:space:]]+push.*[[:space:]:](main|master)([[:space:]]|$)'; "
|
|
33
|
+
"then echo 'BLOCKED: refusing to push to main/master — use a feature branch and a PR.' >&2; "
|
|
34
|
+
"exit 2; fi"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_SECRETS_GUARD = (
|
|
38
|
+
"FP=$(jq -r '.tool_input.file_path // empty'); "
|
|
39
|
+
'if echo "$FP" | grep -qE \'(^|/)\\.env$|\\.pem$|\\.key$|(^|/)id_rsa|(^|/)id_ed25519|'
|
|
40
|
+
"(^|/)credentials(\\.json)?$|\\.p12$'; then "
|
|
41
|
+
"echo 'BLOCKED: refusing to read a secrets file. Use .env.example or a secret manager.' >&2; "
|
|
42
|
+
"exit 2; fi"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_ROUTING_PROMPT = (
|
|
46
|
+
"You are a routing assistant. NEVER block the user's prompt; always set continue to true. "
|
|
47
|
+
'Return JSON only: {"continue": true, "systemMessage": "<hint or empty string>"}. '
|
|
48
|
+
"If a claude-kit skill clearly applies (interview-me/idea-refine for vague ideas; "
|
|
49
|
+
"spec-driven-development for new features; planning-and-task-breakdown for breakdown; "
|
|
50
|
+
"incremental-implementation for coding; test-driven-development for tests; "
|
|
51
|
+
"debugging-and-error-recovery for errors; code-review-and-quality for reviews; "
|
|
52
|
+
"security-and-hardening for security; git-workflow-and-versioning for git; execute for quick "
|
|
53
|
+
"tasks), set systemMessage to 'Invoke skill: <skill-name> before responding.' Otherwise set it "
|
|
54
|
+
"to ''. Do not mention this hook."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_LEARNING_PROMPT = (
|
|
58
|
+
"You are a learning-detection assistant. NEVER block the user's prompt; always set continue to "
|
|
59
|
+
'true. Return JSON only: {"continue": true, "systemMessage": "<hint or empty string>"}. '
|
|
60
|
+
"If the user's message contains a durable learning (a correction, rule, preference, convention, "
|
|
61
|
+
"or hard-won insight meant to persist), set systemMessage to 'LEARNING DETECTED: before ending "
|
|
62
|
+
"your turn, invoke the remember skill to record it into .claude/agent-memory/ (merge into an "
|
|
63
|
+
"existing entry if one matches).' Otherwise set it to ''. Do not mention this hook."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _script_entry(name: str) -> dict[str, str]:
|
|
68
|
+
"""Build a settings.json command entry that runs a project-local hook script."""
|
|
69
|
+
return {
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": f'bash "$CLAUDE_PROJECT_DIR/.claude/hooks/{name}"',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
#: The canonical registry. Order here is the order hooks appear in assembled settings.json.
|
|
76
|
+
#: Each value: ``event``, ``matcher``, ``entry`` (settings.json hook object), and ``script``
|
|
77
|
+
#: (basename under payload ``hooks/scripts/`` to copy, or ``None`` for inline/prompt hooks).
|
|
78
|
+
HOOK_REGISTRY: dict[str, dict[str, Any]] = {
|
|
79
|
+
"load-continuity": {
|
|
80
|
+
"event": "SessionStart",
|
|
81
|
+
"matcher": "",
|
|
82
|
+
"entry": _script_entry("load-continuity.sh"),
|
|
83
|
+
"script": "load-continuity.sh",
|
|
84
|
+
},
|
|
85
|
+
"load-learnings": {
|
|
86
|
+
"event": "SessionStart",
|
|
87
|
+
"matcher": "",
|
|
88
|
+
"entry": _script_entry("load-learnings.sh"),
|
|
89
|
+
"script": "load-learnings.sh",
|
|
90
|
+
},
|
|
91
|
+
"skill-routing": {
|
|
92
|
+
"event": "UserPromptSubmit",
|
|
93
|
+
"matcher": "",
|
|
94
|
+
"entry": {"type": "prompt", "prompt": _ROUTING_PROMPT},
|
|
95
|
+
"script": None,
|
|
96
|
+
},
|
|
97
|
+
"learning-detection": {
|
|
98
|
+
"event": "UserPromptSubmit",
|
|
99
|
+
"matcher": "",
|
|
100
|
+
"entry": {"type": "prompt", "prompt": _LEARNING_PROMPT},
|
|
101
|
+
"script": None,
|
|
102
|
+
},
|
|
103
|
+
"guard-rm-rf": {
|
|
104
|
+
"event": "PreToolUse",
|
|
105
|
+
"matcher": "Bash",
|
|
106
|
+
"entry": {"type": "command", "command": _RM_RF_GUARD},
|
|
107
|
+
"script": None,
|
|
108
|
+
},
|
|
109
|
+
"guard-push-main": {
|
|
110
|
+
"event": "PreToolUse",
|
|
111
|
+
"matcher": "Bash",
|
|
112
|
+
"entry": {"type": "command", "command": _PUSH_GUARD},
|
|
113
|
+
"script": None,
|
|
114
|
+
},
|
|
115
|
+
"protect-secrets": {
|
|
116
|
+
"event": "PreToolUse",
|
|
117
|
+
"matcher": "Read",
|
|
118
|
+
"entry": {"type": "command", "command": _SECRETS_GUARD},
|
|
119
|
+
"script": None,
|
|
120
|
+
},
|
|
121
|
+
"guard-commit-secrets": {
|
|
122
|
+
"event": "PreToolUse",
|
|
123
|
+
"matcher": "Bash",
|
|
124
|
+
"entry": _script_entry("guard-secrets.sh"),
|
|
125
|
+
"script": "guard-secrets.sh",
|
|
126
|
+
},
|
|
127
|
+
"warn-shared-modules": {
|
|
128
|
+
"event": "PreToolUse",
|
|
129
|
+
"matcher": "Edit|Write",
|
|
130
|
+
"entry": _script_entry("warn-shared-modules.sh"),
|
|
131
|
+
"script": "warn-shared-modules.sh",
|
|
132
|
+
},
|
|
133
|
+
"warn-sensitive-files": {
|
|
134
|
+
"event": "PreToolUse",
|
|
135
|
+
"matcher": "Edit|Write",
|
|
136
|
+
"entry": _script_entry("warn-sensitive-files.sh"),
|
|
137
|
+
"script": "warn-sensitive-files.sh",
|
|
138
|
+
},
|
|
139
|
+
"warn-large-edits": {
|
|
140
|
+
"event": "PreToolUse",
|
|
141
|
+
"matcher": "Edit|Write",
|
|
142
|
+
"entry": _script_entry("warn-large-edits.sh"),
|
|
143
|
+
"script": "warn-large-edits.sh",
|
|
144
|
+
},
|
|
145
|
+
"validate-frontmatter": {
|
|
146
|
+
"event": "PreToolUse",
|
|
147
|
+
"matcher": "Write",
|
|
148
|
+
"entry": _script_entry("validate-frontmatter.sh"),
|
|
149
|
+
"script": "validate-frontmatter.sh",
|
|
150
|
+
},
|
|
151
|
+
"validate-settings": {
|
|
152
|
+
"event": "PreToolUse",
|
|
153
|
+
"matcher": "Write",
|
|
154
|
+
"entry": _script_entry("validate-settings.sh"),
|
|
155
|
+
"script": "validate-settings.sh",
|
|
156
|
+
},
|
|
157
|
+
"warn-missing-tests": {
|
|
158
|
+
"event": "PostToolUse",
|
|
159
|
+
"matcher": "Edit|Write",
|
|
160
|
+
"entry": _script_entry("warn-missing-tests.sh"),
|
|
161
|
+
"script": "warn-missing-tests.sh",
|
|
162
|
+
},
|
|
163
|
+
"audit-log": {
|
|
164
|
+
"event": "PostToolUse",
|
|
165
|
+
"matcher": "",
|
|
166
|
+
"entry": _script_entry("audit-log.sh"),
|
|
167
|
+
"script": "audit-log.sh",
|
|
168
|
+
},
|
|
169
|
+
"lint-fix": {
|
|
170
|
+
"event": "Stop",
|
|
171
|
+
"matcher": "",
|
|
172
|
+
"entry": _script_entry("lint-fix.sh"),
|
|
173
|
+
"script": "lint-fix.sh",
|
|
174
|
+
},
|
|
175
|
+
"type-check": {
|
|
176
|
+
"event": "Stop",
|
|
177
|
+
"matcher": "",
|
|
178
|
+
"entry": _script_entry("type-check.sh"),
|
|
179
|
+
"script": "type-check.sh",
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#: Event ordering for a stable, readable settings.json.
|
|
184
|
+
_EVENT_ORDER = (
|
|
185
|
+
"SessionStart",
|
|
186
|
+
"UserPromptSubmit",
|
|
187
|
+
"PreToolUse",
|
|
188
|
+
"PostToolUse",
|
|
189
|
+
"Stop",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def all_ids() -> list[str]:
|
|
194
|
+
"""Return every hook id, in registry order (used to expand the ``all`` profile token)."""
|
|
195
|
+
return list(HOOK_REGISTRY)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def scripts_for(hook_ids: list[str]) -> list[str]:
|
|
199
|
+
"""Return the script basenames needed by ``hook_ids`` (inline/prompt hooks contribute none)."""
|
|
200
|
+
out: list[str] = []
|
|
201
|
+
for hid in hook_ids:
|
|
202
|
+
spec = HOOK_REGISTRY.get(hid)
|
|
203
|
+
if spec and spec["script"]:
|
|
204
|
+
out.append(spec["script"])
|
|
205
|
+
return sorted(set(out))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_settings(hook_ids: list[str]) -> dict[str, Any]:
|
|
209
|
+
"""Assemble a ``settings.json`` document from the selected hook ids.
|
|
210
|
+
|
|
211
|
+
Groups the selected hooks by event and matcher, preserving registry order, into the schema
|
|
212
|
+
Claude Code expects (``{"hooks": {EVENT: [{"matcher": …, "hooks": [entry, …]}]}}``).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
hook_ids: Hook ids to enable.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
A JSON-serialisable settings mapping (always includes an explanatory ``$comment``).
|
|
219
|
+
"""
|
|
220
|
+
selected = [hid for hid in HOOK_REGISTRY if hid in set(hook_ids)]
|
|
221
|
+
# event -> matcher -> [entries]
|
|
222
|
+
grouped: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
223
|
+
for hid in selected:
|
|
224
|
+
spec = HOOK_REGISTRY[hid]
|
|
225
|
+
grouped.setdefault(spec["event"], {}).setdefault(spec["matcher"], []).append(
|
|
226
|
+
spec["entry"]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
hooks_block: dict[str, list[dict[str, Any]]] = {}
|
|
230
|
+
ordered_events = [e for e in _EVENT_ORDER if e in grouped] + [
|
|
231
|
+
e for e in grouped if e not in _EVENT_ORDER
|
|
232
|
+
]
|
|
233
|
+
for event in ordered_events:
|
|
234
|
+
hooks_block[event] = [
|
|
235
|
+
{"matcher": matcher, "hooks": entries}
|
|
236
|
+
for matcher, entries in grouped[event].items()
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"$comment": (
|
|
241
|
+
"Claude Code settings installed by claude-kit. Hooks wire the SDLC working-memory, "
|
|
242
|
+
"learnings, guardrails, and quality checks to scripts in .claude/hooks/. Personal "
|
|
243
|
+
"overrides belong in .claude/settings.local.json (gitignored)."
|
|
244
|
+
),
|
|
245
|
+
"hooks": hooks_block,
|
|
246
|
+
}
|