@softspark/ai-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +412 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +632 -0
- package/action.yml +53 -0
- package/app/.claude-plugin/plugin.json +44 -0
- package/app/ARCHITECTURE.md +306 -0
- package/app/CLAUDE.md.template +23 -0
- package/app/agents/ai-engineer.md +128 -0
- package/app/agents/backend-specialist.md +193 -0
- package/app/agents/business-intelligence.md +54 -0
- package/app/agents/chaos-monkey.md +67 -0
- package/app/agents/chief-of-staff.md +51 -0
- package/app/agents/code-archaeologist.md +127 -0
- package/app/agents/code-reviewer.md +184 -0
- package/app/agents/command-expert.md +131 -0
- package/app/agents/data-analyst.md +205 -0
- package/app/agents/data-scientist.md +151 -0
- package/app/agents/database-architect.md +317 -0
- package/app/agents/debugger.md +238 -0
- package/app/agents/devops-implementer.md +194 -0
- package/app/agents/documenter.md +364 -0
- package/app/agents/explorer-agent.md +145 -0
- package/app/agents/fact-checker.md +172 -0
- package/app/agents/frontend-specialist.md +209 -0
- package/app/agents/game-developer.md +216 -0
- package/app/agents/incident-responder.md +226 -0
- package/app/agents/infrastructure-architect.md +127 -0
- package/app/agents/infrastructure-validator.md +247 -0
- package/app/agents/llm-ops-engineer.md +237 -0
- package/app/agents/mcp-expert.md +228 -0
- package/app/agents/mcp-server-architect.md +195 -0
- package/app/agents/mcp-testing-engineer.md +292 -0
- package/app/agents/meta-architect.md +58 -0
- package/app/agents/ml-engineer.md +136 -0
- package/app/agents/mobile-developer.md +190 -0
- package/app/agents/night-watchman.md +55 -0
- package/app/agents/nlp-engineer.md +154 -0
- package/app/agents/orchestrator.md +437 -0
- package/app/agents/performance-optimizer.md +254 -0
- package/app/agents/predictive-analyst.md +57 -0
- package/app/agents/product-manager.md +194 -0
- package/app/agents/project-planner.md +287 -0
- package/app/agents/prompt-engineer.md +103 -0
- package/app/agents/qa-automation-engineer.md +182 -0
- package/app/agents/rag-engineer.md +201 -0
- package/app/agents/research-synthesizer.md +138 -0
- package/app/agents/search-specialist.md +101 -0
- package/app/agents/security-architect.md +62 -0
- package/app/agents/security-auditor.md +293 -0
- package/app/agents/seo-specialist.md +111 -0
- package/app/agents/system-governor.md +57 -0
- package/app/agents/tech-lead.md +62 -0
- package/app/agents/technical-researcher.md +103 -0
- package/app/agents/test-engineer.md +264 -0
- package/app/constitution.md +38 -0
- package/app/hooks/_profile-check.sh +11 -0
- package/app/hooks/guard-destructive.sh +74 -0
- package/app/hooks/guard-path.sh +73 -0
- package/app/hooks/post-tool-use.sh +35 -0
- package/app/hooks/pre-compact.sh +31 -0
- package/app/hooks/quality-check.sh +22 -0
- package/app/hooks/quality-gate.sh +49 -0
- package/app/hooks/save-session.sh +24 -0
- package/app/hooks/session-end.sh +37 -0
- package/app/hooks/session-start.sh +29 -0
- package/app/hooks/subagent-start.sh +16 -0
- package/app/hooks/subagent-stop.sh +16 -0
- package/app/hooks/track-usage.sh +50 -0
- package/app/hooks/user-prompt-submit.sh +25 -0
- package/app/hooks.json +178 -0
- package/app/mcp-defaults.json +23 -0
- package/app/output-styles/golden-rules.md +43 -0
- package/app/plugins/README.md +19 -0
- package/app/plugins/csharp-pack/README.md +11 -0
- package/app/plugins/csharp-pack/plugin.json +18 -0
- package/app/plugins/enterprise-pack/README.md +16 -0
- package/app/plugins/enterprise-pack/hooks/output-style.sh +6 -0
- package/app/plugins/enterprise-pack/hooks/status-line.sh +8 -0
- package/app/plugins/enterprise-pack/plugin.json +24 -0
- package/app/plugins/frontend-pack/README.md +14 -0
- package/app/plugins/frontend-pack/plugin.json +22 -0
- package/app/plugins/java-pack/README.md +11 -0
- package/app/plugins/java-pack/plugin.json +18 -0
- package/app/plugins/kotlin-pack/README.md +11 -0
- package/app/plugins/kotlin-pack/plugin.json +18 -0
- package/app/plugins/memory-pack/README.md +24 -0
- package/app/plugins/memory-pack/hooks/observation-capture.sh +67 -0
- package/app/plugins/memory-pack/hooks/session-summary.sh +71 -0
- package/app/plugins/memory-pack/plugin.json +22 -0
- package/app/plugins/memory-pack/scripts/init_db.py +81 -0
- package/app/plugins/memory-pack/scripts/strip_private.py +22 -0
- package/app/plugins/memory-pack/skills/mem-search/SKILL.md +70 -0
- package/app/plugins/research-pack/README.md +14 -0
- package/app/plugins/research-pack/plugin.json +22 -0
- package/app/plugins/ruby-pack/README.md +11 -0
- package/app/plugins/ruby-pack/plugin.json +18 -0
- package/app/plugins/rust-pack/README.md +11 -0
- package/app/plugins/rust-pack/plugin.json +18 -0
- package/app/plugins/security-pack/README.md +15 -0
- package/app/plugins/security-pack/plugin.json +23 -0
- package/app/plugins/swift-pack/README.md +11 -0
- package/app/plugins/swift-pack/plugin.json +18 -0
- package/app/rules/claude-toolkit-rules.md +21 -0
- package/app/rules/git-conventions.md +5 -0
- package/app/rules/quality-gates.md +10 -0
- package/app/skills/_lib/__init__.py +1 -0
- package/app/skills/_lib/detect_utils.py +150 -0
- package/app/skills/agent-creator/SKILL.md +82 -0
- package/app/skills/analyze/SKILL.md +92 -0
- package/app/skills/analyze/scripts/complexity.py +165 -0
- package/app/skills/api-patterns/SKILL.md +305 -0
- package/app/skills/app-builder/SKILL.md +187 -0
- package/app/skills/architecture-audit/SKILL.md +141 -0
- package/app/skills/architecture-decision/SKILL.md +55 -0
- package/app/skills/architecture-decision/templates/adr-template.md +36 -0
- package/app/skills/biz-scan/SKILL.md +30 -0
- package/app/skills/briefing/SKILL.md +27 -0
- package/app/skills/build/SKILL.md +97 -0
- package/app/skills/build/scripts/detect-build.py +151 -0
- package/app/skills/chaos/SKILL.md +32 -0
- package/app/skills/ci/SKILL.md +77 -0
- package/app/skills/ci/scripts/ci-detect.py +135 -0
- package/app/skills/ci/templates/github-actions-node.yml +38 -0
- package/app/skills/ci/templates/github-actions-python.yml +42 -0
- package/app/skills/ci-cd-patterns/SKILL.md +299 -0
- package/app/skills/clean-code/SKILL.md +110 -0
- package/app/skills/clean-code/reference/dart.md +18 -0
- package/app/skills/clean-code/reference/go.md +23 -0
- package/app/skills/clean-code/reference/php.md +32 -0
- package/app/skills/clean-code/reference/python.md +180 -0
- package/app/skills/clean-code/reference/typescript.md +26 -0
- package/app/skills/command-creator/SKILL.md +83 -0
- package/app/skills/commit/SKILL.md +98 -0
- package/app/skills/commit/scripts/pre-commit-check.py +87 -0
- package/app/skills/commit/templates/conventional-commit.md +52 -0
- package/app/skills/csharp-patterns/SKILL.md +450 -0
- package/app/skills/database-patterns/SKILL.md +297 -0
- package/app/skills/debug/SKILL.md +154 -0
- package/app/skills/debug/scripts/error-parser.py +187 -0
- package/app/skills/debugging-tactics/SKILL.md +136 -0
- package/app/skills/deploy/SKILL.md +130 -0
- package/app/skills/deploy/scripts/pre_deploy_check.py +171 -0
- package/app/skills/deploy/templates/deployment-checklist.md +31 -0
- package/app/skills/design-an-interface/SKILL.md +105 -0
- package/app/skills/design-engineering/SKILL.md +260 -0
- package/app/skills/docker-devops/SKILL.md +303 -0
- package/app/skills/docs/SKILL.md +145 -0
- package/app/skills/docs/scripts/doc-inventory.py +176 -0
- package/app/skills/docs/templates/adr-template.md +36 -0
- package/app/skills/docs/templates/readme-template.md +67 -0
- package/app/skills/documentation-standards/SKILL.md +191 -0
- package/app/skills/ecommerce-patterns/SKILL.md +209 -0
- package/app/skills/evaluate/SKILL.md +132 -0
- package/app/skills/evolve/SKILL.md +27 -0
- package/app/skills/explain/SKILL.md +54 -0
- package/app/skills/explain/scripts/dependency-graph.py +215 -0
- package/app/skills/explore/SKILL.md +112 -0
- package/app/skills/explore/scripts/visualize.py +117 -0
- package/app/skills/fix/SKILL.md +78 -0
- package/app/skills/fix/scripts/error-classifier.py +191 -0
- package/app/skills/flutter-patterns/SKILL.md +254 -0
- package/app/skills/git-mastery/SKILL.md +70 -0
- package/app/skills/grill-me/SKILL.md +38 -0
- package/app/skills/health/SKILL.md +91 -0
- package/app/skills/health/scripts/health_check.py +162 -0
- package/app/skills/hive-mind/SKILL.md +56 -0
- package/app/skills/hook-creator/SKILL.md +107 -0
- package/app/skills/index/SKILL.md +74 -0
- package/app/skills/instinct-review/SKILL.md +77 -0
- package/app/skills/java-patterns/SKILL.md +442 -0
- package/app/skills/kotlin-patterns/SKILL.md +446 -0
- package/app/skills/lint/SKILL.md +103 -0
- package/app/skills/lint/scripts/detect-linters.py +112 -0
- package/app/skills/mcp-patterns/SKILL.md +270 -0
- package/app/skills/mem-search/SKILL.md +70 -0
- package/app/skills/migrate/SKILL.md +90 -0
- package/app/skills/migrate/scripts/migration-status.py +195 -0
- package/app/skills/migration-patterns/SKILL.md +260 -0
- package/app/skills/night-watch/SKILL.md +28 -0
- package/app/skills/observability-patterns/SKILL.md +203 -0
- package/app/skills/onboard/SKILL.md +76 -0
- package/app/skills/orchestrate/SKILL.md +86 -0
- package/app/skills/panic/SKILL.md +30 -0
- package/app/skills/performance-profiling/SKILL.md +59 -0
- package/app/skills/plan/SKILL.md +110 -0
- package/app/skills/plan/templates/plan-template.md +40 -0
- package/app/skills/plan-writing/SKILL.md +201 -0
- package/app/skills/plugin-creator/SKILL.md +78 -0
- package/app/skills/pr/SKILL.md +129 -0
- package/app/skills/pr/scripts/pr-summary.py +175 -0
- package/app/skills/prd-to-issues/SKILL.md +108 -0
- package/app/skills/prd-to-plan/SKILL.md +120 -0
- package/app/skills/predict/SKILL.md +30 -0
- package/app/skills/qa-session/SKILL.md +110 -0
- package/app/skills/rag-patterns/SKILL.md +203 -0
- package/app/skills/refactor/SKILL.md +124 -0
- package/app/skills/refactor/scripts/refactor-scan.py +210 -0
- package/app/skills/refactor-plan/SKILL.md +112 -0
- package/app/skills/repeat/SKILL.md +149 -0
- package/app/skills/research-mastery/SKILL.md +56 -0
- package/app/skills/review/SKILL.md +141 -0
- package/app/skills/review/scripts/diff-analyzer.py +170 -0
- package/app/skills/rollback/SKILL.md +87 -0
- package/app/skills/rollback/scripts/rollback_info.py +149 -0
- package/app/skills/ruby-patterns/SKILL.md +454 -0
- package/app/skills/rust-patterns/SKILL.md +446 -0
- package/app/skills/search/SKILL.md +64 -0
- package/app/skills/security-patterns/SKILL.md +91 -0
- package/app/skills/security-patterns/reference/authentication.md +37 -0
- package/app/skills/security-patterns/reference/authorization.md +22 -0
- package/app/skills/security-patterns/reference/input-validation.md +30 -0
- package/app/skills/security-patterns/reference/oauth-csrf-audit.md +131 -0
- package/app/skills/skill-creator/SKILL.md +154 -0
- package/app/skills/skill-creator/templates/dashboard/index.html +130 -0
- package/app/skills/skill-creator/templates/reasoning-engine/assets/example.json +12 -0
- package/app/skills/skill-creator/templates/reasoning-engine/search.py +110 -0
- package/app/skills/subagent-development/SKILL.md +225 -0
- package/app/skills/subagent-development/reference/code-quality-reviewer-prompt.md +145 -0
- package/app/skills/subagent-development/reference/implementer-prompt.md +118 -0
- package/app/skills/subagent-development/reference/spec-reviewer-prompt.md +100 -0
- package/app/skills/swarm/SKILL.md +81 -0
- package/app/skills/swift-patterns/SKILL.md +500 -0
- package/app/skills/tdd/SKILL.md +174 -0
- package/app/skills/tdd/reference/deep-modules.md +32 -0
- package/app/skills/tdd/reference/interface-design.md +32 -0
- package/app/skills/tdd/reference/mocking.md +52 -0
- package/app/skills/tdd/reference/refactoring.md +10 -0
- package/app/skills/tdd/reference/tests.md +59 -0
- package/app/skills/teams/SKILL.md +101 -0
- package/app/skills/test/SKILL.md +107 -0
- package/app/skills/test/scripts/detect-runner.py +113 -0
- package/app/skills/testing-patterns/SKILL.md +73 -0
- package/app/skills/testing-patterns/reference/flutter-testing.md +33 -0
- package/app/skills/testing-patterns/reference/go-testing.md +52 -0
- package/app/skills/testing-patterns/reference/php-phpunit.md +39 -0
- package/app/skills/testing-patterns/reference/python-pytest.md +228 -0
- package/app/skills/testing-patterns/reference/typescript-vitest.md +50 -0
- package/app/skills/triage-issue/SKILL.md +120 -0
- package/app/skills/typescript-patterns/SKILL.md +256 -0
- package/app/skills/ubiquitous-language/SKILL.md +74 -0
- package/app/skills/verification-before-completion/SKILL.md +108 -0
- package/app/skills/workflow/SKILL.md +250 -0
- package/app/skills/write-a-prd/SKILL.md +129 -0
- package/app/skills/write-a-prd/reference/visual-companion.md +78 -0
- package/app/skills/write-a-prd/scripts/frame-template.html +111 -0
- package/app/skills/write-a-prd/scripts/visual-server.cjs +79 -0
- package/app/templates/skill/generator/SKILL.md.template +40 -0
- package/app/templates/skill/knowledge/SKILL.md.template +52 -0
- package/app/templates/skill/linter/SKILL.md.template +34 -0
- package/app/templates/skill/reviewer/SKILL.md.template +51 -0
- package/app/templates/skill/workflow/SKILL.md.template +49 -0
- package/benchmarks/README.md +111 -0
- package/benchmarks/ecosystem-dashboard.json +148 -0
- package/benchmarks/ecosystem-harvest.json +148 -0
- package/benchmarks/results.json +38 -0
- package/benchmarks/run.py +351 -0
- package/bin/ai-toolkit.js +345 -0
- package/kb/best-practices/README.md +11 -0
- package/kb/howto/README.md +11 -0
- package/kb/procedures/maintenance-sop.md +306 -0
- package/kb/reference/agents-catalog.md +124 -0
- package/kb/reference/anti-pattern-registry-format.md +221 -0
- package/kb/reference/architecture-overview.md +232 -0
- package/kb/reference/benchmark-config.md +62 -0
- package/kb/reference/ci-integration.md +66 -0
- package/kb/reference/claude-ecosystem-benchmark-snapshot.md +80 -0
- package/kb/reference/claude-ecosystem-expansion-foundations.md +102 -0
- package/kb/reference/commands-catalog.md +21 -0
- package/kb/reference/distribution-model.md +63 -0
- package/kb/reference/global-install-model.md +56 -0
- package/kb/reference/hierarchical-override-pattern.md +200 -0
- package/kb/reference/hooks-catalog.md +306 -0
- package/kb/reference/integrations.md +88 -0
- package/kb/reference/language-packs.md +52 -0
- package/kb/reference/merge-friendly-install-model.md +58 -0
- package/kb/reference/plugin-pack-conventions.md +151 -0
- package/kb/reference/quick-wins-implementation-summary.md +70 -0
- package/kb/reference/skill-templates.md +50 -0
- package/kb/reference/skills-catalog.md +215 -0
- package/kb/reference/skills-unification.md +57 -0
- package/kb/reference/stats.md +69 -0
- package/kb/reference/sync.md +76 -0
- package/kb/troubleshooting/README.md +11 -0
- package/llms-full.txt +3068 -0
- package/llms.txt +39 -0
- package/package.json +75 -0
- package/scripts/_common.py +160 -0
- package/scripts/add_rule.py +50 -0
- package/scripts/benchmark_config.py +127 -0
- package/scripts/benchmark_ecosystem.py +288 -0
- package/scripts/check_deps.py +260 -0
- package/scripts/create_skill.py +118 -0
- package/scripts/doctor.py +504 -0
- package/scripts/eject.py +113 -0
- package/scripts/emission.py +256 -0
- package/scripts/evaluate_skills.py +260 -0
- package/scripts/frontmatter.py +58 -0
- package/scripts/generate_agents_md.py +91 -0
- package/scripts/generate_aider_conf.py +51 -0
- package/scripts/generate_cline.py +35 -0
- package/scripts/generate_copilot.py +30 -0
- package/scripts/generate_cursor_rules.py +35 -0
- package/scripts/generate_gemini.py +28 -0
- package/scripts/generate_llms_txt.py +164 -0
- package/scripts/generate_roo_modes.py +80 -0
- package/scripts/generate_windsurf.py +35 -0
- package/scripts/generator_base.py +140 -0
- package/scripts/harvest_ecosystem.py +50 -0
- package/scripts/inject_rule_cli.py +101 -0
- package/scripts/inject_section_cli.py +47 -0
- package/scripts/injection.py +180 -0
- package/scripts/install.py +236 -0
- package/scripts/install_git_hooks.py +71 -0
- package/scripts/install_steps/__init__.py +5 -0
- package/scripts/install_steps/ai_tools.py +261 -0
- package/scripts/install_steps/hooks.py +90 -0
- package/scripts/install_steps/markers.py +79 -0
- package/scripts/install_steps/symlinks.py +87 -0
- package/scripts/merge-hooks.py +192 -0
- package/scripts/plugin.py +642 -0
- package/scripts/plugin_schema.py +138 -0
- package/scripts/remove_rule.py +58 -0
- package/scripts/stats.py +81 -0
- package/scripts/sync.py +215 -0
- package/scripts/uninstall.py +292 -0
- package/scripts/validate.py +700 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI wrapper around inject_rule() and remove_rule_section() from _common.
|
|
3
|
+
|
|
4
|
+
Injects a rule file into CLAUDE.md using idempotent marker-based injection.
|
|
5
|
+
Re-running updates only the marked section. Content outside markers is
|
|
6
|
+
never touched.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
inject_rule_cli.py <rule-file> [target-dir]
|
|
10
|
+
inject_rule_cli.py --remove <rule-name> [target-dir]
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
rule-file Path to a .md file with the rule content
|
|
14
|
+
target-dir Directory containing .claude/CLAUDE.md (default: $HOME)
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
--remove Remove a rule section instead of injecting
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
25
|
+
from _common import inject_rule, remove_rule_section
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_args(argv: list[str]) -> dict:
|
|
29
|
+
"""Parse CLI arguments."""
|
|
30
|
+
result: dict = {
|
|
31
|
+
"remove_mode": False,
|
|
32
|
+
"remove_name": "",
|
|
33
|
+
"source_file": "",
|
|
34
|
+
"target_dir": str(Path.home()),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
i = 0
|
|
38
|
+
while i < len(argv):
|
|
39
|
+
arg = argv[i]
|
|
40
|
+
if arg == "--remove":
|
|
41
|
+
result["remove_mode"] = True
|
|
42
|
+
i += 1
|
|
43
|
+
if i >= len(argv):
|
|
44
|
+
print("--remove requires a rule name", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
result["remove_name"] = argv[i]
|
|
47
|
+
elif arg.startswith("-"):
|
|
48
|
+
print(f"Unknown option: {arg}", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
elif not result["source_file"] and not result["remove_mode"]:
|
|
51
|
+
result["source_file"] = arg
|
|
52
|
+
else:
|
|
53
|
+
result["target_dir"] = arg
|
|
54
|
+
i += 1
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> None:
|
|
60
|
+
"""Inject or remove a rule in CLAUDE.md."""
|
|
61
|
+
args = _parse_args(sys.argv[1:])
|
|
62
|
+
target_dir = Path(args["target_dir"])
|
|
63
|
+
claude_md = target_dir / ".claude" / "CLAUDE.md"
|
|
64
|
+
|
|
65
|
+
# -- remove mode ---------------------------------------------------------
|
|
66
|
+
if args["remove_mode"]:
|
|
67
|
+
rule_name = args["remove_name"]
|
|
68
|
+
if not claude_md.is_file():
|
|
69
|
+
print(f"No CLAUDE.md found at {target_dir}", file=sys.stderr)
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
found = remove_rule_section(rule_name, target_dir)
|
|
73
|
+
if found:
|
|
74
|
+
print(f"Removed rule '{rule_name}' from {claude_md}")
|
|
75
|
+
else:
|
|
76
|
+
print(f"Rule '{rule_name}' not found in {claude_md}")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# -- inject mode ---------------------------------------------------------
|
|
80
|
+
source_file = args["source_file"]
|
|
81
|
+
if not source_file:
|
|
82
|
+
print("Usage: inject_rule_cli.py <rule-file> [target-dir]", file=sys.stderr)
|
|
83
|
+
print(" inject_rule_cli.py --remove <rule-name> [target-dir]", file=sys.stderr)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
source_path = Path(source_file)
|
|
87
|
+
if not source_path.is_file():
|
|
88
|
+
print(f"Rule file not found: {source_path}", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
# Ensure .claude/ directory and CLAUDE.md exist
|
|
92
|
+
claude_md.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
if not claude_md.is_file():
|
|
94
|
+
print(f"Created: {claude_md}")
|
|
95
|
+
|
|
96
|
+
action = inject_rule(source_path, target_dir)
|
|
97
|
+
print(f" {action}: {claude_md}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI wrapper around inject_section() from _common.
|
|
3
|
+
|
|
4
|
+
Injects content between TOOLKIT markers into any file.
|
|
5
|
+
Existing content outside the markers is preserved -- never overwritten.
|
|
6
|
+
Re-running updates only the marked section (idempotent).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
inject_section_cli.py <content-file> <target-file> [section-name]
|
|
10
|
+
|
|
11
|
+
Arguments:
|
|
12
|
+
content-file Path to file with content to inject
|
|
13
|
+
target-file Path to target file (created if missing)
|
|
14
|
+
section-name Marker label (default: ai-toolkit)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
from _common import inject_section
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
"""Inject a content file into a target file between TOOLKIT markers."""
|
|
27
|
+
if len(sys.argv) < 3:
|
|
28
|
+
print(
|
|
29
|
+
"Usage: inject_section_cli.py <content-file> <target-file> [section-name]",
|
|
30
|
+
file=sys.stderr,
|
|
31
|
+
)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
content_file = Path(sys.argv[1])
|
|
35
|
+
target_file = Path(sys.argv[2])
|
|
36
|
+
section = sys.argv[3] if len(sys.argv) > 3 else "ai-toolkit"
|
|
37
|
+
|
|
38
|
+
if not content_file.is_file():
|
|
39
|
+
print(f"Content file not found: {content_file}", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
action = inject_section(content_file, target_file, section)
|
|
43
|
+
print(f" {action}: {target_file}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Marker-based section injection and rule management.
|
|
2
|
+
|
|
3
|
+
Handles idempotent injection of content between TOOLKIT markers
|
|
4
|
+
into target files. Also manages rule injection into CLAUDE.md.
|
|
5
|
+
|
|
6
|
+
Stdlib-only.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from injection import inject_section, inject_rule, remove_rule_section
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Markers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def markers_start(section: str = "ai-toolkit") -> str:
|
|
23
|
+
"""Return the TOOLKIT start marker block."""
|
|
24
|
+
return (
|
|
25
|
+
f"<!-- TOOLKIT:{section} START -->\n"
|
|
26
|
+
f"<!-- Auto-injected by ai-toolkit. Re-run to update. -->\n"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def markers_end(section: str = "ai-toolkit") -> str:
|
|
31
|
+
"""Return the TOOLKIT end marker."""
|
|
32
|
+
return f"\n<!-- TOOLKIT:{section} END -->"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Internal helpers
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def strip_section(content: str, section: str) -> str:
|
|
40
|
+
"""Remove a TOOLKIT marker section from content."""
|
|
41
|
+
start = f"<!-- TOOLKIT:{section} START -->"
|
|
42
|
+
end = f"<!-- TOOLKIT:{section} END -->"
|
|
43
|
+
lines: list[str] = []
|
|
44
|
+
skip = False
|
|
45
|
+
for line in content.splitlines(keepends=True):
|
|
46
|
+
stripped = line.rstrip("\n")
|
|
47
|
+
if stripped == start:
|
|
48
|
+
skip = True
|
|
49
|
+
continue
|
|
50
|
+
if stripped == end:
|
|
51
|
+
skip = False
|
|
52
|
+
continue
|
|
53
|
+
if not skip:
|
|
54
|
+
lines.append(line)
|
|
55
|
+
return "".join(lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def trim_trailing_blanks(text: str) -> str:
|
|
59
|
+
"""Remove trailing blank lines from text."""
|
|
60
|
+
lines = text.splitlines()
|
|
61
|
+
while lines and not lines[-1].strip():
|
|
62
|
+
lines.pop()
|
|
63
|
+
return "\n".join(lines)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def collapse_blank_runs(text: str, max_blanks: int = 2) -> str:
|
|
67
|
+
"""Collapse runs of more than max_blanks consecutive blank lines."""
|
|
68
|
+
lines = text.splitlines(keepends=True)
|
|
69
|
+
result: list[str] = []
|
|
70
|
+
blanks = 0
|
|
71
|
+
for line in lines:
|
|
72
|
+
if not line.strip():
|
|
73
|
+
blanks += 1
|
|
74
|
+
if blanks <= max_blanks:
|
|
75
|
+
result.append(line)
|
|
76
|
+
else:
|
|
77
|
+
blanks = 0
|
|
78
|
+
result.append(line)
|
|
79
|
+
return "".join(result)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Public API
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def inject_section(
|
|
87
|
+
content_file: str | Path,
|
|
88
|
+
target_file: str | Path,
|
|
89
|
+
section: str = "ai-toolkit",
|
|
90
|
+
) -> str:
|
|
91
|
+
"""Inject content between TOOLKIT markers into target file.
|
|
92
|
+
|
|
93
|
+
Existing content outside markers is preserved. Re-running updates
|
|
94
|
+
only the marked section (idempotent).
|
|
95
|
+
|
|
96
|
+
Returns action taken: "Created" or "Updated".
|
|
97
|
+
"""
|
|
98
|
+
content_file = Path(content_file)
|
|
99
|
+
target_file = Path(target_file)
|
|
100
|
+
|
|
101
|
+
# Sanitize section name
|
|
102
|
+
section = re.sub(r"[^a-zA-Z0-9_-]", "", section)
|
|
103
|
+
|
|
104
|
+
# Create parent dir and target if missing
|
|
105
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
if not target_file.exists():
|
|
107
|
+
target_file.touch()
|
|
108
|
+
action = "Created"
|
|
109
|
+
else:
|
|
110
|
+
action = "Updated"
|
|
111
|
+
|
|
112
|
+
# Read existing content
|
|
113
|
+
existing = target_file.read_text(encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
# Strip existing section
|
|
116
|
+
existing = strip_section(existing, section)
|
|
117
|
+
existing = trim_trailing_blanks(existing)
|
|
118
|
+
|
|
119
|
+
# Read content to inject
|
|
120
|
+
new_content = content_file.read_text(encoding="utf-8")
|
|
121
|
+
|
|
122
|
+
# Build output
|
|
123
|
+
parts: list[str] = []
|
|
124
|
+
if existing.strip():
|
|
125
|
+
parts.append(existing)
|
|
126
|
+
parts.append("")
|
|
127
|
+
|
|
128
|
+
parts.append(f"<!-- TOOLKIT:{section} START -->")
|
|
129
|
+
parts.append("<!-- Auto-injected by ai-toolkit. Re-run to update. -->")
|
|
130
|
+
parts.append("")
|
|
131
|
+
parts.append(new_content.rstrip("\n"))
|
|
132
|
+
parts.append("")
|
|
133
|
+
parts.append(f"<!-- TOOLKIT:{section} END -->")
|
|
134
|
+
|
|
135
|
+
output = "\n".join(parts) + "\n"
|
|
136
|
+
output = collapse_blank_runs(output)
|
|
137
|
+
|
|
138
|
+
target_file.write_text(output, encoding="utf-8")
|
|
139
|
+
return action
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def inject_rule(rule_file: str | Path, target_dir: str | Path) -> str:
|
|
143
|
+
"""Inject a rule file into target_dir/.claude/CLAUDE.md.
|
|
144
|
+
|
|
145
|
+
Returns action taken.
|
|
146
|
+
"""
|
|
147
|
+
rule_file = Path(rule_file)
|
|
148
|
+
target_dir = Path(target_dir)
|
|
149
|
+
|
|
150
|
+
if not rule_file.is_file():
|
|
151
|
+
raise FileNotFoundError(f"Rule file not found: {rule_file}")
|
|
152
|
+
|
|
153
|
+
rule_name = re.sub(r"[^a-zA-Z0-9_-]", "", rule_file.stem)
|
|
154
|
+
claude_md = target_dir / ".claude" / "CLAUDE.md"
|
|
155
|
+
claude_md.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
|
|
157
|
+
return inject_section(rule_file, claude_md, rule_name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def remove_rule_section(rule_name: str, target_dir: str | Path) -> bool:
|
|
161
|
+
"""Remove a rule section from target_dir/.claude/CLAUDE.md.
|
|
162
|
+
|
|
163
|
+
Returns True if section was found and removed.
|
|
164
|
+
"""
|
|
165
|
+
target_dir = Path(target_dir)
|
|
166
|
+
claude_md = target_dir / ".claude" / "CLAUDE.md"
|
|
167
|
+
|
|
168
|
+
if not claude_md.is_file():
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
172
|
+
start_marker = f"<!-- TOOLKIT:{rule_name} START -->"
|
|
173
|
+
|
|
174
|
+
if start_marker not in content:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
content = strip_section(content, rule_name)
|
|
178
|
+
content = trim_trailing_blanks(content) + "\n"
|
|
179
|
+
claude_md.write_text(content, encoding="utf-8")
|
|
180
|
+
return True
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AI Toolkit Installer.
|
|
3
|
+
|
|
4
|
+
Installs toolkit GLOBALLY -- Claude Code + all supported AI tools.
|
|
5
|
+
Re-running is idempotent: updates only marker-delimited sections,
|
|
6
|
+
never touching user content outside the markers.
|
|
7
|
+
|
|
8
|
+
Claude Code (~/.claude/):
|
|
9
|
+
- Per-file symlinks: agents/*.md, skills/*/ (merges with user files)
|
|
10
|
+
- Merged JSON: hooks.json (toolkit entries tagged with _source)
|
|
11
|
+
- Marker injection: constitution.md, ARCHITECTURE.md (preserves user content)
|
|
12
|
+
- Rules injected into ~/.claude/CLAUDE.md
|
|
13
|
+
|
|
14
|
+
Other tools (global config locations):
|
|
15
|
+
- Cursor: ~/.cursor/rules
|
|
16
|
+
- Windsurf: ~/.codeium/windsurf/memories/global_rules.md
|
|
17
|
+
- Gemini: ~/.gemini/GEMINI.md
|
|
18
|
+
|
|
19
|
+
Registered rules (~/.ai-toolkit/rules/*.md) are also injected into
|
|
20
|
+
all of the above. Add rules with: ai-toolkit add-rule <rule.md>
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 scripts/install.py [target-dir] [options]
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--only agents,hooks Install only listed components
|
|
27
|
+
--skip skills Skip listed components
|
|
28
|
+
--local Also inject into project-local configs
|
|
29
|
+
--list, --dry-run Dry-run: show what would be installed
|
|
30
|
+
--reset Wipe and recreate local configs
|
|
31
|
+
--profile <p> minimal|standard|strict
|
|
32
|
+
|
|
33
|
+
Components: agents, skills, hooks, constitution, architecture, rules,
|
|
34
|
+
cursor, windsurf, gemini
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
42
|
+
from _common import toolkit_dir
|
|
43
|
+
from emission import agent_count as count_agents, skill_count as count_skills
|
|
44
|
+
|
|
45
|
+
# Step modules
|
|
46
|
+
from install_steps.symlinks import install_agents, install_skills, clean_legacy_commands
|
|
47
|
+
from install_steps.hooks import install_hooks
|
|
48
|
+
from install_steps.markers import install_marker_files, inject_rules
|
|
49
|
+
from install_steps.ai_tools import install_ai_tools, install_local_project, run_script
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Argument parsing
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def parse_args(argv: list[str]) -> dict:
|
|
57
|
+
"""Parse CLI arguments into a config dict."""
|
|
58
|
+
cfg: dict = {
|
|
59
|
+
"target_dir": Path.home(),
|
|
60
|
+
"only": "",
|
|
61
|
+
"skip": "",
|
|
62
|
+
"dry_run": False,
|
|
63
|
+
"local": False,
|
|
64
|
+
"reset": False,
|
|
65
|
+
"profile": "",
|
|
66
|
+
}
|
|
67
|
+
i = 0
|
|
68
|
+
while i < len(argv):
|
|
69
|
+
arg = argv[i]
|
|
70
|
+
if arg in ("--list", "--dry-run"):
|
|
71
|
+
cfg["dry_run"] = True
|
|
72
|
+
elif arg == "--local":
|
|
73
|
+
cfg["local"] = True
|
|
74
|
+
elif arg == "--reset":
|
|
75
|
+
cfg["reset"] = True
|
|
76
|
+
elif arg.startswith("--only="):
|
|
77
|
+
cfg["only"] = arg.split("=", 1)[1]
|
|
78
|
+
elif arg == "--only":
|
|
79
|
+
i += 1
|
|
80
|
+
cfg["only"] = argv[i] if i < len(argv) else ""
|
|
81
|
+
elif arg.startswith("--skip="):
|
|
82
|
+
cfg["skip"] = arg.split("=", 1)[1]
|
|
83
|
+
elif arg == "--skip":
|
|
84
|
+
i += 1
|
|
85
|
+
cfg["skip"] = argv[i] if i < len(argv) else ""
|
|
86
|
+
elif arg.startswith("--profile="):
|
|
87
|
+
cfg["profile"] = arg.split("=", 1)[1]
|
|
88
|
+
elif arg == "--profile":
|
|
89
|
+
i += 1
|
|
90
|
+
cfg["profile"] = argv[i] if i < len(argv) else ""
|
|
91
|
+
elif arg.startswith("-"):
|
|
92
|
+
print(f"Unknown option: {arg}")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
else:
|
|
95
|
+
cfg["target_dir"] = Path(arg)
|
|
96
|
+
i += 1
|
|
97
|
+
return cfg
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Dependency check
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def check_dependencies() -> None:
|
|
105
|
+
"""Run check_deps and abort if any required dep is missing."""
|
|
106
|
+
from check_deps import check_deps
|
|
107
|
+
|
|
108
|
+
results = check_deps(verbose=False)
|
|
109
|
+
if not results["all_ok"]:
|
|
110
|
+
from check_deps import print_report
|
|
111
|
+
print_report(results)
|
|
112
|
+
print("Aborting install: missing required dependencies.")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Profile / Banner / Summary
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def resolve_profile(profile: str, only: str) -> str:
|
|
121
|
+
if profile == "minimal":
|
|
122
|
+
if not only:
|
|
123
|
+
return "agents,skills"
|
|
124
|
+
elif profile in ("standard", ""):
|
|
125
|
+
pass
|
|
126
|
+
elif profile == "strict":
|
|
127
|
+
pass
|
|
128
|
+
else:
|
|
129
|
+
print(f"Unknown profile: {profile} (valid: minimal, standard, strict)")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
return only
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def print_banner(target_dir: Path, rules_dir: Path, profile: str,
|
|
135
|
+
only: str, skip: str, dry_run: bool) -> None:
|
|
136
|
+
print("AI Toolkit Installer")
|
|
137
|
+
print("========================")
|
|
138
|
+
print(f"Toolkit: {toolkit_dir}")
|
|
139
|
+
print(f"Target: {target_dir.resolve()}")
|
|
140
|
+
print(f"Rules: {rules_dir}")
|
|
141
|
+
if profile:
|
|
142
|
+
print(f"Profile: {profile}")
|
|
143
|
+
if only:
|
|
144
|
+
print(f"Only: {only}")
|
|
145
|
+
if skip:
|
|
146
|
+
print(f"Skip: {skip}")
|
|
147
|
+
if dry_run:
|
|
148
|
+
print("Mode: DRY-RUN (no changes)")
|
|
149
|
+
print()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def print_summary() -> None:
|
|
153
|
+
print()
|
|
154
|
+
print("Done.")
|
|
155
|
+
print()
|
|
156
|
+
print("Next steps:")
|
|
157
|
+
print(" 1. Edit ~/.claude/CLAUDE.md -- add your global rules above the toolkit sections")
|
|
158
|
+
print(" 2. Per project: ai-toolkit install --local (or: ai-toolkit update --local)")
|
|
159
|
+
print(" 3. To update: npm install -g @softspark/ai-toolkit@latest && ai-toolkit update")
|
|
160
|
+
print(" 4. To register rules from other tools: ai-toolkit add-rule <rule.md>")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Install Claude Code (orchestrator)
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def install_claude_code(target_dir: Path, hooks_scripts_dir: Path,
|
|
168
|
+
rules_dir: Path, only: str, skip: str,
|
|
169
|
+
dry_run: bool) -> None:
|
|
170
|
+
print("## Claude Code (~/.claude/)")
|
|
171
|
+
print()
|
|
172
|
+
|
|
173
|
+
claude_dir = target_dir / ".claude"
|
|
174
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
install_agents(claude_dir, only, skip, dry_run)
|
|
177
|
+
install_skills(claude_dir, only, skip, dry_run)
|
|
178
|
+
clean_legacy_commands(claude_dir, dry_run)
|
|
179
|
+
install_hooks(claude_dir, hooks_scripts_dir, only, skip, dry_run)
|
|
180
|
+
install_marker_files(claude_dir, only, skip, dry_run)
|
|
181
|
+
|
|
182
|
+
if not dry_run:
|
|
183
|
+
print(" Note: settings.local.json is project-specific -- use 'ai-toolkit install --local' per project")
|
|
184
|
+
|
|
185
|
+
print()
|
|
186
|
+
print(f" Available: {count_agents()} agents, {count_skills()} skills")
|
|
187
|
+
|
|
188
|
+
inject_rules(claude_dir, target_dir, rules_dir, only, skip, dry_run)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
|
|
192
|
+
if profile == "strict" and not local and not dry_run:
|
|
193
|
+
cwd = Path.cwd()
|
|
194
|
+
if (cwd / ".git").is_dir():
|
|
195
|
+
run_script("install-git-hooks.sh", str(cwd))
|
|
196
|
+
print(f" Strict profile: git hooks installed in {cwd}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Main
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def main() -> None:
|
|
204
|
+
cfg = parse_args(sys.argv[1:])
|
|
205
|
+
|
|
206
|
+
target_dir: Path = cfg["target_dir"]
|
|
207
|
+
only: str = cfg["only"]
|
|
208
|
+
skip: str = cfg["skip"]
|
|
209
|
+
dry_run: bool = cfg["dry_run"]
|
|
210
|
+
local: bool = cfg["local"]
|
|
211
|
+
reset: bool = cfg["reset"]
|
|
212
|
+
profile: str = cfg["profile"]
|
|
213
|
+
|
|
214
|
+
rules_dir = Path.home() / ".ai-toolkit" / "rules"
|
|
215
|
+
hooks_scripts_dir = Path.home() / ".ai-toolkit" / "hooks"
|
|
216
|
+
|
|
217
|
+
only = resolve_profile(profile, only)
|
|
218
|
+
check_dependencies()
|
|
219
|
+
print_banner(target_dir, rules_dir, profile, only, skip, dry_run)
|
|
220
|
+
|
|
221
|
+
if not dry_run:
|
|
222
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
hooks_scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
|
|
225
|
+
install_claude_code(target_dir, hooks_scripts_dir, rules_dir, only, skip, dry_run)
|
|
226
|
+
install_ai_tools(target_dir, rules_dir, only, skip, dry_run)
|
|
227
|
+
|
|
228
|
+
if local:
|
|
229
|
+
install_local_project(rules_dir, dry_run, reset)
|
|
230
|
+
|
|
231
|
+
install_strict_git_hooks(profile, local, dry_run)
|
|
232
|
+
print_summary()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Install a fallback pre-commit hook for the ai-toolkit.
|
|
3
|
+
|
|
4
|
+
Used for non-Claude editors to enforce the constitution and quality gates.
|
|
5
|
+
|
|
6
|
+
Usage: python3 install_git_hooks.py [target-dir]
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
PRE_COMMIT_CONTENT = """\
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
# ai-toolkit fallback pre-commit hook
|
|
17
|
+
# Auto-generated by ai-toolkit.
|
|
18
|
+
|
|
19
|
+
# This hook acts as a safety fallback to ensure AI-generated commits
|
|
20
|
+
# aren't bypassing standard quality checks (like unresolved conflicts or missing tests)
|
|
21
|
+
# when using editors without native bash process hooks (like Cursor or Windsurf).
|
|
22
|
+
|
|
23
|
+
echo "[ai-toolkit] Running pre-commit quality gate..."
|
|
24
|
+
|
|
25
|
+
# 1. Check for unresolved merge conflicts
|
|
26
|
+
if git diff --cached -S'<<<<<<<' --name-only | grep -q '.*'; then
|
|
27
|
+
echo "ERROR: Unresolved merge conflicts found in staged files."
|
|
28
|
+
echo " Please resolve conflicts and remove '<<<<<<<' markers before committing."
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 2. Call the global quality-check if it exists
|
|
33
|
+
QUALITY_CHECK="$HOME/.ai-toolkit/hooks/quality-check.sh"
|
|
34
|
+
if [ -x "$QUALITY_CHECK" ]; then
|
|
35
|
+
if ! "$QUALITY_CHECK"; then
|
|
36
|
+
echo "ERROR: Linter or type checks failed."
|
|
37
|
+
echo " Use 'git commit --no-verify' if you absolutely must bypass this."
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "[ai-toolkit] Pre-commit checks passed."
|
|
43
|
+
exit 0
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main() -> None:
|
|
48
|
+
target_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
49
|
+
git_hooks_dir = target_dir / ".git" / "hooks"
|
|
50
|
+
|
|
51
|
+
if not git_hooks_dir.is_dir():
|
|
52
|
+
print(" Skipped: git hooks (not a git repository)")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
pre_commit = git_hooks_dir / "pre-commit"
|
|
56
|
+
|
|
57
|
+
# Back up existing pre-commit hook if not ours
|
|
58
|
+
if pre_commit.is_file():
|
|
59
|
+
content = pre_commit.read_text(encoding="utf-8", errors="replace")
|
|
60
|
+
if "ai-toolkit fallback pre-commit hook" not in content:
|
|
61
|
+
backup = git_hooks_dir / "pre-commit.backup"
|
|
62
|
+
pre_commit.rename(backup)
|
|
63
|
+
print(" Backed up existing pre-commit hook to pre-commit.backup")
|
|
64
|
+
|
|
65
|
+
pre_commit.write_text(PRE_COMMIT_CONTENT, encoding="utf-8")
|
|
66
|
+
pre_commit.chmod(pre_commit.stat().st_mode | 0o111)
|
|
67
|
+
print(" Installed: .git/hooks/pre-commit (ai-toolkit safety fallback)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|