@arthai/agents 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/README.md +123 -0
- package/VERSION +1 -0
- package/agents/ai-consultant.md +999 -0
- package/agents/architect.md +174 -0
- package/agents/code-reviewer.md +115 -0
- package/agents/competitive-analyst.md +688 -0
- package/agents/content-strategist.md +607 -0
- package/agents/design-studio-create.md +304 -0
- package/agents/design-studio-critique.md +258 -0
- package/agents/design-studio-think.md +79 -0
- package/agents/domain-hunter.md +519 -0
- package/agents/explore-light.md +52 -0
- package/agents/frontend.md +261 -0
- package/agents/gtm-expert.md +811 -0
- package/agents/meeting-prep.md +318 -0
- package/agents/ops.md +149 -0
- package/agents/product-manager.md +563 -0
- package/agents/python-backend.md +286 -0
- package/agents/qa-baseline-updater.md +45 -0
- package/agents/qa-challenger.md +97 -0
- package/agents/qa-domain.md +145 -0
- package/agents/qa-e2e.md +184 -0
- package/agents/qa-test-promoter.md +97 -0
- package/agents/qa.md +226 -0
- package/agents/setup.md +134 -0
- package/agents/sre.md +165 -0
- package/agents/stakeholder-reporter.md +94 -0
- package/agents/user-researcher.md +602 -0
- package/bin/cli.js +322 -0
- package/bundles/canvas.json +16 -0
- package/bundles/compass.json +16 -0
- package/bundles/counsel.json +31 -0
- package/bundles/cruise.json +11 -0
- package/bundles/forge.json +26 -0
- package/bundles/prime.json +10 -0
- package/bundles/prism.json +23 -0
- package/bundles/scalpel.json +17 -0
- package/bundles/sentinel.json +19 -0
- package/bundles/shield.json +14 -0
- package/bundles/spark.json +19 -0
- package/compiler.sh +305 -0
- package/dist/plugins/canvas/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/canvas/agents/design-studio-create.md +304 -0
- package/dist/plugins/canvas/agents/design-studio-critique.md +258 -0
- package/dist/plugins/canvas/agents/design-studio-think.md +79 -0
- package/dist/plugins/canvas/agents/frontend.md +261 -0
- package/dist/plugins/canvas/skills/planning/SKILL.md +436 -0
- package/dist/plugins/compass/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/compass/agents/content-strategist.md +607 -0
- package/dist/plugins/compass/agents/gtm-expert.md +811 -0
- package/dist/plugins/compass/agents/product-manager.md +563 -0
- package/dist/plugins/compass/agents/user-researcher.md +602 -0
- package/dist/plugins/compass/skills/planning/SKILL.md +436 -0
- package/dist/plugins/counsel/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/counsel/agents/ai-consultant.md +999 -0
- package/dist/plugins/counsel/agents/competitive-analyst.md +688 -0
- package/dist/plugins/counsel/agents/meeting-prep.md +318 -0
- package/dist/plugins/counsel/agents/stakeholder-reporter.md +94 -0
- package/dist/plugins/counsel/hooks/check-deliverable.sh +65 -0
- package/dist/plugins/counsel/hooks/ensure-client-dir.sh +59 -0
- package/dist/plugins/counsel/hooks/hooks.json +28 -0
- package/dist/plugins/counsel/skills/client-discovery/SKILL.md +266 -0
- package/dist/plugins/counsel/skills/consulting/SKILL.md +282 -0
- package/dist/plugins/counsel/skills/deliverable-builder/SKILL.md +928 -0
- package/dist/plugins/counsel/skills/engagement-tracker/SKILL.md +380 -0
- package/dist/plugins/counsel/skills/market-research/SKILL.md +300 -0
- package/dist/plugins/counsel/skills/opportunity-map/SKILL.md +307 -0
- package/dist/plugins/counsel/skills/pitch-generator/SKILL.md +378 -0
- package/dist/plugins/counsel/skills/roi-calculator/SKILL.md +469 -0
- package/dist/plugins/counsel/skills/share/SKILL.md +211 -0
- package/dist/plugins/counsel/skills/solution-architect/SKILL.md +566 -0
- package/dist/plugins/counsel/skills/templates/SKILL.md +194 -0
- package/dist/plugins/counsel/skills/welcome/SKILL.md +136 -0
- package/dist/plugins/counsel/skills/wizard/SKILL.md +411 -0
- package/dist/plugins/cruise/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/cruise/skills/autopilot/SKILL.md +425 -0
- package/dist/plugins/forge/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/forge/agents/architect.md +174 -0
- package/dist/plugins/forge/agents/code-reviewer.md +115 -0
- package/dist/plugins/forge/agents/frontend.md +261 -0
- package/dist/plugins/forge/agents/product-manager.md +563 -0
- package/dist/plugins/forge/agents/python-backend.md +286 -0
- package/dist/plugins/forge/agents/qa.md +226 -0
- package/dist/plugins/forge/hooks/hooks.json +28 -0
- package/dist/plugins/forge/hooks/post-test-summary.sh +115 -0
- package/dist/plugins/forge/hooks/triage-router.sh +740 -0
- package/dist/plugins/forge/skills/implement/SKILL.md +532 -0
- package/dist/plugins/forge/skills/planning/SKILL.md +436 -0
- package/dist/plugins/forge/skills/pr/SKILL.md +275 -0
- package/dist/plugins/forge/skills/precheck/SKILL.md +159 -0
- package/dist/plugins/forge/skills/qa/SKILL.md +127 -0
- package/dist/plugins/forge/skills/review-pr/SKILL.md +367 -0
- package/dist/plugins/prime/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/prime/agents/ai-consultant.md +999 -0
- package/dist/plugins/prime/agents/architect.md +174 -0
- package/dist/plugins/prime/agents/code-reviewer.md +115 -0
- package/dist/plugins/prime/agents/competitive-analyst.md +688 -0
- package/dist/plugins/prime/agents/content-strategist.md +607 -0
- package/dist/plugins/prime/agents/design-studio-create.md +304 -0
- package/dist/plugins/prime/agents/design-studio-critique.md +258 -0
- package/dist/plugins/prime/agents/design-studio-think.md +79 -0
- package/dist/plugins/prime/agents/explore-light.md +52 -0
- package/dist/plugins/prime/agents/frontend.md +261 -0
- package/dist/plugins/prime/agents/gtm-expert.md +811 -0
- package/dist/plugins/prime/agents/meeting-prep.md +318 -0
- package/dist/plugins/prime/agents/ops.md +149 -0
- package/dist/plugins/prime/agents/product-manager.md +563 -0
- package/dist/plugins/prime/agents/python-backend.md +286 -0
- package/dist/plugins/prime/agents/qa-baseline-updater.md +45 -0
- package/dist/plugins/prime/agents/qa-challenger.md +97 -0
- package/dist/plugins/prime/agents/qa-domain.md +145 -0
- package/dist/plugins/prime/agents/qa-e2e.md +184 -0
- package/dist/plugins/prime/agents/qa-test-promoter.md +97 -0
- package/dist/plugins/prime/agents/qa.md +226 -0
- package/dist/plugins/prime/agents/setup.md +134 -0
- package/dist/plugins/prime/agents/sre.md +165 -0
- package/dist/plugins/prime/agents/stakeholder-reporter.md +94 -0
- package/dist/plugins/prime/agents/user-researcher.md +602 -0
- package/dist/plugins/prime/hooks/check-deliverable.sh +65 -0
- package/dist/plugins/prime/hooks/ensure-client-dir.sh +59 -0
- package/dist/plugins/prime/hooks/hooks.json +184 -0
- package/dist/plugins/prime/hooks/post-deploy-health.sh +83 -0
- package/dist/plugins/prime/hooks/post-diff-test-compare.sh +125 -0
- package/dist/plugins/prime/hooks/post-edit-lint.sh +92 -0
- package/dist/plugins/prime/hooks/post-git-state.sh +54 -0
- package/dist/plugins/prime/hooks/post-merge-cleanup.sh +101 -0
- package/dist/plugins/prime/hooks/post-test-summary.sh +115 -0
- package/dist/plugins/prime/hooks/pre-bash-guard.sh +142 -0
- package/dist/plugins/prime/hooks/pre-edit-guard.sh +121 -0
- package/dist/plugins/prime/hooks/pre-task-context.sh +113 -0
- package/dist/plugins/prime/hooks/session-bootstrap.sh +379 -0
- package/dist/plugins/prime/hooks/session-end.sh +107 -0
- package/dist/plugins/prime/hooks/session-summary.sh +97 -0
- package/dist/plugins/prime/hooks/sync-agents.sh +269 -0
- package/dist/plugins/prime/hooks/triage-router.sh +740 -0
- package/dist/plugins/prime/skills/arth/SKILL.md +165 -0
- package/dist/plugins/prime/skills/autopilot/SKILL.md +425 -0
- package/dist/plugins/prime/skills/calibrate/SKILL.md +1807 -0
- package/dist/plugins/prime/skills/ci-fix/SKILL.md +293 -0
- package/dist/plugins/prime/skills/client-discovery/SKILL.md +266 -0
- package/dist/plugins/prime/skills/consulting/SKILL.md +282 -0
- package/dist/plugins/prime/skills/custom-domain/SKILL.md +261 -0
- package/dist/plugins/prime/skills/deliverable-builder/SKILL.md +928 -0
- package/dist/plugins/prime/skills/discord-ops/SKILL.md +125 -0
- package/dist/plugins/prime/skills/engagement-tracker/SKILL.md +380 -0
- package/dist/plugins/prime/skills/explore.md +43 -0
- package/dist/plugins/prime/skills/fix/SKILL.md +1058 -0
- package/dist/plugins/prime/skills/implement/SKILL.md +532 -0
- package/dist/plugins/prime/skills/incident/SKILL.md +910 -0
- package/dist/plugins/prime/skills/issue/SKILL.md +134 -0
- package/dist/plugins/prime/skills/market-research/SKILL.md +300 -0
- package/dist/plugins/prime/skills/onboard/SKILL.md +344 -0
- package/dist/plugins/prime/skills/opportunity-map/SKILL.md +307 -0
- package/dist/plugins/prime/skills/pitch-generator/SKILL.md +378 -0
- package/dist/plugins/prime/skills/planning/SKILL.md +436 -0
- package/dist/plugins/prime/skills/pr/SKILL.md +275 -0
- package/dist/plugins/prime/skills/precheck/SKILL.md +159 -0
- package/dist/plugins/prime/skills/qa/SKILL.md +127 -0
- package/dist/plugins/prime/skills/qa-incident/SKILL.md +54 -0
- package/dist/plugins/prime/skills/qa-learn/SKILL.md +47 -0
- package/dist/plugins/prime/skills/restart/SKILL.md +70 -0
- package/dist/plugins/prime/skills/review-pr/SKILL.md +367 -0
- package/dist/plugins/prime/skills/roi-calculator/SKILL.md +469 -0
- package/dist/plugins/prime/skills/scan/SKILL.md +232 -0
- package/dist/plugins/prime/skills/setup/SKILL.md +691 -0
- package/dist/plugins/prime/skills/share/SKILL.md +211 -0
- package/dist/plugins/prime/skills/solution-architect/SKILL.md +566 -0
- package/dist/plugins/prime/skills/sre/SKILL.md +362 -0
- package/dist/plugins/prime/skills/sync/SKILL.md +188 -0
- package/dist/plugins/prime/skills/templates/SKILL.md +194 -0
- package/dist/plugins/prime/skills/welcome/SKILL.md +136 -0
- package/dist/plugins/prime/skills/wizard/SKILL.md +411 -0
- package/dist/plugins/prism/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/prism/agents/qa-baseline-updater.md +45 -0
- package/dist/plugins/prism/agents/qa-challenger.md +97 -0
- package/dist/plugins/prism/agents/qa-domain.md +145 -0
- package/dist/plugins/prism/agents/qa-e2e.md +184 -0
- package/dist/plugins/prism/agents/qa-test-promoter.md +97 -0
- package/dist/plugins/prism/agents/qa.md +226 -0
- package/dist/plugins/prism/hooks/hooks.json +26 -0
- package/dist/plugins/prism/hooks/post-diff-test-compare.sh +125 -0
- package/dist/plugins/prism/hooks/post-test-summary.sh +115 -0
- package/dist/plugins/prism/skills/qa/SKILL.md +127 -0
- package/dist/plugins/prism/skills/qa-incident/SKILL.md +54 -0
- package/dist/plugins/prism/skills/qa-learn/SKILL.md +47 -0
- package/dist/plugins/scalpel/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/scalpel/agents/code-reviewer.md +115 -0
- package/dist/plugins/scalpel/hooks/hooks.json +26 -0
- package/dist/plugins/scalpel/hooks/pre-edit-guard.sh +121 -0
- package/dist/plugins/scalpel/skills/ci-fix/SKILL.md +293 -0
- package/dist/plugins/scalpel/skills/fix/SKILL.md +1058 -0
- package/dist/plugins/scalpel/skills/issue/SKILL.md +134 -0
- package/dist/plugins/sentinel/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/sentinel/agents/ops.md +149 -0
- package/dist/plugins/sentinel/agents/sre.md +165 -0
- package/dist/plugins/sentinel/hooks/hooks.json +26 -0
- package/dist/plugins/sentinel/hooks/post-deploy-health.sh +83 -0
- package/dist/plugins/sentinel/hooks/post-git-state.sh +54 -0
- package/dist/plugins/sentinel/skills/incident/SKILL.md +910 -0
- package/dist/plugins/sentinel/skills/restart/SKILL.md +70 -0
- package/dist/plugins/sentinel/skills/sre/SKILL.md +362 -0
- package/dist/plugins/shield/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/shield/hooks/hooks.json +60 -0
- package/dist/plugins/shield/hooks/pre-bash-guard.sh +142 -0
- package/dist/plugins/shield/hooks/pre-edit-guard.sh +121 -0
- package/dist/plugins/shield/hooks/session-bootstrap.sh +379 -0
- package/dist/plugins/shield/hooks/triage-router.sh +740 -0
- package/dist/plugins/spark/.claude-plugin/plugin.json +6 -0
- package/dist/plugins/spark/agents/explore-light.md +52 -0
- package/dist/plugins/spark/agents/setup.md +134 -0
- package/dist/plugins/spark/hooks/hooks.json +16 -0
- package/dist/plugins/spark/hooks/session-bootstrap.sh +379 -0
- package/dist/plugins/spark/skills/calibrate/SKILL.md +1807 -0
- package/dist/plugins/spark/skills/onboard/SKILL.md +344 -0
- package/dist/plugins/spark/skills/scan/SKILL.md +232 -0
- package/dist/plugins/spark/skills/setup/SKILL.md +691 -0
- package/hook-defs.json +104 -0
- package/hooks/check-deliverable.sh +65 -0
- package/hooks/ensure-client-dir.sh +59 -0
- package/hooks/hooks.json +16 -0
- package/hooks/post-deploy-health.sh +83 -0
- package/hooks/post-diff-test-compare.sh +125 -0
- package/hooks/post-edit-lint.sh +92 -0
- package/hooks/post-git-state.sh +54 -0
- package/hooks/post-merge-cleanup.sh +101 -0
- package/hooks/post-test-summary.sh +115 -0
- package/hooks/pre-bash-guard.sh +142 -0
- package/hooks/pre-edit-guard.sh +121 -0
- package/hooks/pre-task-context.sh +113 -0
- package/hooks/session-bootstrap.sh +379 -0
- package/hooks/session-end.sh +107 -0
- package/hooks/session-start.sh +46 -0
- package/hooks/session-summary.sh +97 -0
- package/hooks/sync-agents.sh +269 -0
- package/hooks/triage-router.sh +740 -0
- package/install.sh +3185 -0
- package/package.json +40 -0
- package/portable.manifest +112 -0
- package/skills/arth/SKILL.md +165 -0
- package/skills/autopilot/SKILL.md +425 -0
- package/skills/calibrate/SKILL.md +1807 -0
- package/skills/ci-fix/SKILL.md +293 -0
- package/skills/client-discovery/SKILL.md +266 -0
- package/skills/consulting/SKILL.md +282 -0
- package/skills/continue/SKILL.md +174 -0
- package/skills/custom-domain/SKILL.md +261 -0
- package/skills/deliverable-builder/SKILL.md +928 -0
- package/skills/discord-ops/SKILL.md +125 -0
- package/skills/engagement-tracker/SKILL.md +380 -0
- package/skills/explore.md +43 -0
- package/skills/fix/SKILL.md +1058 -0
- package/skills/implement/SKILL.md +532 -0
- package/skills/incident/SKILL.md +910 -0
- package/skills/issue/SKILL.md +134 -0
- package/skills/market-research/SKILL.md +300 -0
- package/skills/onboard/SKILL.md +344 -0
- package/skills/opportunity-map/SKILL.md +307 -0
- package/skills/pitch-generator/SKILL.md +378 -0
- package/skills/planning/SKILL.md +436 -0
- package/skills/pr/SKILL.md +275 -0
- package/skills/precheck/SKILL.md +159 -0
- package/skills/qa/SKILL.md +127 -0
- package/skills/qa-incident/SKILL.md +54 -0
- package/skills/qa-learn/SKILL.md +47 -0
- package/skills/railway/central-station/SKILL.md +226 -0
- package/skills/railway/central-station/references/environment-config.md +183 -0
- package/skills/railway/central-station/references/monorepo.md +216 -0
- package/skills/railway/central-station/references/railpack.md +257 -0
- package/skills/railway/central-station/references/variables.md +170 -0
- package/skills/railway/database/SKILL.md +284 -0
- package/skills/railway/database/references/environment-config.md +183 -0
- package/skills/railway/database/references/monorepo.md +216 -0
- package/skills/railway/database/references/railpack.md +257 -0
- package/skills/railway/database/references/variables.md +170 -0
- package/skills/railway/database/scripts/railway-api.sh +41 -0
- package/skills/railway/deploy/SKILL.md +128 -0
- package/skills/railway/deploy/references/environment-config.md +183 -0
- package/skills/railway/deploy/references/monorepo.md +216 -0
- package/skills/railway/deploy/references/railpack.md +257 -0
- package/skills/railway/deploy/references/variables.md +170 -0
- package/skills/railway/deployment/SKILL.md +222 -0
- package/skills/railway/deployment/references/environment-config.md +183 -0
- package/skills/railway/deployment/references/monorepo.md +216 -0
- package/skills/railway/deployment/references/railpack.md +257 -0
- package/skills/railway/deployment/references/variables.md +170 -0
- package/skills/railway/domain/SKILL.md +137 -0
- package/skills/railway/domain/references/environment-config.md +183 -0
- package/skills/railway/domain/references/monorepo.md +216 -0
- package/skills/railway/domain/references/railpack.md +257 -0
- package/skills/railway/domain/references/variables.md +170 -0
- package/skills/railway/environment/SKILL.md +266 -0
- package/skills/railway/environment/references/environment-config.md +183 -0
- package/skills/railway/environment/references/monorepo.md +216 -0
- package/skills/railway/environment/references/railpack.md +257 -0
- package/skills/railway/environment/references/variables.md +170 -0
- package/skills/railway/metrics/SKILL.md +211 -0
- package/skills/railway/metrics/references/environment-config.md +183 -0
- package/skills/railway/metrics/references/monorepo.md +216 -0
- package/skills/railway/metrics/references/railpack.md +257 -0
- package/skills/railway/metrics/references/variables.md +170 -0
- package/skills/railway/metrics/scripts/railway-api.sh +41 -0
- package/skills/railway/new/SKILL.md +489 -0
- package/skills/railway/new/references/environment-config.md +183 -0
- package/skills/railway/new/references/monorepo.md +216 -0
- package/skills/railway/new/references/railpack.md +257 -0
- package/skills/railway/new/references/variables.md +170 -0
- package/skills/railway/projects/SKILL.md +142 -0
- package/skills/railway/projects/references/environment-config.md +183 -0
- package/skills/railway/projects/references/monorepo.md +216 -0
- package/skills/railway/projects/references/railpack.md +257 -0
- package/skills/railway/projects/references/variables.md +170 -0
- package/skills/railway/projects/scripts/railway-api.sh +41 -0
- package/skills/railway/railway-docs/SKILL.md +47 -0
- package/skills/railway/railway-docs/references/environment-config.md +183 -0
- package/skills/railway/railway-docs/references/monorepo.md +216 -0
- package/skills/railway/railway-docs/references/railpack.md +257 -0
- package/skills/railway/railway-docs/references/variables.md +170 -0
- package/skills/railway/service/SKILL.md +249 -0
- package/skills/railway/service/references/environment-config.md +183 -0
- package/skills/railway/service/references/monorepo.md +216 -0
- package/skills/railway/service/references/railpack.md +257 -0
- package/skills/railway/service/references/variables.md +170 -0
- package/skills/railway/service/scripts/railway-api.sh +41 -0
- package/skills/railway/status/SKILL.md +91 -0
- package/skills/railway/status/references/environment-config.md +183 -0
- package/skills/railway/status/references/monorepo.md +216 -0
- package/skills/railway/status/references/railpack.md +257 -0
- package/skills/railway/status/references/variables.md +170 -0
- package/skills/railway/templates/SKILL.md +275 -0
- package/skills/railway/templates/references/environment-config.md +183 -0
- package/skills/railway/templates/references/monorepo.md +216 -0
- package/skills/railway/templates/references/railpack.md +257 -0
- package/skills/railway/templates/references/variables.md +170 -0
- package/skills/railway/templates/scripts/railway-api.sh +41 -0
- package/skills/restart/SKILL.md +70 -0
- package/skills/review-pr/SKILL.md +367 -0
- package/skills/roi-calculator/SKILL.md +469 -0
- package/skills/scan/SKILL.md +232 -0
- package/skills/setup/SKILL.md +691 -0
- package/skills/share/SKILL.md +211 -0
- package/skills/solution-architect/SKILL.md +566 -0
- package/skills/sre/SKILL.md +362 -0
- package/skills/superpowers/brainstorming/SKILL.md +96 -0
- package/skills/superpowers/dispatching-parallel-agents/SKILL.md +180 -0
- package/skills/superpowers/executing-plans/SKILL.md +84 -0
- package/skills/superpowers/finishing-a-development-branch/SKILL.md +200 -0
- package/skills/superpowers/receiving-code-review/SKILL.md +213 -0
- package/skills/superpowers/requesting-code-review/SKILL.md +105 -0
- package/skills/superpowers/requesting-code-review/code-reviewer.md +146 -0
- package/skills/superpowers/subagent-driven-development/SKILL.md +242 -0
- package/skills/superpowers/subagent-driven-development/code-quality-reviewer-prompt.md +20 -0
- package/skills/superpowers/subagent-driven-development/implementer-prompt.md +78 -0
- package/skills/superpowers/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/skills/superpowers/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/superpowers/systematic-debugging/SKILL.md +296 -0
- package/skills/superpowers/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/superpowers/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/superpowers/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/superpowers/systematic-debugging/find-polluter.sh +63 -0
- package/skills/superpowers/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/superpowers/systematic-debugging/test-academic.md +14 -0
- package/skills/superpowers/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/superpowers/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/superpowers/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/superpowers/test-driven-development/SKILL.md +371 -0
- package/skills/superpowers/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/superpowers/using-git-worktrees/SKILL.md +218 -0
- package/skills/superpowers/using-superpowers/SKILL.md +95 -0
- package/skills/superpowers/verification-before-completion/SKILL.md +139 -0
- package/skills/superpowers/writing-plans/SKILL.md +116 -0
- package/skills/superpowers/writing-skills/SKILL.md +655 -0
- package/skills/superpowers/writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/superpowers/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/superpowers/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/superpowers/writing-skills/persuasion-principles.md +187 -0
- package/skills/superpowers/writing-skills/render-graphs.js +168 -0
- package/skills/superpowers/writing-skills/testing-skills-with-subagents.md +384 -0
- package/skills/sync/SKILL.md +188 -0
- package/skills/templates/SKILL.md +194 -0
- package/skills/welcome/SKILL.md +136 -0
- package/skills/wizard/SKILL.md +411 -0
- package/templates/CLAUDE.md.managed-block +123 -0
- package/templates/CLAUDE.md.template +111 -0
- package/templates/consulting/engagement-tracker-template.md +181 -0
- package/templates/consulting/executive-summary-template.md +83 -0
- package/templates/consulting/maturity-assessment-template.md +182 -0
- package/templates/consulting/proposal-template.md +209 -0
- package/templates/consulting/roi-model-template.md +139 -0
- package/templates/consulting/solution-architecture-template.md +313 -0
- package/templates/settings.json +130 -0
package/install.sh
ADDED
|
@@ -0,0 +1,3185 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Agents — Symlink Install System
|
|
3
|
+
#
|
|
4
|
+
# Installs portable agents, skills, and hooks into any project's .claude/
|
|
5
|
+
# directory using symlinks. Changes to the repo propagate automatically.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# install.sh [path] Sync symlinks into project (auto-detects setup needs)
|
|
9
|
+
# install.sh --setup [path] Interactive setup (pick categories, configure hooks)
|
|
10
|
+
# install.sh --uninstall [path] Clean uninstall (restore everything)
|
|
11
|
+
# install.sh --sync-from-config [path] Sync only configured categories (used by hook)
|
|
12
|
+
# install.sh --init [path] Scaffold + sync (greenfield)
|
|
13
|
+
# install.sh --convert [path] One-time migration: replace matching copies with symlinks
|
|
14
|
+
# install.sh --assess [path] Brownfield assessment (read-only, no license needed)
|
|
15
|
+
# install.sh --upgrade [path] Brownfield upgrade (converts stale/identical to symlinks)
|
|
16
|
+
# install.sh --generate-config [path] Generate config from existing .claude/ contents
|
|
17
|
+
# install.sh --status [path] Show symlink status report
|
|
18
|
+
# install.sh --key KEY Store license key
|
|
19
|
+
# install.sh --railway Include Railway sub-skills
|
|
20
|
+
# install.sh --categories cat1,cat2 Install specific categories (non-interactive)
|
|
21
|
+
# install.sh --json-output Emit JSON summary on completion
|
|
22
|
+
# install.sh --check-license-only Validate license and exit (used by sync hook)
|
|
23
|
+
# install.sh --help Show help
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
# CLAUDE_AGENTS_TOOLKIT_DIR can override SCRIPT_DIR (used by test harness in CI
|
|
28
|
+
# where ~/.claude-agents is a symlink and BASH_SOURCE resolves to physical path)
|
|
29
|
+
SCRIPT_DIR="${CLAUDE_AGENTS_TOOLKIT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
|
30
|
+
MANIFEST="$SCRIPT_DIR/portable.manifest"
|
|
31
|
+
LICENSE_FILE="$SCRIPT_DIR/.license"
|
|
32
|
+
AUTHORIZED_KEYS="$SCRIPT_DIR/authorized-keys.txt"
|
|
33
|
+
|
|
34
|
+
# Config & backup constants
|
|
35
|
+
CONFIG_NAME=".claude-agents.conf"
|
|
36
|
+
BACKUP_DIR_NAME=".claude-agents-backup"
|
|
37
|
+
GITIGNORE_MARKER_START="# >>> claude-agents managed (DO NOT EDIT THIS BLOCK) >>>"
|
|
38
|
+
GITIGNORE_MARKER_END="# <<< claude-agents managed <<<"
|
|
39
|
+
CLAUDEMD_MARKER_START="<!-- >>> claude-agents toolkit (DO NOT EDIT THIS BLOCK) >>> -->"
|
|
40
|
+
CLAUDEMD_MARKER_END="<!-- <<< claude-agents toolkit <<< -->"
|
|
41
|
+
CLAUDEMD_MANAGED_TEMPLATE="$SCRIPT_DIR/templates/CLAUDE.md.managed-block"
|
|
42
|
+
|
|
43
|
+
# Colors
|
|
44
|
+
RED='\033[0;31m'
|
|
45
|
+
GREEN='\033[0;32m'
|
|
46
|
+
YELLOW='\033[0;33m'
|
|
47
|
+
CYAN='\033[0;36m'
|
|
48
|
+
BOLD='\033[1m'
|
|
49
|
+
DIM='\033[2m'
|
|
50
|
+
NC='\033[0m' # No Color
|
|
51
|
+
|
|
52
|
+
# Counters
|
|
53
|
+
CREATED=0
|
|
54
|
+
UPDATED=0
|
|
55
|
+
SKIPPED=0
|
|
56
|
+
CONVERTED=0
|
|
57
|
+
UPGRADED=0
|
|
58
|
+
ERRORS=0
|
|
59
|
+
REMOVED=0
|
|
60
|
+
KEPT=0
|
|
61
|
+
STALE_DEFERRED=()
|
|
62
|
+
SKIP_ITEMS=""
|
|
63
|
+
FORCE_YES=false
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# License Validation
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
check_license() {
|
|
70
|
+
if [ ! -f "$LICENSE_FILE" ]; then
|
|
71
|
+
echo -e "${RED}No license key found.${NC}"
|
|
72
|
+
echo "Run: $0 --key YOUR_KEY"
|
|
73
|
+
echo ""
|
|
74
|
+
echo "Get a license key at https://github.com/ArthTech-AI/claude-agents or contact support."
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
local key
|
|
79
|
+
key=$(cat "$LICENSE_FILE")
|
|
80
|
+
|
|
81
|
+
# Validate key format: ARTH-XXXX-XXXX-XXXX-XXXX
|
|
82
|
+
if ! echo "$key" | grep -qE '^ARTH-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'; then
|
|
83
|
+
echo -e "${RED}Invalid license key format.${NC} Expected: ARTH-XXXX-XXXX-XXXX-XXXX"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Phase 1: Check against local hashed allowlist
|
|
88
|
+
if [ ! -f "$AUTHORIZED_KEYS" ]; then
|
|
89
|
+
echo -e "${RED}Authorization file missing.${NC} Re-clone the repo."
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
local key_hash
|
|
94
|
+
key_hash=$(echo -n "$key" | shasum -a 256 | awk '{print $1}')
|
|
95
|
+
|
|
96
|
+
if ! grep -v '^#' "$AUTHORIZED_KEYS" | grep -q "$key_hash"; then
|
|
97
|
+
echo -e "${RED}Invalid license key.${NC} Contact support."
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Category Definitions
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Maps categories to their manifest entry paths.
|
|
106
|
+
# Categories: core, agents, skills, hooks, railway
|
|
107
|
+
|
|
108
|
+
get_category_items() {
|
|
109
|
+
case "$1" in
|
|
110
|
+
core)
|
|
111
|
+
echo "agents/explore-light.md skills/sync skills/scan skills/calibrate hooks/sync-agents.sh"
|
|
112
|
+
;;
|
|
113
|
+
strategy)
|
|
114
|
+
echo "agents/architect.md agents/code-reviewer.md agents/design-studio-create.md agents/design-studio-critique.md agents/design-studio-think.md agents/gtm-expert.md agents/product-manager.md agents/stakeholder-reporter.md agents/meeting-prep.md agents/content-strategist.md agents/user-researcher.md agents/competitive-analyst.md"
|
|
115
|
+
;;
|
|
116
|
+
development)
|
|
117
|
+
echo "agents/frontend.md agents/python-backend.md agents/ops.md skills/planning skills/implement skills/fix skills/pr skills/precheck skills/review-pr skills/issue"
|
|
118
|
+
;;
|
|
119
|
+
quality)
|
|
120
|
+
echo "agents/qa.md agents/qa-e2e.md agents/qa-challenger.md agents/qa-test-promoter.md agents/qa-baseline-updater.md agents/qa-domain.md skills/qa skills/ci-fix skills/qa-incident skills/qa-learn"
|
|
121
|
+
;;
|
|
122
|
+
operations)
|
|
123
|
+
echo "agents/sre.md agents/setup.md skills/sre skills/incident skills/setup skills/restart skills/discord-ops skills/arth skills/custom-domain"
|
|
124
|
+
;;
|
|
125
|
+
hooks)
|
|
126
|
+
echo "hooks/triage-router.sh hooks/session-end.sh"
|
|
127
|
+
;;
|
|
128
|
+
guardrails)
|
|
129
|
+
echo "hooks/session-bootstrap.sh hooks/pre-bash-guard.sh hooks/pre-task-context.sh hooks/pre-edit-guard.sh hooks/post-test-summary.sh hooks/post-edit-lint.sh hooks/post-deploy-health.sh hooks/post-diff-test-compare.sh hooks/post-git-state.sh hooks/post-merge-cleanup.sh skills/onboard skills/welcome skills/share skills/templates skills/wizard skills/autopilot skills/continue"
|
|
130
|
+
;;
|
|
131
|
+
railway)
|
|
132
|
+
echo "skills/railway/central-station skills/railway/database skills/railway/deploy skills/railway/deployment skills/railway/domain skills/railway/environment skills/railway/metrics skills/railway/new skills/railway/projects skills/railway/railway-docs skills/railway/service skills/railway/status skills/railway/templates"
|
|
133
|
+
;;
|
|
134
|
+
superpowers)
|
|
135
|
+
echo "skills/superpowers skills/explore.md"
|
|
136
|
+
;;
|
|
137
|
+
consulting)
|
|
138
|
+
echo "agents/ai-consultant.md skills/consulting skills/client-discovery skills/opportunity-map skills/pitch-generator skills/roi-calculator skills/market-research skills/solution-architect skills/deliverable-builder skills/engagement-tracker hooks/ensure-client-dir.sh hooks/check-deliverable.sh hooks/session-summary.sh"
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get_category_label() {
|
|
144
|
+
case "$1" in
|
|
145
|
+
core) echo "Core (required)" ;;
|
|
146
|
+
strategy) echo "Strategy & Design (12 agents)" ;;
|
|
147
|
+
development) echo "Development (3 agents, 6 skills)" ;;
|
|
148
|
+
quality) echo "Quality Assurance (6 agents, 4 skills)" ;;
|
|
149
|
+
operations) echo "Operations (2 agents, 5 skills)" ;;
|
|
150
|
+
hooks) echo "Hooks (3)" ;;
|
|
151
|
+
guardrails) echo "Guardrails (8 hooks, 7 skills)" ;;
|
|
152
|
+
railway) echo "Railway (13 skills, 1 hook)" ;;
|
|
153
|
+
superpowers) echo "Superpowers (2 skills)" ;;
|
|
154
|
+
consulting) echo "Consulting (1 agent, 9 skills, 3 hooks)" ;;
|
|
155
|
+
esac
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get_category_desc() {
|
|
159
|
+
case "$1" in
|
|
160
|
+
core) echo "Sync, codebase scanner, exploration, auto-update hook" ;;
|
|
161
|
+
strategy) echo "Architect, code-review, design-studio, PM, GTM, competitive-analyst, stakeholder-reporter, meeting-prep, content-strategist, user-researcher agents" ;;
|
|
162
|
+
development) echo "Frontend, backend, ops agents + planning, implement, fix, PR, review-pr, issue skills" ;;
|
|
163
|
+
quality) echo "QA orchestrator, E2E, challenger, promoter, baseline, domain + QA skills" ;;
|
|
164
|
+
operations) echo "SRE + setup agents, SRE, setup, restart, discord-ops, arth, custom-domain skills" ;;
|
|
165
|
+
hooks) echo "Triage router (cost delegation) + git worktree sync + session-end memory" ;;
|
|
166
|
+
guardrails) echo "Safety guards, auto-lint, test summaries, deploy checks, onboard, welcome, share, templates, wizard, autopilot, continue" ;;
|
|
167
|
+
railway) echo "Railway deployment skills (deploy, database, domain, etc.)" ;;
|
|
168
|
+
superpowers) echo "Superpowers + deep explore skills" ;;
|
|
169
|
+
consulting) echo "AI consulting agent, client discovery, opportunity mapping, pitch generation, ROI calculator" ;;
|
|
170
|
+
esac
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get_category_default() {
|
|
174
|
+
case "$1" in
|
|
175
|
+
guardrails) echo "1" ;;
|
|
176
|
+
railway) echo "0" ;;
|
|
177
|
+
superpowers) echo "0" ;;
|
|
178
|
+
consulting) echo "0" ;;
|
|
179
|
+
*) echo "1" ;;
|
|
180
|
+
esac
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Returns the settings.json hook names that correspond to a category
|
|
184
|
+
get_category_hooks() {
|
|
185
|
+
case "$1" in
|
|
186
|
+
core) echo "sync-agents" ;;
|
|
187
|
+
hooks) echo "triage-router session-end" ;;
|
|
188
|
+
guardrails) echo "session-bootstrap pre-bash-guard pre-task-context pre-edit-guard pre-write-guard post-test-summary post-edit-lint post-deploy-health post-diff-test-compare post-merge-cleanup" ;;
|
|
189
|
+
*) echo "" ;;
|
|
190
|
+
esac
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Build combined item list for a space-separated list of categories
|
|
194
|
+
items_for_categories() {
|
|
195
|
+
local categories="$1"
|
|
196
|
+
local all_items=""
|
|
197
|
+
for cat in $categories; do
|
|
198
|
+
local items
|
|
199
|
+
items=$(get_category_items "$cat")
|
|
200
|
+
all_items="$all_items $items"
|
|
201
|
+
done
|
|
202
|
+
echo "$all_items" | sed 's/^ *//' | sed 's/ *$//'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Build combined settings hook list for categories
|
|
206
|
+
hooks_for_categories() {
|
|
207
|
+
local categories="$1"
|
|
208
|
+
local all_hooks=""
|
|
209
|
+
for cat in $categories; do
|
|
210
|
+
local h
|
|
211
|
+
h=$(get_category_hooks "$cat")
|
|
212
|
+
if [ -n "$h" ]; then
|
|
213
|
+
all_hooks="$all_hooks $h"
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
216
|
+
echo "$all_hooks" | sed 's/^ *//' | sed 's/ *$//'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# ============================================================================
|
|
220
|
+
# Config System
|
|
221
|
+
# ============================================================================
|
|
222
|
+
|
|
223
|
+
config_path() {
|
|
224
|
+
echo "$1/.claude/$CONFIG_NAME"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
config_exists() {
|
|
228
|
+
[ -f "$(config_path "$1")" ]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Sources config into CONF_* variables
|
|
232
|
+
load_config() {
|
|
233
|
+
local conf_file
|
|
234
|
+
conf_file=$(config_path "$1")
|
|
235
|
+
if [ ! -f "$conf_file" ]; then
|
|
236
|
+
return 1
|
|
237
|
+
fi
|
|
238
|
+
# Source the file — sets INSTALLED_CATEGORIES, INSTALLED_AGENTS, etc.
|
|
239
|
+
# shellcheck disable=SC1090
|
|
240
|
+
. "$conf_file"
|
|
241
|
+
# Export as CONF_ prefixed for clarity
|
|
242
|
+
CONF_INSTALLED_CATEGORIES="${INSTALLED_CATEGORIES:-}"
|
|
243
|
+
CONF_INSTALLED_AGENTS="${INSTALLED_AGENTS:-}"
|
|
244
|
+
CONF_INSTALLED_SKILLS="${INSTALLED_SKILLS:-}"
|
|
245
|
+
CONF_INSTALLED_HOOKS="${INSTALLED_HOOKS:-}"
|
|
246
|
+
CONF_INSTALLED_RAILWAY="${INSTALLED_RAILWAY:-}"
|
|
247
|
+
CONF_SETTINGS_HOOKS_ADDED="${SETTINGS_HOOKS_ADDED:-}"
|
|
248
|
+
CONF_BACKUP_EXISTS="${BACKUP_EXISTS:-false}"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
write_config() {
|
|
252
|
+
local target_dir="$1"
|
|
253
|
+
local categories="$2" # space-separated
|
|
254
|
+
local conf_file
|
|
255
|
+
conf_file=$(config_path "$target_dir")
|
|
256
|
+
|
|
257
|
+
# Derive individual lists from categories
|
|
258
|
+
local agents="" skills="" hooks_list="" railway=""
|
|
259
|
+
for cat in $categories; do
|
|
260
|
+
local items
|
|
261
|
+
items=$(get_category_items "$cat")
|
|
262
|
+
for item in $items; do
|
|
263
|
+
local bname
|
|
264
|
+
bname=$(basename "$item")
|
|
265
|
+
bname="${bname%.md}"
|
|
266
|
+
bname="${bname%.sh}"
|
|
267
|
+
case "$item" in
|
|
268
|
+
agents/*)
|
|
269
|
+
agents="$agents $bname"
|
|
270
|
+
;;
|
|
271
|
+
skills/railway/*)
|
|
272
|
+
railway="$railway $bname"
|
|
273
|
+
;;
|
|
274
|
+
skills/*)
|
|
275
|
+
skills="$skills $bname"
|
|
276
|
+
;;
|
|
277
|
+
hooks/*)
|
|
278
|
+
hooks_list="$hooks_list $bname"
|
|
279
|
+
;;
|
|
280
|
+
esac
|
|
281
|
+
done
|
|
282
|
+
done
|
|
283
|
+
|
|
284
|
+
# Trim leading spaces
|
|
285
|
+
agents="${agents# }"
|
|
286
|
+
skills="${skills# }"
|
|
287
|
+
hooks_list="${hooks_list# }"
|
|
288
|
+
railway="${railway# }"
|
|
289
|
+
|
|
290
|
+
# Settings hooks we manage
|
|
291
|
+
local settings_hooks
|
|
292
|
+
settings_hooks=$(hooks_for_categories "$categories")
|
|
293
|
+
|
|
294
|
+
# Backup status
|
|
295
|
+
local backup_exists="false"
|
|
296
|
+
if [ -d "$target_dir/.claude/$BACKUP_DIR_NAME" ]; then
|
|
297
|
+
backup_exists="true"
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
cat > "$conf_file" << CONFEOF
|
|
301
|
+
# claude-agents project config — generated by install.sh
|
|
302
|
+
# Re-run: install.sh --setup $(basename "$target_dir")
|
|
303
|
+
INSTALLED_CATEGORIES="$categories"
|
|
304
|
+
INSTALLED_AGENTS="$agents"
|
|
305
|
+
INSTALLED_SKILLS="$skills"
|
|
306
|
+
INSTALLED_HOOKS="$hooks_list"
|
|
307
|
+
INSTALLED_RAILWAY="$railway"
|
|
308
|
+
INSTALLED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
309
|
+
SETTINGS_HOOKS_ADDED="$settings_hooks"
|
|
310
|
+
BACKUP_EXISTS=$backup_exists
|
|
311
|
+
CONFEOF
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# ============================================================================
|
|
315
|
+
# JSON Output
|
|
316
|
+
# ============================================================================
|
|
317
|
+
|
|
318
|
+
emit_json_summary() {
|
|
319
|
+
local mode="${1:-sync}"
|
|
320
|
+
local target="${2:-}"
|
|
321
|
+
local conf_file
|
|
322
|
+
conf_file=$(config_path "$target")
|
|
323
|
+
local categories=""
|
|
324
|
+
if config_exists "$target"; then
|
|
325
|
+
load_config "$target"
|
|
326
|
+
categories="${CONF_INSTALLED_CATEGORIES:-}"
|
|
327
|
+
fi
|
|
328
|
+
printf '{"status":"success","mode":"%s","target":"%s","stats":{"created":%d,"updated":%d,"skipped":%d,"errors":%d},"categories":"%s","configPath":"%s"}\n' \
|
|
329
|
+
"$mode" "$target" "$CREATED" "$UPDATED" "$SKIPPED" "$ERRORS" "$categories" "$conf_file"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# ============================================================================
|
|
333
|
+
# Backup System
|
|
334
|
+
# ============================================================================
|
|
335
|
+
|
|
336
|
+
backup_dir() {
|
|
337
|
+
echo "$1/.claude/$BACKUP_DIR_NAME"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Create backup snapshot — only on FIRST install (or if backup dir is missing)
|
|
341
|
+
snapshot_pre_install() {
|
|
342
|
+
local target_dir="$1"
|
|
343
|
+
local bdir
|
|
344
|
+
bdir=$(backup_dir "$target_dir")
|
|
345
|
+
|
|
346
|
+
if [ -d "$bdir" ]; then
|
|
347
|
+
return 0 # Backup already exists, don't overwrite
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
mkdir -p "$bdir"
|
|
351
|
+
|
|
352
|
+
# Back up settings.json if it exists
|
|
353
|
+
if [ -f "$target_dir/.claude/settings.json" ]; then
|
|
354
|
+
cp "$target_dir/.claude/settings.json" "$bdir/settings.json.bak"
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Back up .gitignore if it exists
|
|
358
|
+
if [ -f "$target_dir/.claude/.gitignore" ]; then
|
|
359
|
+
cp "$target_dir/.claude/.gitignore" "$bdir/gitignore.bak"
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
echo -e " ${DIM}Backup created at .claude/$BACKUP_DIR_NAME/${NC}" >&2
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Record all symlinks we created (written AFTER install completes)
|
|
366
|
+
post_install_record() {
|
|
367
|
+
local target_dir="$1"
|
|
368
|
+
local bdir
|
|
369
|
+
bdir=$(backup_dir "$target_dir")
|
|
370
|
+
mkdir -p "$bdir"
|
|
371
|
+
|
|
372
|
+
local manifest_file="$bdir/manifest.txt"
|
|
373
|
+
: > "$manifest_file" # Truncate
|
|
374
|
+
|
|
375
|
+
# Walk .claude/ for symlinks pointing to our repo
|
|
376
|
+
for subdir in agents skills hooks; do
|
|
377
|
+
local search_dir="$target_dir/.claude/$subdir"
|
|
378
|
+
[ ! -d "$search_dir" ] && continue
|
|
379
|
+
|
|
380
|
+
# Find symlinks (non-recursive for agents/hooks, handle skill subdirs)
|
|
381
|
+
for item in "$search_dir"/*; do
|
|
382
|
+
[ ! -e "$item" ] && [ ! -L "$item" ] && continue
|
|
383
|
+
if [ -L "$item" ]; then
|
|
384
|
+
local link_target
|
|
385
|
+
link_target=$(readlink "$item" 2>/dev/null || true)
|
|
386
|
+
if echo "$link_target" | grep -q "/.claude-agents/"; then
|
|
387
|
+
echo "$item" >> "$manifest_file"
|
|
388
|
+
fi
|
|
389
|
+
fi
|
|
390
|
+
done
|
|
391
|
+
done
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# ============================================================================
|
|
395
|
+
# Settings.json Merge (Phase 3)
|
|
396
|
+
# ============================================================================
|
|
397
|
+
# Uses embedded Python3 for safe JSON manipulation.
|
|
398
|
+
# Adds/removes hook entries without touching other settings.
|
|
399
|
+
|
|
400
|
+
merge_settings_hooks() {
|
|
401
|
+
local settings_file="$1"
|
|
402
|
+
local action="$2" # "add" or "remove"
|
|
403
|
+
local hooks_str="$3" # space-separated hook names: "sync-agents triage-router"
|
|
404
|
+
|
|
405
|
+
if [ -z "$hooks_str" ]; then
|
|
406
|
+
return 0
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
if ! command -v python3 &>/dev/null; then
|
|
410
|
+
echo -e "${YELLOW}Warning: python3 not found. Cannot auto-merge hooks into settings.json.${NC}" >&2
|
|
411
|
+
echo -e "Please manually add/remove hooks in $settings_file" >&2
|
|
412
|
+
return 1
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
python3 - "$settings_file" "$action" "$hooks_str" "$SCRIPT_DIR" <<'PYEOF'
|
|
416
|
+
import json, sys, os
|
|
417
|
+
|
|
418
|
+
settings_file = sys.argv[1]
|
|
419
|
+
action = sys.argv[2]
|
|
420
|
+
hooks_to_process = sys.argv[3].split()
|
|
421
|
+
script_dir = sys.argv[4] if len(sys.argv) > 4 else os.path.dirname(os.path.abspath(__file__))
|
|
422
|
+
|
|
423
|
+
# Load hook definitions from hook-defs.json (single source of truth)
|
|
424
|
+
# Translates entries-array format into the flat per-matcher HOOK_DEFS dict
|
|
425
|
+
# that install.sh's settings merge logic expects.
|
|
426
|
+
hook_defs_path = os.path.join(script_dir, "hook-defs.json")
|
|
427
|
+
_raw_defs = {}
|
|
428
|
+
if os.path.exists(hook_defs_path):
|
|
429
|
+
with open(hook_defs_path) as _f:
|
|
430
|
+
_raw_defs = json.load(_f)
|
|
431
|
+
|
|
432
|
+
HOOK_DEFS = {}
|
|
433
|
+
for _name, _defn in _raw_defs.items():
|
|
434
|
+
_script = _defn["script"]
|
|
435
|
+
_entries = _defn["entries"]
|
|
436
|
+
if _name == "sync-agents":
|
|
437
|
+
# sync-agents uses a special command_override (not $CLAUDE_PROJECT_DIR path)
|
|
438
|
+
_entry0 = _entries[0]
|
|
439
|
+
_cmd = _entry0.get("command_override",
|
|
440
|
+
"bash -c 'export HOME=$(eval echo ~$(id -un)); [ -x \"$HOME/.claude-agents/hooks/sync-agents.sh\" ] && \"$HOME/.claude-agents/hooks/sync-agents.sh\" || true'")
|
|
441
|
+
HOOK_DEFS["sync-agents"] = {
|
|
442
|
+
"event": _entry0["event"],
|
|
443
|
+
"entry": {"matcher": _entry0["matcher"], "hooks": [{"type": "command", "command": _cmd, "timeout": _entry0["timeout"]}]},
|
|
444
|
+
"fingerprint": _script,
|
|
445
|
+
}
|
|
446
|
+
elif len(_entries) == 1:
|
|
447
|
+
_e = _entries[0]
|
|
448
|
+
HOOK_DEFS[_name] = {
|
|
449
|
+
"event": _e["event"],
|
|
450
|
+
"entry": {"matcher": _e["matcher"], "hooks": [{"type": "command", "command": f"$CLAUDE_PROJECT_DIR/.claude/hooks/{_script}", "timeout": _e["timeout"]}]},
|
|
451
|
+
"fingerprint": _script,
|
|
452
|
+
}
|
|
453
|
+
else:
|
|
454
|
+
# Multi-entry hook (e.g. pre-edit-guard fires on both Edit and Write).
|
|
455
|
+
# Expand into separate HOOK_DEFS keys so each matcher gets its own entry.
|
|
456
|
+
for _i, _e in enumerate(_entries):
|
|
457
|
+
_key = _name if _i == 0 else f"{_name.rsplit('-', 1)[0]}-write-guard" if _e["matcher"] == "Write" else f"{_name}-{_i}"
|
|
458
|
+
HOOK_DEFS[_key] = {
|
|
459
|
+
"event": _e["event"],
|
|
460
|
+
"entry": {"matcher": _e["matcher"], "hooks": [{"type": "command", "command": f"$CLAUDE_PROJECT_DIR/.claude/hooks/{_script}", "timeout": _e["timeout"]}]},
|
|
461
|
+
"fingerprint": _script,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
# Fallback: if hook-defs.json missing, keep hardcoded sync-agents for safety
|
|
465
|
+
if not HOOK_DEFS:
|
|
466
|
+
HOOK_DEFS = {
|
|
467
|
+
"sync-agents": {
|
|
468
|
+
"event": "SessionStart",
|
|
469
|
+
"entry": {"matcher": "", "hooks": [{"type": "command", "command": "bash -c 'export HOME=$(eval echo ~$(id -un)); [ -x \"$HOME/.claude-agents/hooks/sync-agents.sh\" ] && \"$HOME/.claude-agents/hooks/sync-agents.sh\" || true'", "timeout": 30}]},
|
|
470
|
+
"fingerprint": "sync-agents.sh",
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Read or create settings
|
|
475
|
+
if os.path.exists(settings_file):
|
|
476
|
+
with open(settings_file) as f:
|
|
477
|
+
try:
|
|
478
|
+
settings = json.load(f)
|
|
479
|
+
except json.JSONDecodeError:
|
|
480
|
+
settings = {}
|
|
481
|
+
else:
|
|
482
|
+
settings = {}
|
|
483
|
+
|
|
484
|
+
if "hooks" not in settings:
|
|
485
|
+
settings["hooks"] = {}
|
|
486
|
+
|
|
487
|
+
for hook_name in hooks_to_process:
|
|
488
|
+
if hook_name not in HOOK_DEFS:
|
|
489
|
+
continue
|
|
490
|
+
defn = HOOK_DEFS[hook_name]
|
|
491
|
+
event = defn["event"]
|
|
492
|
+
fingerprint = defn["fingerprint"]
|
|
493
|
+
|
|
494
|
+
if action == "add":
|
|
495
|
+
if event not in settings["hooks"]:
|
|
496
|
+
settings["hooks"][event] = []
|
|
497
|
+
# Check if already present (by fingerprint AND matcher to avoid false dedup)
|
|
498
|
+
entry_matcher = defn["entry"].get("matcher", "")
|
|
499
|
+
already_present = False
|
|
500
|
+
for entry in settings["hooks"][event]:
|
|
501
|
+
if entry.get("matcher", "") != entry_matcher:
|
|
502
|
+
continue
|
|
503
|
+
for h in entry.get("hooks", []):
|
|
504
|
+
if fingerprint in h.get("command", ""):
|
|
505
|
+
already_present = True
|
|
506
|
+
break
|
|
507
|
+
if already_present:
|
|
508
|
+
break
|
|
509
|
+
if not already_present:
|
|
510
|
+
settings["hooks"][event].append(defn["entry"])
|
|
511
|
+
|
|
512
|
+
elif action == "remove":
|
|
513
|
+
if event in settings["hooks"]:
|
|
514
|
+
entry_matcher = defn["entry"].get("matcher", "")
|
|
515
|
+
settings["hooks"][event] = [
|
|
516
|
+
entry for entry in settings["hooks"][event]
|
|
517
|
+
if not (
|
|
518
|
+
entry.get("matcher", "") == entry_matcher and
|
|
519
|
+
any(fingerprint in h.get("command", "") for h in entry.get("hooks", []))
|
|
520
|
+
)
|
|
521
|
+
]
|
|
522
|
+
if not settings["hooks"][event]:
|
|
523
|
+
del settings["hooks"][event]
|
|
524
|
+
|
|
525
|
+
# Clean up empty hooks dict
|
|
526
|
+
if not settings.get("hooks"):
|
|
527
|
+
if "hooks" in settings:
|
|
528
|
+
del settings["hooks"]
|
|
529
|
+
|
|
530
|
+
with open(settings_file, "w") as f:
|
|
531
|
+
json.dump(settings, f, indent=2)
|
|
532
|
+
f.write("\n")
|
|
533
|
+
PYEOF
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# ============================================================================
|
|
537
|
+
# Gitignore Management
|
|
538
|
+
# ============================================================================
|
|
539
|
+
# Maintains a marker block in .claude/.gitignore for our symlinks.
|
|
540
|
+
# User entries outside the block are never touched.
|
|
541
|
+
|
|
542
|
+
update_gitignore_block() {
|
|
543
|
+
local target_dir="$1"
|
|
544
|
+
local items="$2" # space-separated list of manifest entry paths
|
|
545
|
+
local gitignore="$target_dir/.claude/.gitignore"
|
|
546
|
+
|
|
547
|
+
# Build the block content
|
|
548
|
+
local block=""
|
|
549
|
+
block="$GITIGNORE_MARKER_START"$'\n'
|
|
550
|
+
|
|
551
|
+
for item in $items; do
|
|
552
|
+
local bname
|
|
553
|
+
bname=$(basename "$item")
|
|
554
|
+
case "$item" in
|
|
555
|
+
agents/*) block="${block}agents/$bname"$'\n' ;;
|
|
556
|
+
skills/railway/*) block="${block}skills/$bname"$'\n' ;;
|
|
557
|
+
skills/*) block="${block}skills/$bname"$'\n' ;;
|
|
558
|
+
hooks/*) block="${block}hooks/$bname"$'\n' ;;
|
|
559
|
+
esac
|
|
560
|
+
done
|
|
561
|
+
|
|
562
|
+
# Always gitignore our config and backup
|
|
563
|
+
block="${block}$CONFIG_NAME"$'\n'
|
|
564
|
+
block="${block}$BACKUP_DIR_NAME/"$'\n'
|
|
565
|
+
block="$block$GITIGNORE_MARKER_END"
|
|
566
|
+
|
|
567
|
+
if [ -f "$gitignore" ]; then
|
|
568
|
+
# Remove existing block if present, then append new one
|
|
569
|
+
local tmp
|
|
570
|
+
tmp=$(mktemp)
|
|
571
|
+
local in_block=false
|
|
572
|
+
while IFS= read -r line; do
|
|
573
|
+
if [ "$line" = "$GITIGNORE_MARKER_START" ]; then
|
|
574
|
+
in_block=true
|
|
575
|
+
continue
|
|
576
|
+
fi
|
|
577
|
+
if [ "$line" = "$GITIGNORE_MARKER_END" ]; then
|
|
578
|
+
in_block=false
|
|
579
|
+
continue
|
|
580
|
+
fi
|
|
581
|
+
if ! $in_block; then
|
|
582
|
+
echo "$line" >> "$tmp"
|
|
583
|
+
fi
|
|
584
|
+
done < "$gitignore"
|
|
585
|
+
# Append our block
|
|
586
|
+
echo "" >> "$tmp"
|
|
587
|
+
echo "$block" >> "$tmp"
|
|
588
|
+
mv "$tmp" "$gitignore"
|
|
589
|
+
else
|
|
590
|
+
# Create new file with just our block
|
|
591
|
+
echo "$block" > "$gitignore"
|
|
592
|
+
fi
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
remove_gitignore_block() {
|
|
596
|
+
local target_dir="$1"
|
|
597
|
+
local gitignore="$target_dir/.claude/.gitignore"
|
|
598
|
+
|
|
599
|
+
if [ ! -f "$gitignore" ]; then
|
|
600
|
+
return 0
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
local tmp
|
|
604
|
+
tmp=$(mktemp)
|
|
605
|
+
local in_block=false
|
|
606
|
+
while IFS= read -r line; do
|
|
607
|
+
if [ "$line" = "$GITIGNORE_MARKER_START" ]; then
|
|
608
|
+
in_block=true
|
|
609
|
+
continue
|
|
610
|
+
fi
|
|
611
|
+
if [ "$line" = "$GITIGNORE_MARKER_END" ]; then
|
|
612
|
+
in_block=false
|
|
613
|
+
continue
|
|
614
|
+
fi
|
|
615
|
+
if ! $in_block; then
|
|
616
|
+
echo "$line" >> "$tmp"
|
|
617
|
+
fi
|
|
618
|
+
done < "$gitignore"
|
|
619
|
+
|
|
620
|
+
# Remove trailing blank lines
|
|
621
|
+
sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' "$tmp" > "$gitignore" 2>/dev/null || mv "$tmp" "$gitignore"
|
|
622
|
+
rm -f "$tmp"
|
|
623
|
+
|
|
624
|
+
# If gitignore is now empty, remove it
|
|
625
|
+
if [ ! -s "$gitignore" ]; then
|
|
626
|
+
rm -f "$gitignore"
|
|
627
|
+
fi
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
# ============================================================================
|
|
631
|
+
# CLAUDE.md Managed Block (toolkit instructions that auto-update)
|
|
632
|
+
# ============================================================================
|
|
633
|
+
|
|
634
|
+
update_claudemd_block() {
|
|
635
|
+
local target_dir="$1"
|
|
636
|
+
local mode="${2:-silent}" # "silent" = update existing block only, "ask" = offer to inject
|
|
637
|
+
local claudemd="$target_dir/CLAUDE.md"
|
|
638
|
+
|
|
639
|
+
# No template? Nothing to do.
|
|
640
|
+
if [ ! -f "$CLAUDEMD_MANAGED_TEMPLATE" ]; then
|
|
641
|
+
return 0
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
# No CLAUDE.md? Nothing to do. (--init creates it separately)
|
|
645
|
+
if [ ! -f "$claudemd" ]; then
|
|
646
|
+
return 0
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
# Build the block content (markers + template)
|
|
650
|
+
local block_content
|
|
651
|
+
block_content="$CLAUDEMD_MARKER_START"$'\n'
|
|
652
|
+
block_content+=$(cat "$CLAUDEMD_MANAGED_TEMPLATE")
|
|
653
|
+
block_content+=$'\n'"$CLAUDEMD_MARKER_END"
|
|
654
|
+
|
|
655
|
+
# Check if markers already exist
|
|
656
|
+
if grep -qF "$CLAUDEMD_MARKER_START" "$claudemd" 2>/dev/null; then
|
|
657
|
+
# Markers exist — replace the block in-place
|
|
658
|
+
local tmp
|
|
659
|
+
tmp=$(mktemp)
|
|
660
|
+
local in_block=false
|
|
661
|
+
local replaced=false
|
|
662
|
+
while IFS= read -r line; do
|
|
663
|
+
if [ "$line" = "$CLAUDEMD_MARKER_START" ]; then
|
|
664
|
+
in_block=true
|
|
665
|
+
if ! $replaced; then
|
|
666
|
+
echo "$block_content" >> "$tmp"
|
|
667
|
+
replaced=true
|
|
668
|
+
fi
|
|
669
|
+
continue
|
|
670
|
+
fi
|
|
671
|
+
if $in_block && [ "$line" = "$CLAUDEMD_MARKER_END" ]; then
|
|
672
|
+
in_block=false
|
|
673
|
+
continue
|
|
674
|
+
fi
|
|
675
|
+
if ! $in_block; then
|
|
676
|
+
echo "$line" >> "$tmp"
|
|
677
|
+
fi
|
|
678
|
+
done < "$claudemd"
|
|
679
|
+
mv "$tmp" "$claudemd"
|
|
680
|
+
return 0
|
|
681
|
+
fi
|
|
682
|
+
|
|
683
|
+
# No markers yet — behavior depends on mode
|
|
684
|
+
if [ "$mode" = "silent" ]; then
|
|
685
|
+
# Silent mode (sync): don't inject into unmarked files
|
|
686
|
+
return 0
|
|
687
|
+
fi
|
|
688
|
+
|
|
689
|
+
# Ask mode (upgrade/setup): offer to inject
|
|
690
|
+
echo ""
|
|
691
|
+
echo -e " ${BOLD}CLAUDE.md toolkit instructions:${NC}"
|
|
692
|
+
echo -e " Your CLAUDE.md doesn't have toolkit instructions yet."
|
|
693
|
+
echo -e " These tell Claude how to use the toolkit (routing, session start, delegation)."
|
|
694
|
+
echo -e " They go in a managed block — your content won't be touched."
|
|
695
|
+
echo ""
|
|
696
|
+
|
|
697
|
+
if [ "$FORCE_YES" = "true" ]; then
|
|
698
|
+
echo -e " Adding toolkit instructions to CLAUDE.md (--yes)"
|
|
699
|
+
else
|
|
700
|
+
echo -n " Add toolkit instructions to CLAUDE.md? [y/N] "
|
|
701
|
+
local answer
|
|
702
|
+
read -r answer < /dev/tty 2>/dev/null || read -r answer
|
|
703
|
+
case "$answer" in
|
|
704
|
+
y|Y) ;;
|
|
705
|
+
*)
|
|
706
|
+
echo -e " ${DIM}Skipped — run --upgrade again to add later${NC}"
|
|
707
|
+
return 0
|
|
708
|
+
;;
|
|
709
|
+
esac
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
# Inject after the first line (the # heading)
|
|
713
|
+
local tmp
|
|
714
|
+
tmp=$(mktemp)
|
|
715
|
+
local first_line=true
|
|
716
|
+
while IFS= read -r line; do
|
|
717
|
+
echo "$line" >> "$tmp"
|
|
718
|
+
if $first_line; then
|
|
719
|
+
first_line=false
|
|
720
|
+
echo "" >> "$tmp"
|
|
721
|
+
echo "$block_content" >> "$tmp"
|
|
722
|
+
fi
|
|
723
|
+
done < "$claudemd"
|
|
724
|
+
mv "$tmp" "$claudemd"
|
|
725
|
+
echo -e " ${GREEN}✓${NC} Added toolkit instructions to CLAUDE.md"
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
remove_claudemd_block() {
|
|
729
|
+
local target_dir="$1"
|
|
730
|
+
local claudemd="$target_dir/CLAUDE.md"
|
|
731
|
+
|
|
732
|
+
if [ ! -f "$claudemd" ]; then
|
|
733
|
+
return 0
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
# No markers? Nothing to remove.
|
|
737
|
+
if ! grep -qF "$CLAUDEMD_MARKER_START" "$claudemd" 2>/dev/null; then
|
|
738
|
+
return 0
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
local tmp
|
|
742
|
+
tmp=$(mktemp)
|
|
743
|
+
local in_block=false
|
|
744
|
+
local prev_blank=false
|
|
745
|
+
while IFS= read -r line; do
|
|
746
|
+
if [ "$line" = "$CLAUDEMD_MARKER_START" ]; then
|
|
747
|
+
in_block=true
|
|
748
|
+
continue
|
|
749
|
+
fi
|
|
750
|
+
if [ "$line" = "$CLAUDEMD_MARKER_END" ]; then
|
|
751
|
+
in_block=false
|
|
752
|
+
# Skip the blank line after the block too
|
|
753
|
+
prev_blank=true
|
|
754
|
+
continue
|
|
755
|
+
fi
|
|
756
|
+
if ! $in_block; then
|
|
757
|
+
# Skip one blank line right after block removal
|
|
758
|
+
if $prev_blank && [ -z "$line" ]; then
|
|
759
|
+
prev_blank=false
|
|
760
|
+
continue
|
|
761
|
+
fi
|
|
762
|
+
prev_blank=false
|
|
763
|
+
echo "$line" >> "$tmp"
|
|
764
|
+
fi
|
|
765
|
+
done < "$claudemd"
|
|
766
|
+
mv "$tmp" "$claudemd"
|
|
767
|
+
echo -e " CLAUDE.md: ${GREEN}removed toolkit instructions block${NC}"
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
# ============================================================================
|
|
771
|
+
# Project State Detection
|
|
772
|
+
# ============================================================================
|
|
773
|
+
# Detects the state of the target project to tailor the setup experience.
|
|
774
|
+
# Returns: "greenfield" | "minimal" | "established"
|
|
775
|
+
#
|
|
776
|
+
# greenfield — no .claude/ dir, or .claude/ exists but is mostly empty
|
|
777
|
+
# minimal — .claude/ exists with settings.json/CLAUDE.md but few agents/skills
|
|
778
|
+
# established — .claude/ exists with significant existing agents/skills/hooks
|
|
779
|
+
|
|
780
|
+
detect_project_state() {
|
|
781
|
+
local target_dir="$1"
|
|
782
|
+
|
|
783
|
+
# No .claude dir at all → greenfield
|
|
784
|
+
if [ ! -d "$target_dir/.claude" ]; then
|
|
785
|
+
echo "greenfield"
|
|
786
|
+
return
|
|
787
|
+
fi
|
|
788
|
+
|
|
789
|
+
# Count existing non-symlink agents, skills, hooks
|
|
790
|
+
local agent_count=0
|
|
791
|
+
local skill_count=0
|
|
792
|
+
local hook_count=0
|
|
793
|
+
|
|
794
|
+
if [ -d "$target_dir/.claude/agents" ]; then
|
|
795
|
+
for f in "$target_dir/.claude/agents"/*.md; do
|
|
796
|
+
[ ! -f "$f" ] && continue
|
|
797
|
+
[ -L "$f" ] && continue
|
|
798
|
+
agent_count=$((agent_count + 1))
|
|
799
|
+
done
|
|
800
|
+
fi
|
|
801
|
+
|
|
802
|
+
if [ -d "$target_dir/.claude/skills" ]; then
|
|
803
|
+
for d in "$target_dir/.claude/skills"/*/; do
|
|
804
|
+
[ ! -d "$d" ] && continue
|
|
805
|
+
[ -L "${d%/}" ] && continue
|
|
806
|
+
skill_count=$((skill_count + 1))
|
|
807
|
+
done
|
|
808
|
+
# Also count single-file skills
|
|
809
|
+
for f in "$target_dir/.claude/skills"/*.md; do
|
|
810
|
+
[ ! -f "$f" ] && continue
|
|
811
|
+
[ -L "$f" ] && continue
|
|
812
|
+
skill_count=$((skill_count + 1))
|
|
813
|
+
done
|
|
814
|
+
fi
|
|
815
|
+
|
|
816
|
+
if [ -d "$target_dir/.claude/hooks" ]; then
|
|
817
|
+
for f in "$target_dir/.claude/hooks"/*.sh; do
|
|
818
|
+
[ ! -f "$f" ] && continue
|
|
819
|
+
[ -L "$f" ] && continue
|
|
820
|
+
hook_count=$((hook_count + 1))
|
|
821
|
+
done
|
|
822
|
+
fi
|
|
823
|
+
|
|
824
|
+
local total=$((agent_count + skill_count + hook_count))
|
|
825
|
+
|
|
826
|
+
# Has settings.json but very few custom tools → minimal
|
|
827
|
+
if [ "$total" -le 2 ]; then
|
|
828
|
+
if [ -f "$target_dir/.claude/settings.json" ] || [ -f "$target_dir/CLAUDE.md" ]; then
|
|
829
|
+
echo "minimal"
|
|
830
|
+
else
|
|
831
|
+
echo "greenfield"
|
|
832
|
+
fi
|
|
833
|
+
return
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
# Significant existing setup
|
|
837
|
+
echo "established"
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
# ============================================================================
|
|
841
|
+
# Interactive Menu
|
|
842
|
+
# ============================================================================
|
|
843
|
+
# Pure bash interactive category selector.
|
|
844
|
+
# All UI output goes to stderr; result (selected categories) goes to stdout.
|
|
845
|
+
|
|
846
|
+
show_interactive_menu() {
|
|
847
|
+
local target_dir="$1"
|
|
848
|
+
local preselected="$2" # space-separated category names, or "" for defaults
|
|
849
|
+
local project_state="$3" # greenfield | minimal | established
|
|
850
|
+
|
|
851
|
+
local num_categories=9
|
|
852
|
+
local current=0
|
|
853
|
+
|
|
854
|
+
# Category names (indexed array — bash 3+ compatible)
|
|
855
|
+
# Note: 'consulting' is a hidden category — not shown in menu.
|
|
856
|
+
# Install via: INSTALLED_CATEGORIES="... consulting" in .claude-agents.conf
|
|
857
|
+
local cat_names_0="core"
|
|
858
|
+
local cat_names_1="strategy"
|
|
859
|
+
local cat_names_2="development"
|
|
860
|
+
local cat_names_3="quality"
|
|
861
|
+
local cat_names_4="operations"
|
|
862
|
+
local cat_names_5="hooks"
|
|
863
|
+
local cat_names_6="guardrails"
|
|
864
|
+
local cat_names_7="railway"
|
|
865
|
+
local cat_names_8="superpowers"
|
|
866
|
+
|
|
867
|
+
# Initialize selections from defaults
|
|
868
|
+
local sel_0=1 sel_1=1 sel_2=1 sel_3=1 sel_4=1 sel_5=1 sel_6=1 sel_7=0 sel_8=0
|
|
869
|
+
|
|
870
|
+
# Pre-populate from existing config if provided
|
|
871
|
+
if [ -n "$preselected" ]; then
|
|
872
|
+
sel_0=0; sel_1=0; sel_2=0; sel_3=0; sel_4=0; sel_5=0; sel_6=0; sel_7=0; sel_8=0
|
|
873
|
+
# Hidden categories (consulting) are preserved if already in config
|
|
874
|
+
local _hidden_cats=""
|
|
875
|
+
for cat in $preselected; do
|
|
876
|
+
case "$cat" in
|
|
877
|
+
core) sel_0=1 ;;
|
|
878
|
+
strategy) sel_1=1 ;;
|
|
879
|
+
development) sel_2=1 ;;
|
|
880
|
+
quality) sel_3=1 ;;
|
|
881
|
+
operations) sel_4=1 ;;
|
|
882
|
+
hooks) sel_5=1 ;;
|
|
883
|
+
guardrails) sel_6=1 ;;
|
|
884
|
+
railway) sel_7=1 ;;
|
|
885
|
+
superpowers) sel_8=1 ;;
|
|
886
|
+
consulting) _hidden_cats="$_hidden_cats consulting" ;;
|
|
887
|
+
esac
|
|
888
|
+
done
|
|
889
|
+
fi
|
|
890
|
+
|
|
891
|
+
# Hide cursor
|
|
892
|
+
printf '\033[?25l' >&2
|
|
893
|
+
|
|
894
|
+
# Ensure cursor is restored on exit/interrupt
|
|
895
|
+
trap 'printf "\033[?25h" >&2' EXIT INT TERM
|
|
896
|
+
|
|
897
|
+
# Header
|
|
898
|
+
echo "" >&2
|
|
899
|
+
echo -e "${BOLD}Claude Agents — Setup${NC}" >&2
|
|
900
|
+
echo -e "Target: ${CYAN}$target_dir${NC}" >&2
|
|
901
|
+
echo "" >&2
|
|
902
|
+
|
|
903
|
+
# Show project state context
|
|
904
|
+
case "$project_state" in
|
|
905
|
+
greenfield)
|
|
906
|
+
echo -e "${GREEN}Fresh project detected.${NC} We'll set up the full toolkit — agents, skills," >&2
|
|
907
|
+
echo -e "hooks, and workflows — so Claude Code works optimally from day one." >&2
|
|
908
|
+
echo "" >&2
|
|
909
|
+
;;
|
|
910
|
+
minimal)
|
|
911
|
+
echo -e "${CYAN}Existing project with basic Claude Code setup.${NC} We'll add our agents" >&2
|
|
912
|
+
echo -e "and skills alongside your existing configuration. Your CLAUDE.md and" >&2
|
|
913
|
+
echo -e "settings stay untouched — we only add, never replace." >&2
|
|
914
|
+
echo "" >&2
|
|
915
|
+
# Count and show what they have
|
|
916
|
+
local their_agents=0 their_skills=0 their_hooks=0
|
|
917
|
+
if [ -d "$target_dir/.claude/agents" ]; then
|
|
918
|
+
for f in "$target_dir/.claude/agents"/*.md; do
|
|
919
|
+
[ -f "$f" ] && [ ! -L "$f" ] && their_agents=$((their_agents + 1))
|
|
920
|
+
done
|
|
921
|
+
fi
|
|
922
|
+
if [ -d "$target_dir/.claude/skills" ]; then
|
|
923
|
+
for d in "$target_dir/.claude/skills"/*/; do
|
|
924
|
+
[ -d "$d" ] && [ ! -L "${d%/}" ] && their_skills=$((their_skills + 1))
|
|
925
|
+
done
|
|
926
|
+
fi
|
|
927
|
+
if [ -d "$target_dir/.claude/hooks" ]; then
|
|
928
|
+
for f in "$target_dir/.claude/hooks"/*.sh; do
|
|
929
|
+
[ -f "$f" ] && [ ! -L "$f" ] && their_hooks=$((their_hooks + 1))
|
|
930
|
+
done
|
|
931
|
+
fi
|
|
932
|
+
if [ $((their_agents + their_skills + their_hooks)) -gt 0 ]; then
|
|
933
|
+
echo -e " ${DIM}Your existing tools: ${their_agents} agents, ${their_skills} skills, ${their_hooks} hooks${NC}" >&2
|
|
934
|
+
echo -e " ${DIM}These will be auto-discovered by the triage router and work${NC}" >&2
|
|
935
|
+
echo -e " ${DIM}alongside our portable tools — better together.${NC}" >&2
|
|
936
|
+
echo "" >&2
|
|
937
|
+
fi
|
|
938
|
+
;;
|
|
939
|
+
established)
|
|
940
|
+
echo -e "${YELLOW}Established Claude Code project detected.${NC} Your existing agents," >&2
|
|
941
|
+
echo -e "skills, and hooks are preserved. We symlink our tools alongside yours —" >&2
|
|
942
|
+
echo -e "the triage router auto-discovers everything and routes optimally." >&2
|
|
943
|
+
echo "" >&2
|
|
944
|
+
;;
|
|
945
|
+
esac
|
|
946
|
+
|
|
947
|
+
echo "Select categories to install (arrows to move, space to toggle, enter to confirm):" >&2
|
|
948
|
+
echo "" >&2
|
|
949
|
+
|
|
950
|
+
while true; do
|
|
951
|
+
# Render each category line
|
|
952
|
+
local i=0
|
|
953
|
+
while [ "$i" -lt "$num_categories" ]; do
|
|
954
|
+
local prefix=" "
|
|
955
|
+
if [ "$i" -eq "$current" ]; then
|
|
956
|
+
prefix="> "
|
|
957
|
+
fi
|
|
958
|
+
|
|
959
|
+
# Get selection state
|
|
960
|
+
local sel_val=0
|
|
961
|
+
case "$i" in
|
|
962
|
+
0) sel_val=$sel_0 ;; 1) sel_val=$sel_1 ;; 2) sel_val=$sel_2 ;;
|
|
963
|
+
3) sel_val=$sel_3 ;; 4) sel_val=$sel_4 ;; 5) sel_val=$sel_5 ;;
|
|
964
|
+
6) sel_val=$sel_6 ;; 7) sel_val=$sel_7 ;; 8) sel_val=$sel_8 ;;
|
|
965
|
+
esac
|
|
966
|
+
|
|
967
|
+
local checkbox="[ ]"
|
|
968
|
+
if [ "$sel_val" -eq 1 ]; then
|
|
969
|
+
checkbox="[x]"
|
|
970
|
+
fi
|
|
971
|
+
# Core is required — show locked indicator
|
|
972
|
+
if [ "$i" -eq 0 ]; then
|
|
973
|
+
checkbox="[x]" # Always checked, cannot toggle
|
|
974
|
+
fi
|
|
975
|
+
|
|
976
|
+
# Get label and description
|
|
977
|
+
local cat_name=""
|
|
978
|
+
case "$i" in
|
|
979
|
+
0) cat_name="core" ;; 1) cat_name="strategy" ;; 2) cat_name="development" ;;
|
|
980
|
+
3) cat_name="quality" ;; 4) cat_name="operations" ;; 5) cat_name="hooks" ;;
|
|
981
|
+
6) cat_name="guardrails" ;; 7) cat_name="railway" ;; 8) cat_name="superpowers" ;;
|
|
982
|
+
esac
|
|
983
|
+
|
|
984
|
+
local label desc
|
|
985
|
+
label=$(get_category_label "$cat_name")
|
|
986
|
+
desc=$(get_category_desc "$cat_name")
|
|
987
|
+
|
|
988
|
+
# Highlight current row
|
|
989
|
+
if [ "$i" -eq "$current" ]; then
|
|
990
|
+
printf '\033[K' >&2
|
|
991
|
+
printf "${BOLD}%s %s %-22s${NC} %s\n" "$prefix" "$checkbox" "$label" "$desc" >&2
|
|
992
|
+
else
|
|
993
|
+
printf '\033[K' >&2
|
|
994
|
+
printf "%s %s %-22s ${DIM}%s${NC}\n" "$prefix" "$checkbox" "$label" "$desc" >&2
|
|
995
|
+
fi
|
|
996
|
+
|
|
997
|
+
i=$((i + 1))
|
|
998
|
+
done
|
|
999
|
+
|
|
1000
|
+
# Read key
|
|
1001
|
+
IFS= read -rsn1 key < /dev/tty 2>/dev/null || IFS= read -rsn1 key
|
|
1002
|
+
|
|
1003
|
+
if [ "$key" = $'\x1b' ]; then
|
|
1004
|
+
IFS= read -rsn2 key < /dev/tty 2>/dev/null || IFS= read -rsn2 key
|
|
1005
|
+
case "$key" in
|
|
1006
|
+
'[A') # Up
|
|
1007
|
+
current=$(( (current - 1 + num_categories) % num_categories ))
|
|
1008
|
+
;;
|
|
1009
|
+
'[B') # Down
|
|
1010
|
+
current=$(( (current + 1) % num_categories ))
|
|
1011
|
+
;;
|
|
1012
|
+
esac
|
|
1013
|
+
elif [ "$key" = " " ]; then
|
|
1014
|
+
# Toggle selection (core is always-on — cannot be deselected)
|
|
1015
|
+
case "$current" in
|
|
1016
|
+
0) ;; # core is required — no toggle
|
|
1017
|
+
1) sel_1=$(( 1 - sel_1 )) ;; 2) sel_2=$(( 1 - sel_2 )) ;;
|
|
1018
|
+
3) sel_3=$(( 1 - sel_3 )) ;; 4) sel_4=$(( 1 - sel_4 )) ;;
|
|
1019
|
+
5) sel_5=$(( 1 - sel_5 )) ;; 6) sel_6=$(( 1 - sel_6 )) ;;
|
|
1020
|
+
7) sel_7=$(( 1 - sel_7 )) ;; 8) sel_8=$(( 1 - sel_8 )) ;;
|
|
1021
|
+
esac
|
|
1022
|
+
elif [ "$key" = "" ]; then
|
|
1023
|
+
# Enter — confirm
|
|
1024
|
+
break
|
|
1025
|
+
fi
|
|
1026
|
+
|
|
1027
|
+
# Move cursor back up to reprint menu
|
|
1028
|
+
printf '\033[%dA' "$num_categories" >&2
|
|
1029
|
+
done
|
|
1030
|
+
|
|
1031
|
+
# Restore cursor
|
|
1032
|
+
printf '\033[?25h' >&2
|
|
1033
|
+
trap - EXIT INT TERM
|
|
1034
|
+
|
|
1035
|
+
echo "" >&2
|
|
1036
|
+
|
|
1037
|
+
# Build result string (to stdout)
|
|
1038
|
+
local result=""
|
|
1039
|
+
[ "$sel_0" -eq 1 ] && result="$result core"
|
|
1040
|
+
[ "$sel_1" -eq 1 ] && result="$result strategy"
|
|
1041
|
+
[ "$sel_2" -eq 1 ] && result="$result development"
|
|
1042
|
+
[ "$sel_3" -eq 1 ] && result="$result quality"
|
|
1043
|
+
[ "$sel_4" -eq 1 ] && result="$result operations"
|
|
1044
|
+
[ "$sel_5" -eq 1 ] && result="$result hooks"
|
|
1045
|
+
[ "$sel_6" -eq 1 ] && result="$result guardrails"
|
|
1046
|
+
[ "$sel_7" -eq 1 ] && result="$result railway"
|
|
1047
|
+
[ "$sel_8" -eq 1 ] && result="$result superpowers"
|
|
1048
|
+
|
|
1049
|
+
# Re-append hidden categories that were already in config
|
|
1050
|
+
result="$result$_hidden_cats"
|
|
1051
|
+
|
|
1052
|
+
# Trim leading space and output
|
|
1053
|
+
echo "${result# }"
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
# ============================================================================
|
|
1057
|
+
# Symlink Helpers
|
|
1058
|
+
# ============================================================================
|
|
1059
|
+
|
|
1060
|
+
# safe_symlink: Default mode for daily sync
|
|
1061
|
+
# - Symlink exists → update (it's ours)
|
|
1062
|
+
# - Regular file exists → SKIP (project override)
|
|
1063
|
+
# - Doesn't exist → create symlink
|
|
1064
|
+
safe_symlink() {
|
|
1065
|
+
local source="$1" # Absolute path in repo
|
|
1066
|
+
local target="$2" # Absolute path in project .claude/
|
|
1067
|
+
|
|
1068
|
+
if [ -L "$target" ]; then
|
|
1069
|
+
# It's a symlink — update it (managed by us)
|
|
1070
|
+
rm "$target"
|
|
1071
|
+
ln -s "$source" "$target"
|
|
1072
|
+
UPDATED=$((UPDATED + 1))
|
|
1073
|
+
elif [ -e "$target" ]; then
|
|
1074
|
+
# Regular file/dir exists — project override, skip
|
|
1075
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1076
|
+
else
|
|
1077
|
+
# Doesn't exist — create symlink
|
|
1078
|
+
local target_dir
|
|
1079
|
+
target_dir=$(dirname "$target")
|
|
1080
|
+
mkdir -p "$target_dir"
|
|
1081
|
+
ln -s "$source" "$target"
|
|
1082
|
+
CREATED=$((CREATED + 1))
|
|
1083
|
+
fi
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
# convert_to_symlink: One-time migration mode (--convert)
|
|
1087
|
+
# - Symlink exists → update
|
|
1088
|
+
# - Regular file, matches repo → replace with symlink
|
|
1089
|
+
# - Regular file, differs → SKIP + warn (project override)
|
|
1090
|
+
# - Doesn't exist → create symlink
|
|
1091
|
+
convert_to_symlink() {
|
|
1092
|
+
local source="$1"
|
|
1093
|
+
local target="$2"
|
|
1094
|
+
|
|
1095
|
+
if [ -L "$target" ]; then
|
|
1096
|
+
# Already a symlink — update
|
|
1097
|
+
rm "$target"
|
|
1098
|
+
ln -s "$source" "$target"
|
|
1099
|
+
UPDATED=$((UPDATED + 1))
|
|
1100
|
+
elif [ -d "$target" ] && [ -d "$source" ]; then
|
|
1101
|
+
# Both are directories — compare contents
|
|
1102
|
+
if diff -rq "$source" "$target" >/dev/null 2>&1; then
|
|
1103
|
+
# Contents match — replace with symlink
|
|
1104
|
+
rm -rf "$target"
|
|
1105
|
+
ln -s "$source" "$target"
|
|
1106
|
+
CONVERTED=$((CONVERTED + 1))
|
|
1107
|
+
else
|
|
1108
|
+
# Differs — project override
|
|
1109
|
+
echo -e " ${YELLOW}OVERRIDE${NC} $(basename "$target") (differs from repo)"
|
|
1110
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1111
|
+
fi
|
|
1112
|
+
elif [ -f "$target" ] && [ -f "$source" ]; then
|
|
1113
|
+
# Both are files — compare
|
|
1114
|
+
if diff -q "$source" "$target" >/dev/null 2>&1; then
|
|
1115
|
+
# Matches — replace with symlink
|
|
1116
|
+
rm "$target"
|
|
1117
|
+
ln -s "$source" "$target"
|
|
1118
|
+
CONVERTED=$((CONVERTED + 1))
|
|
1119
|
+
else
|
|
1120
|
+
# Differs — project override
|
|
1121
|
+
echo -e " ${YELLOW}OVERRIDE${NC} $(basename "$target") (differs from repo)"
|
|
1122
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1123
|
+
fi
|
|
1124
|
+
elif [ ! -e "$target" ]; then
|
|
1125
|
+
# Doesn't exist — create
|
|
1126
|
+
local target_dir
|
|
1127
|
+
target_dir=$(dirname "$target")
|
|
1128
|
+
mkdir -p "$target_dir"
|
|
1129
|
+
ln -s "$source" "$target"
|
|
1130
|
+
CREATED=$((CREATED + 1))
|
|
1131
|
+
else
|
|
1132
|
+
echo -e " ${RED}ERROR${NC} $(basename "$target") — unexpected state"
|
|
1133
|
+
ERRORS=$((ERRORS + 1))
|
|
1134
|
+
fi
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
# ============================================================================
|
|
1138
|
+
# Brownfield Assessment Helpers
|
|
1139
|
+
# ============================================================================
|
|
1140
|
+
|
|
1141
|
+
# get_size: Returns total bytes for a file or directory
|
|
1142
|
+
get_size() {
|
|
1143
|
+
local path="$1"
|
|
1144
|
+
if [ -f "$path" ]; then
|
|
1145
|
+
wc -c < "$path" | tr -d ' '
|
|
1146
|
+
elif [ -d "$path" ]; then
|
|
1147
|
+
find "$path" -type f -exec cat {} + 2>/dev/null | wc -c | tr -d ' '
|
|
1148
|
+
else
|
|
1149
|
+
echo "0"
|
|
1150
|
+
fi
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
# classify_file: Compare a regular file/dir at target to its toolkit source
|
|
1154
|
+
# Returns: SYMLINK | MISSING | IDENTICAL | STALE | CUSTOMIZED | MODIFIED
|
|
1155
|
+
classify_file() {
|
|
1156
|
+
local source="$1"
|
|
1157
|
+
local target="$2"
|
|
1158
|
+
|
|
1159
|
+
if [ -L "$target" ]; then
|
|
1160
|
+
echo "SYMLINK"
|
|
1161
|
+
return
|
|
1162
|
+
fi
|
|
1163
|
+
|
|
1164
|
+
if [ ! -e "$target" ]; then
|
|
1165
|
+
echo "MISSING"
|
|
1166
|
+
return
|
|
1167
|
+
fi
|
|
1168
|
+
|
|
1169
|
+
# Compare content
|
|
1170
|
+
if [ -d "$source" ] && [ -d "$target" ]; then
|
|
1171
|
+
if diff -rq "$source" "$target" >/dev/null 2>&1; then
|
|
1172
|
+
echo "IDENTICAL"
|
|
1173
|
+
return
|
|
1174
|
+
fi
|
|
1175
|
+
elif [ -f "$source" ] && [ -f "$target" ]; then
|
|
1176
|
+
if diff -q "$source" "$target" >/dev/null 2>&1; then
|
|
1177
|
+
echo "IDENTICAL"
|
|
1178
|
+
return
|
|
1179
|
+
fi
|
|
1180
|
+
fi
|
|
1181
|
+
|
|
1182
|
+
# Size-based classification
|
|
1183
|
+
local source_size target_size
|
|
1184
|
+
source_size=$(get_size "$source")
|
|
1185
|
+
target_size=$(get_size "$target")
|
|
1186
|
+
|
|
1187
|
+
if [ "$source_size" -eq 0 ]; then
|
|
1188
|
+
echo "MODIFIED"
|
|
1189
|
+
return
|
|
1190
|
+
fi
|
|
1191
|
+
|
|
1192
|
+
local ratio=$((target_size * 100 / source_size))
|
|
1193
|
+
|
|
1194
|
+
if [ "$ratio" -lt 70 ]; then
|
|
1195
|
+
echo "STALE"
|
|
1196
|
+
elif [ "$ratio" -gt 150 ]; then
|
|
1197
|
+
echo "CUSTOMIZED"
|
|
1198
|
+
else
|
|
1199
|
+
echo "MODIFIED"
|
|
1200
|
+
fi
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
# generate_config_from_existing: Scan what exists in .claude/ and derive a config
|
|
1204
|
+
# Args: $1=target_dir $2=dry_run (optional, "true" to skip writing)
|
|
1205
|
+
# Echoes: space-separated category list
|
|
1206
|
+
generate_config_from_existing() {
|
|
1207
|
+
local target_dir="$1"
|
|
1208
|
+
local dry_run="${2:-false}"
|
|
1209
|
+
|
|
1210
|
+
local categories="core"
|
|
1211
|
+
|
|
1212
|
+
local cat_list="strategy development quality operations hooks guardrails railway superpowers"
|
|
1213
|
+
for cat in $cat_list; do
|
|
1214
|
+
local items
|
|
1215
|
+
items=$(get_category_items "$cat")
|
|
1216
|
+
local found=false
|
|
1217
|
+
for item in $items; do
|
|
1218
|
+
local bname target_path
|
|
1219
|
+
bname=$(basename "$item")
|
|
1220
|
+
case "$item" in
|
|
1221
|
+
agents/*) target_path="$target_dir/.claude/agents/$bname" ;;
|
|
1222
|
+
skills/railway/*) target_path="$target_dir/.claude/skills/$bname" ;;
|
|
1223
|
+
skills/*) target_path="$target_dir/.claude/skills/$bname" ;;
|
|
1224
|
+
hooks/*) target_path="$target_dir/.claude/hooks/$bname" ;;
|
|
1225
|
+
*) continue ;;
|
|
1226
|
+
esac
|
|
1227
|
+
if [ -e "$target_path" ] || [ -L "$target_path" ]; then
|
|
1228
|
+
found=true
|
|
1229
|
+
break
|
|
1230
|
+
fi
|
|
1231
|
+
done
|
|
1232
|
+
if $found; then
|
|
1233
|
+
categories="$categories $cat"
|
|
1234
|
+
fi
|
|
1235
|
+
done
|
|
1236
|
+
|
|
1237
|
+
if [ "$dry_run" != "true" ]; then
|
|
1238
|
+
write_config "$target_dir" "$categories"
|
|
1239
|
+
fi
|
|
1240
|
+
|
|
1241
|
+
echo "$categories"
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
# smart_sync: Enhanced symlink for brownfield upgrades
|
|
1245
|
+
# - Symlink → update (same as safe_symlink)
|
|
1246
|
+
# - Missing → create symlink (same as safe_symlink)
|
|
1247
|
+
# - IDENTICAL regular file → backup + convert to symlink
|
|
1248
|
+
# - STALE regular file → backup + convert to symlink + warning
|
|
1249
|
+
# - CUSTOMIZED/MODIFIED → skip
|
|
1250
|
+
smart_sync() {
|
|
1251
|
+
local source="$1"
|
|
1252
|
+
local target="$2"
|
|
1253
|
+
|
|
1254
|
+
if [ -L "$target" ]; then
|
|
1255
|
+
rm "$target"
|
|
1256
|
+
ln -s "$source" "$target"
|
|
1257
|
+
UPDATED=$((UPDATED + 1))
|
|
1258
|
+
return
|
|
1259
|
+
fi
|
|
1260
|
+
|
|
1261
|
+
if [ ! -e "$target" ]; then
|
|
1262
|
+
local target_parent
|
|
1263
|
+
target_parent=$(dirname "$target")
|
|
1264
|
+
mkdir -p "$target_parent"
|
|
1265
|
+
ln -s "$source" "$target"
|
|
1266
|
+
CREATED=$((CREATED + 1))
|
|
1267
|
+
return
|
|
1268
|
+
fi
|
|
1269
|
+
|
|
1270
|
+
# Regular file/dir exists
|
|
1271
|
+
local bname
|
|
1272
|
+
bname=$(basename "$target")
|
|
1273
|
+
|
|
1274
|
+
# Check --skip list (only for regular files — symlinks still updated, missing still created)
|
|
1275
|
+
if [ -n "$SKIP_ITEMS" ] && echo ",$SKIP_ITEMS," | grep -q ",$bname,"; then
|
|
1276
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1277
|
+
return
|
|
1278
|
+
fi
|
|
1279
|
+
|
|
1280
|
+
# Classify it
|
|
1281
|
+
local classification
|
|
1282
|
+
classification=$(classify_file "$source" "$target")
|
|
1283
|
+
|
|
1284
|
+
# Derive project root from target path (strip /.claude/...)
|
|
1285
|
+
local project_root
|
|
1286
|
+
project_root=$(echo "$target" | sed 's|/.claude/.*||')
|
|
1287
|
+
local bdir
|
|
1288
|
+
bdir=$(backup_dir "$project_root")
|
|
1289
|
+
mkdir -p "$bdir"
|
|
1290
|
+
|
|
1291
|
+
case "$classification" in
|
|
1292
|
+
IDENTICAL)
|
|
1293
|
+
# Backup + convert to symlink
|
|
1294
|
+
cp -a "$target" "$bdir/$bname.pre-upgrade" 2>/dev/null || true
|
|
1295
|
+
if [ -d "$target" ]; then
|
|
1296
|
+
rm -rf "$target"
|
|
1297
|
+
else
|
|
1298
|
+
rm "$target"
|
|
1299
|
+
fi
|
|
1300
|
+
ln -s "$source" "$target"
|
|
1301
|
+
CONVERTED=$((CONVERTED + 1))
|
|
1302
|
+
;;
|
|
1303
|
+
STALE)
|
|
1304
|
+
# Defer for interactive confirmation (or auto-convert with --yes)
|
|
1305
|
+
STALE_DEFERRED+=("$source|$target|$bname")
|
|
1306
|
+
;;
|
|
1307
|
+
CUSTOMIZED|MODIFIED)
|
|
1308
|
+
SKIPPED=$((SKIPPED + 1))
|
|
1309
|
+
;;
|
|
1310
|
+
esac
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
# do_stale_upgrade: Performs the actual backup + symlink conversion for a stale item
|
|
1314
|
+
do_stale_upgrade() {
|
|
1315
|
+
local source="$1" target="$2" name="$3"
|
|
1316
|
+
local project_root
|
|
1317
|
+
project_root=$(echo "$target" | sed 's|/.claude/.*||')
|
|
1318
|
+
local bdir
|
|
1319
|
+
bdir=$(backup_dir "$project_root")
|
|
1320
|
+
mkdir -p "$bdir"
|
|
1321
|
+
|
|
1322
|
+
cp -a "$target" "$bdir/$name.pre-upgrade" 2>/dev/null || true
|
|
1323
|
+
if [ -d "$target" ]; then
|
|
1324
|
+
rm -rf "$target"
|
|
1325
|
+
else
|
|
1326
|
+
rm "$target"
|
|
1327
|
+
fi
|
|
1328
|
+
ln -s "$source" "$target"
|
|
1329
|
+
UPGRADED=$((UPGRADED + 1))
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
# prompt_stale_conversion: Interactive per-file confirmation for stale items
|
|
1333
|
+
prompt_stale_conversion() {
|
|
1334
|
+
local source="$1" target="$2" name="$3"
|
|
1335
|
+
local source_size target_size
|
|
1336
|
+
source_size=$(get_size "$source")
|
|
1337
|
+
target_size=$(get_size "$target")
|
|
1338
|
+
local ratio=0
|
|
1339
|
+
if [ "$source_size" -gt 0 ]; then
|
|
1340
|
+
ratio=$((target_size * 100 / source_size))
|
|
1341
|
+
fi
|
|
1342
|
+
|
|
1343
|
+
echo -e " ⚠ ${BOLD}$name${NC} (${target_size}B → ${source_size}B toolkit — ${ratio}%)"
|
|
1344
|
+
|
|
1345
|
+
# Show preview of each version
|
|
1346
|
+
local your_line="" toolkit_line=""
|
|
1347
|
+
if [ -f "$target" ]; then
|
|
1348
|
+
your_line=$(head -5 "$target" 2>/dev/null | grep -v '^$' | grep -v '^---$' | head -1 | cut -c1-80)
|
|
1349
|
+
elif [ -d "$target" ]; then
|
|
1350
|
+
your_line="[directory: $(ls "$target" 2>/dev/null | wc -l | tr -d ' ') files]"
|
|
1351
|
+
fi
|
|
1352
|
+
if [ -f "$source" ]; then
|
|
1353
|
+
toolkit_line=$(head -5 "$source" 2>/dev/null | grep -v '^$' | grep -v '^---$' | head -1 | cut -c1-80)
|
|
1354
|
+
elif [ -d "$source" ]; then
|
|
1355
|
+
toolkit_line="[directory: $(ls "$source" 2>/dev/null | wc -l | tr -d ' ') files]"
|
|
1356
|
+
fi
|
|
1357
|
+
echo -e " ${DIM}Your version:${NC} $your_line"
|
|
1358
|
+
echo -e " ${DIM}Toolkit version:${NC} $toolkit_line"
|
|
1359
|
+
|
|
1360
|
+
while true; do
|
|
1361
|
+
if [ "$FORCE_YES" = "true" ]; then
|
|
1362
|
+
echo -e " Replace with toolkit version? [y/N/d(diff)] ${GREEN}y${NC} (--yes)"
|
|
1363
|
+
do_stale_upgrade "$source" "$target" "$name"
|
|
1364
|
+
echo -e " ${GREEN}✓ Upgraded${NC} $name"
|
|
1365
|
+
return
|
|
1366
|
+
fi
|
|
1367
|
+
printf " Replace with toolkit version? [y/N/d(diff)] " >&2
|
|
1368
|
+
read -r answer < /dev/tty 2>/dev/null || read -r answer
|
|
1369
|
+
case "$answer" in
|
|
1370
|
+
y|Y)
|
|
1371
|
+
do_stale_upgrade "$source" "$target" "$name"
|
|
1372
|
+
echo -e " ${GREEN}✓ Upgraded${NC} $name"
|
|
1373
|
+
return
|
|
1374
|
+
;;
|
|
1375
|
+
d|D)
|
|
1376
|
+
echo ""
|
|
1377
|
+
if [ -d "$target" ]; then
|
|
1378
|
+
diff -r "$target" "$source" 2>/dev/null | head -40 || true
|
|
1379
|
+
else
|
|
1380
|
+
diff "$target" "$source" 2>/dev/null | head -40 || true
|
|
1381
|
+
fi
|
|
1382
|
+
echo -e " ${DIM}(showing first 40 lines of diff)${NC}"
|
|
1383
|
+
echo ""
|
|
1384
|
+
;;
|
|
1385
|
+
*)
|
|
1386
|
+
echo -e " ${CYAN}Kept${NC} your version of $name"
|
|
1387
|
+
KEPT=$((KEPT + 1))
|
|
1388
|
+
return
|
|
1389
|
+
;;
|
|
1390
|
+
esac
|
|
1391
|
+
done
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
# ============================================================================
|
|
1395
|
+
# Manifest Parsing
|
|
1396
|
+
# ============================================================================
|
|
1397
|
+
|
|
1398
|
+
# Process entries from the manifest
|
|
1399
|
+
# Args: $1=target_dir $2=mode(safe|convert) $3=include_railway(true|false) $4=filter_items(optional)
|
|
1400
|
+
# If filter_items is provided (space-separated paths), only those entries are processed.
|
|
1401
|
+
# If empty, all entries are processed (with railway controlled by $3).
|
|
1402
|
+
process_manifest() {
|
|
1403
|
+
local target_dir="$1"
|
|
1404
|
+
local mode="$2"
|
|
1405
|
+
local include_railway="$3"
|
|
1406
|
+
local filter_items="${4:-}"
|
|
1407
|
+
|
|
1408
|
+
local linker="safe_symlink"
|
|
1409
|
+
if [ "$mode" = "convert" ]; then
|
|
1410
|
+
linker="convert_to_symlink"
|
|
1411
|
+
elif [ "$mode" = "smart" ]; then
|
|
1412
|
+
linker="smart_sync"
|
|
1413
|
+
fi
|
|
1414
|
+
|
|
1415
|
+
while IFS= read -r line; do
|
|
1416
|
+
# Skip comments and empty lines
|
|
1417
|
+
[[ "$line" =~ ^#.*$ ]] && continue
|
|
1418
|
+
[[ -z "$line" ]] && continue
|
|
1419
|
+
|
|
1420
|
+
local entry_type entry_path
|
|
1421
|
+
entry_type=$(echo "$line" | awk '{print $1}')
|
|
1422
|
+
entry_path=$(echo "$line" | awk '{print $2}')
|
|
1423
|
+
|
|
1424
|
+
# Apply filter if provided
|
|
1425
|
+
if [ -n "$filter_items" ]; then
|
|
1426
|
+
local in_filter=false
|
|
1427
|
+
for allowed in $filter_items; do
|
|
1428
|
+
if [ "$entry_path" = "$allowed" ]; then
|
|
1429
|
+
in_filter=true
|
|
1430
|
+
break
|
|
1431
|
+
fi
|
|
1432
|
+
done
|
|
1433
|
+
if ! $in_filter; then
|
|
1434
|
+
continue
|
|
1435
|
+
fi
|
|
1436
|
+
else
|
|
1437
|
+
# No filter — use railway flag for backwards compat
|
|
1438
|
+
if [ "$entry_type" = "railway-skill-dir" ] && [ "$include_railway" != "true" ]; then
|
|
1439
|
+
continue
|
|
1440
|
+
fi
|
|
1441
|
+
fi
|
|
1442
|
+
|
|
1443
|
+
local source_abs="$SCRIPT_DIR/$entry_path"
|
|
1444
|
+
local target_abs=""
|
|
1445
|
+
|
|
1446
|
+
case "$entry_type" in
|
|
1447
|
+
agent)
|
|
1448
|
+
local filename
|
|
1449
|
+
filename=$(basename "$entry_path")
|
|
1450
|
+
target_abs="$target_dir/.claude/agents/$filename"
|
|
1451
|
+
;;
|
|
1452
|
+
skill)
|
|
1453
|
+
local filename
|
|
1454
|
+
filename=$(basename "$entry_path")
|
|
1455
|
+
target_abs="$target_dir/.claude/skills/$filename"
|
|
1456
|
+
;;
|
|
1457
|
+
skill-dir)
|
|
1458
|
+
local dirname
|
|
1459
|
+
dirname=$(basename "$entry_path")
|
|
1460
|
+
target_abs="$target_dir/.claude/skills/$dirname"
|
|
1461
|
+
;;
|
|
1462
|
+
hook)
|
|
1463
|
+
local filename
|
|
1464
|
+
filename=$(basename "$entry_path")
|
|
1465
|
+
target_abs="$target_dir/.claude/hooks/$filename"
|
|
1466
|
+
;;
|
|
1467
|
+
railway-skill-dir)
|
|
1468
|
+
local dirname
|
|
1469
|
+
dirname=$(basename "$entry_path")
|
|
1470
|
+
target_abs="$target_dir/.claude/skills/$dirname"
|
|
1471
|
+
;;
|
|
1472
|
+
*)
|
|
1473
|
+
echo -e " ${YELLOW}WARN${NC} Unknown entry type: $entry_type"
|
|
1474
|
+
continue
|
|
1475
|
+
;;
|
|
1476
|
+
esac
|
|
1477
|
+
|
|
1478
|
+
# Verify source exists
|
|
1479
|
+
if [ ! -e "$source_abs" ]; then
|
|
1480
|
+
echo -e " ${RED}MISSING${NC} $entry_path (not found in repo)"
|
|
1481
|
+
ERRORS=$((ERRORS + 1))
|
|
1482
|
+
continue
|
|
1483
|
+
fi
|
|
1484
|
+
|
|
1485
|
+
$linker "$source_abs" "$target_abs"
|
|
1486
|
+
done < "$MANIFEST"
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
# ============================================================================
|
|
1490
|
+
# Deselection Handler (Phase 7)
|
|
1491
|
+
# ============================================================================
|
|
1492
|
+
# Removes items for categories that were previously installed but are now deselected.
|
|
1493
|
+
|
|
1494
|
+
remove_category_items() {
|
|
1495
|
+
local target_dir="$1"
|
|
1496
|
+
local category="$2"
|
|
1497
|
+
local items
|
|
1498
|
+
items=$(get_category_items "$category")
|
|
1499
|
+
|
|
1500
|
+
for item in $items; do
|
|
1501
|
+
local target_abs=""
|
|
1502
|
+
local bname
|
|
1503
|
+
bname=$(basename "$item")
|
|
1504
|
+
|
|
1505
|
+
case "$item" in
|
|
1506
|
+
agents/*) target_abs="$target_dir/.claude/agents/$bname" ;;
|
|
1507
|
+
skills/railway/*) target_abs="$target_dir/.claude/skills/$bname" ;;
|
|
1508
|
+
skills/*) target_abs="$target_dir/.claude/skills/$bname" ;;
|
|
1509
|
+
hooks/*) target_abs="$target_dir/.claude/hooks/$bname" ;;
|
|
1510
|
+
esac
|
|
1511
|
+
|
|
1512
|
+
# Only remove if it's a symlink pointing to our repo
|
|
1513
|
+
if [ -L "$target_abs" ]; then
|
|
1514
|
+
local link_target
|
|
1515
|
+
link_target=$(readlink "$target_abs" 2>/dev/null || true)
|
|
1516
|
+
if echo "$link_target" | grep -q "/.claude-agents/"; then
|
|
1517
|
+
rm "$target_abs"
|
|
1518
|
+
REMOVED=$((REMOVED + 1))
|
|
1519
|
+
fi
|
|
1520
|
+
fi
|
|
1521
|
+
done
|
|
1522
|
+
|
|
1523
|
+
# Remove corresponding hooks from settings.json
|
|
1524
|
+
local hooks_to_remove
|
|
1525
|
+
hooks_to_remove=$(get_category_hooks "$category")
|
|
1526
|
+
if [ -n "$hooks_to_remove" ]; then
|
|
1527
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "remove" "$hooks_to_remove"
|
|
1528
|
+
fi
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
handle_deselection() {
|
|
1532
|
+
local target_dir="$1"
|
|
1533
|
+
local old_cats="$2"
|
|
1534
|
+
local new_cats="$3"
|
|
1535
|
+
|
|
1536
|
+
for old_cat in $old_cats; do
|
|
1537
|
+
local still_selected=false
|
|
1538
|
+
for new_cat in $new_cats; do
|
|
1539
|
+
if [ "$old_cat" = "$new_cat" ]; then
|
|
1540
|
+
still_selected=true
|
|
1541
|
+
break
|
|
1542
|
+
fi
|
|
1543
|
+
done
|
|
1544
|
+
|
|
1545
|
+
if ! $still_selected; then
|
|
1546
|
+
echo -e " Removing ${YELLOW}$old_cat${NC} (deselected)..."
|
|
1547
|
+
remove_category_items "$target_dir" "$old_cat"
|
|
1548
|
+
fi
|
|
1549
|
+
done
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
# ============================================================================
|
|
1553
|
+
# Uninstall Handler (Phase 4)
|
|
1554
|
+
# ============================================================================
|
|
1555
|
+
|
|
1556
|
+
handle_uninstall() {
|
|
1557
|
+
local target_dir="$1"
|
|
1558
|
+
|
|
1559
|
+
echo -e "${BOLD}Claude Agents — Uninstall${NC}"
|
|
1560
|
+
echo -e "Target: ${CYAN}$target_dir${NC}"
|
|
1561
|
+
echo ""
|
|
1562
|
+
|
|
1563
|
+
local bdir
|
|
1564
|
+
bdir=$(backup_dir "$target_dir")
|
|
1565
|
+
local manifest_file="$bdir/manifest.txt"
|
|
1566
|
+
|
|
1567
|
+
# Count what will be removed
|
|
1568
|
+
local symlink_count=0
|
|
1569
|
+
local preserved_count=0
|
|
1570
|
+
|
|
1571
|
+
if [ -f "$manifest_file" ]; then
|
|
1572
|
+
while IFS= read -r item_path; do
|
|
1573
|
+
[ -z "$item_path" ] && continue
|
|
1574
|
+
if [ -L "$item_path" ]; then
|
|
1575
|
+
local link_target
|
|
1576
|
+
link_target=$(readlink "$item_path" 2>/dev/null || true)
|
|
1577
|
+
if echo "$link_target" | grep -q "/.claude-agents/"; then
|
|
1578
|
+
symlink_count=$((symlink_count + 1))
|
|
1579
|
+
fi
|
|
1580
|
+
elif [ -e "$item_path" ]; then
|
|
1581
|
+
preserved_count=$((preserved_count + 1))
|
|
1582
|
+
fi
|
|
1583
|
+
done < "$manifest_file"
|
|
1584
|
+
else
|
|
1585
|
+
# No manifest — scan for symlinks
|
|
1586
|
+
for subdir in agents skills hooks; do
|
|
1587
|
+
local search_dir="$target_dir/.claude/$subdir"
|
|
1588
|
+
[ ! -d "$search_dir" ] && continue
|
|
1589
|
+
for item in "$search_dir"/*; do
|
|
1590
|
+
[ ! -e "$item" ] && [ ! -L "$item" ] && continue
|
|
1591
|
+
if [ -L "$item" ]; then
|
|
1592
|
+
local link_target
|
|
1593
|
+
link_target=$(readlink "$item" 2>/dev/null || true)
|
|
1594
|
+
if echo "$link_target" | grep -q "/.claude-agents/"; then
|
|
1595
|
+
symlink_count=$((symlink_count + 1))
|
|
1596
|
+
fi
|
|
1597
|
+
fi
|
|
1598
|
+
done
|
|
1599
|
+
done
|
|
1600
|
+
fi
|
|
1601
|
+
|
|
1602
|
+
echo "This will:"
|
|
1603
|
+
echo " - Remove $symlink_count symlinks managed by claude-agents"
|
|
1604
|
+
if [ "$preserved_count" -gt 0 ]; then
|
|
1605
|
+
echo -e " - ${GREEN}Preserve $preserved_count project-specific files${NC} (not ours)"
|
|
1606
|
+
fi
|
|
1607
|
+
if [ -f "$bdir/settings.json.bak" ]; then
|
|
1608
|
+
echo " - Restore settings.json from backup"
|
|
1609
|
+
else
|
|
1610
|
+
echo " - Remove our hook entries from settings.json"
|
|
1611
|
+
fi
|
|
1612
|
+
if [ -f "$bdir/gitignore.bak" ]; then
|
|
1613
|
+
echo " - Restore .gitignore from backup"
|
|
1614
|
+
fi
|
|
1615
|
+
echo " - Remove config and backup files"
|
|
1616
|
+
echo ""
|
|
1617
|
+
|
|
1618
|
+
# Confirm
|
|
1619
|
+
printf "Proceed? [y/N] "
|
|
1620
|
+
local confirm
|
|
1621
|
+
read -r confirm < /dev/tty 2>/dev/null || read -r confirm
|
|
1622
|
+
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
1623
|
+
echo "Aborted."
|
|
1624
|
+
exit 0
|
|
1625
|
+
fi
|
|
1626
|
+
|
|
1627
|
+
echo ""
|
|
1628
|
+
|
|
1629
|
+
# Step 1: Remove symlinks
|
|
1630
|
+
local removed=0
|
|
1631
|
+
local kept=0
|
|
1632
|
+
|
|
1633
|
+
remove_our_symlinks() {
|
|
1634
|
+
local search_path="$1"
|
|
1635
|
+
if [ -L "$search_path" ]; then
|
|
1636
|
+
local lt
|
|
1637
|
+
lt=$(readlink "$search_path" 2>/dev/null || true)
|
|
1638
|
+
if echo "$lt" | grep -q "/.claude-agents/"; then
|
|
1639
|
+
rm "$search_path"
|
|
1640
|
+
removed=$((removed + 1))
|
|
1641
|
+
return
|
|
1642
|
+
fi
|
|
1643
|
+
fi
|
|
1644
|
+
if [ -e "$search_path" ] && [ ! -L "$search_path" ]; then
|
|
1645
|
+
kept=$((kept + 1))
|
|
1646
|
+
fi
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if [ -f "$manifest_file" ]; then
|
|
1650
|
+
while IFS= read -r item_path; do
|
|
1651
|
+
[ -z "$item_path" ] && continue
|
|
1652
|
+
remove_our_symlinks "$item_path"
|
|
1653
|
+
done < "$manifest_file"
|
|
1654
|
+
else
|
|
1655
|
+
for subdir in agents skills hooks; do
|
|
1656
|
+
local search_dir="$target_dir/.claude/$subdir"
|
|
1657
|
+
[ ! -d "$search_dir" ] && continue
|
|
1658
|
+
for item in "$search_dir"/*; do
|
|
1659
|
+
[ ! -e "$item" ] && [ ! -L "$item" ] && continue
|
|
1660
|
+
remove_our_symlinks "$item"
|
|
1661
|
+
done
|
|
1662
|
+
done
|
|
1663
|
+
fi
|
|
1664
|
+
|
|
1665
|
+
echo -e " Symlinks removed: ${GREEN}$removed${NC}"
|
|
1666
|
+
if [ "$kept" -gt 0 ]; then
|
|
1667
|
+
echo -e " Project files preserved: ${GREEN}$kept${NC}"
|
|
1668
|
+
fi
|
|
1669
|
+
|
|
1670
|
+
# Step 2: Restore settings.json
|
|
1671
|
+
if [ -f "$bdir/settings.json.bak" ]; then
|
|
1672
|
+
cp "$bdir/settings.json.bak" "$target_dir/.claude/settings.json"
|
|
1673
|
+
echo -e " Settings.json: ${GREEN}restored from backup${NC}"
|
|
1674
|
+
elif [ -f "$target_dir/.claude/settings.json" ]; then
|
|
1675
|
+
# No backup — remove our hooks by fingerprint
|
|
1676
|
+
local hooks_to_remove
|
|
1677
|
+
hooks_to_remove=""
|
|
1678
|
+
if config_exists "$target_dir"; then
|
|
1679
|
+
load_config "$target_dir"
|
|
1680
|
+
hooks_to_remove="$CONF_SETTINGS_HOOKS_ADDED"
|
|
1681
|
+
else
|
|
1682
|
+
hooks_to_remove="sync-agents triage-router session-end session-bootstrap pre-bash-guard pre-task-context pre-edit-guard pre-write-guard post-test-summary post-edit-lint post-deploy-health"
|
|
1683
|
+
fi
|
|
1684
|
+
if [ -n "$hooks_to_remove" ]; then
|
|
1685
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "remove" "$hooks_to_remove"
|
|
1686
|
+
echo -e " Settings.json: ${GREEN}removed our hook entries${NC}"
|
|
1687
|
+
fi
|
|
1688
|
+
fi
|
|
1689
|
+
|
|
1690
|
+
# Step 3: Restore .gitignore
|
|
1691
|
+
if [ -f "$bdir/gitignore.bak" ]; then
|
|
1692
|
+
cp "$bdir/gitignore.bak" "$target_dir/.claude/.gitignore"
|
|
1693
|
+
echo -e " .gitignore: ${GREEN}restored from backup${NC}"
|
|
1694
|
+
else
|
|
1695
|
+
remove_gitignore_block "$target_dir"
|
|
1696
|
+
echo -e " .gitignore: ${GREEN}removed our marker block${NC}"
|
|
1697
|
+
fi
|
|
1698
|
+
|
|
1699
|
+
# Step 3b: Remove CLAUDE.md managed block
|
|
1700
|
+
remove_claudemd_block "$target_dir"
|
|
1701
|
+
|
|
1702
|
+
# Step 4: Remove empty directories
|
|
1703
|
+
for subdir in agents skills hooks; do
|
|
1704
|
+
local dir="$target_dir/.claude/$subdir"
|
|
1705
|
+
if [ -d "$dir" ]; then
|
|
1706
|
+
# Remove if empty (rmdir fails on non-empty, which is what we want)
|
|
1707
|
+
rmdir "$dir" 2>/dev/null && echo -e " ${DIM}Removed empty .claude/$subdir/${NC}" || true
|
|
1708
|
+
fi
|
|
1709
|
+
done
|
|
1710
|
+
|
|
1711
|
+
# Step 5: Remove our config and backup
|
|
1712
|
+
rm -f "$(config_path "$target_dir")"
|
|
1713
|
+
rm -rf "$bdir"
|
|
1714
|
+
|
|
1715
|
+
echo ""
|
|
1716
|
+
echo -e "${BOLD}Uninstall complete.${NC} Your project is restored to its pre-install state."
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
# ============================================================================
|
|
1720
|
+
# Setup Handler (Phase 2 + 7)
|
|
1721
|
+
# ============================================================================
|
|
1722
|
+
# Full interactive setup flow: detect state, show menu, install, configure.
|
|
1723
|
+
|
|
1724
|
+
handle_setup() {
|
|
1725
|
+
local target_dir="$1"
|
|
1726
|
+
|
|
1727
|
+
# Detect project state
|
|
1728
|
+
local project_state
|
|
1729
|
+
project_state=$(detect_project_state "$target_dir")
|
|
1730
|
+
|
|
1731
|
+
# Load existing config if present (for re-setup)
|
|
1732
|
+
local old_categories=""
|
|
1733
|
+
if config_exists "$target_dir"; then
|
|
1734
|
+
load_config "$target_dir"
|
|
1735
|
+
old_categories="$CONF_INSTALLED_CATEGORIES"
|
|
1736
|
+
fi
|
|
1737
|
+
|
|
1738
|
+
# Show interactive menu
|
|
1739
|
+
local new_categories
|
|
1740
|
+
new_categories=$(show_interactive_menu "$target_dir" "$old_categories" "$project_state")
|
|
1741
|
+
|
|
1742
|
+
if [ -z "$new_categories" ]; then
|
|
1743
|
+
echo -e "${YELLOW}No categories selected. Nothing to install.${NC}"
|
|
1744
|
+
exit 0
|
|
1745
|
+
fi
|
|
1746
|
+
|
|
1747
|
+
echo -e "${BOLD}Claude Agents${NC} — Installing into: ${CYAN}$target_dir${NC}"
|
|
1748
|
+
echo ""
|
|
1749
|
+
|
|
1750
|
+
# Snapshot backup (first time only)
|
|
1751
|
+
snapshot_pre_install "$target_dir"
|
|
1752
|
+
|
|
1753
|
+
# Handle deselections (re-setup only)
|
|
1754
|
+
if [ -n "$old_categories" ]; then
|
|
1755
|
+
handle_deselection "$target_dir" "$old_categories" "$new_categories"
|
|
1756
|
+
fi
|
|
1757
|
+
|
|
1758
|
+
# Build filter list from selected categories
|
|
1759
|
+
local filter_items
|
|
1760
|
+
filter_items=$(items_for_categories "$new_categories")
|
|
1761
|
+
|
|
1762
|
+
# Ensure directories
|
|
1763
|
+
mkdir -p "$target_dir/.claude/agents"
|
|
1764
|
+
mkdir -p "$target_dir/.claude/skills"
|
|
1765
|
+
mkdir -p "$target_dir/.claude/hooks"
|
|
1766
|
+
|
|
1767
|
+
# Process manifest (filtered)
|
|
1768
|
+
echo -e "Mode: ${GREEN}setup${NC} (interactive install)"
|
|
1769
|
+
process_manifest "$target_dir" "safe" "false" "$filter_items"
|
|
1770
|
+
|
|
1771
|
+
# Merge settings.json hooks
|
|
1772
|
+
local hooks_to_add
|
|
1773
|
+
hooks_to_add=$(hooks_for_categories "$new_categories")
|
|
1774
|
+
if [ -n "$hooks_to_add" ]; then
|
|
1775
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "add" "$hooks_to_add"
|
|
1776
|
+
fi
|
|
1777
|
+
|
|
1778
|
+
# If hooks were deselected, make sure they're removed
|
|
1779
|
+
if [ -n "$old_categories" ]; then
|
|
1780
|
+
local old_hooks new_hooks
|
|
1781
|
+
old_hooks=$(hooks_for_categories "$old_categories")
|
|
1782
|
+
new_hooks=$(hooks_for_categories "$new_categories")
|
|
1783
|
+
for oh in $old_hooks; do
|
|
1784
|
+
local still_needed=false
|
|
1785
|
+
for nh in $new_hooks; do
|
|
1786
|
+
if [ "$oh" = "$nh" ]; then
|
|
1787
|
+
still_needed=true
|
|
1788
|
+
break
|
|
1789
|
+
fi
|
|
1790
|
+
done
|
|
1791
|
+
if ! $still_needed; then
|
|
1792
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "remove" "$oh"
|
|
1793
|
+
fi
|
|
1794
|
+
done
|
|
1795
|
+
fi
|
|
1796
|
+
|
|
1797
|
+
# Update .gitignore marker block
|
|
1798
|
+
update_gitignore_block "$target_dir" "$filter_items"
|
|
1799
|
+
|
|
1800
|
+
# Create CLAUDE.md from template if it doesn't exist (greenfield setup)
|
|
1801
|
+
if [ ! -f "$target_dir/CLAUDE.md" ]; then
|
|
1802
|
+
local project_name
|
|
1803
|
+
project_name=$(basename "$target_dir")
|
|
1804
|
+
sed "s/{{PROJECT_NAME}}/$project_name/g" "$SCRIPT_DIR/templates/CLAUDE.md.template" > "$target_dir/CLAUDE.md"
|
|
1805
|
+
# Inject toolkit managed block into freshly created CLAUDE.md
|
|
1806
|
+
if [ -f "$CLAUDEMD_MANAGED_TEMPLATE" ]; then
|
|
1807
|
+
local tmp block_content
|
|
1808
|
+
tmp=$(mktemp)
|
|
1809
|
+
block_content="$CLAUDEMD_MARKER_START"$'\n'
|
|
1810
|
+
block_content+=$(cat "$CLAUDEMD_MANAGED_TEMPLATE")
|
|
1811
|
+
block_content+=$'\n'"$CLAUDEMD_MARKER_END"
|
|
1812
|
+
local first_line=true
|
|
1813
|
+
while IFS= read -r line; do
|
|
1814
|
+
echo "$line" >> "$tmp"
|
|
1815
|
+
if $first_line; then
|
|
1816
|
+
first_line=false
|
|
1817
|
+
echo "" >> "$tmp"
|
|
1818
|
+
echo "$block_content" >> "$tmp"
|
|
1819
|
+
fi
|
|
1820
|
+
done < "$target_dir/CLAUDE.md"
|
|
1821
|
+
mv "$tmp" "$target_dir/CLAUDE.md"
|
|
1822
|
+
fi
|
|
1823
|
+
echo -e " ${GREEN}Created${NC} CLAUDE.md (edit the TODOs)"
|
|
1824
|
+
fi
|
|
1825
|
+
|
|
1826
|
+
# Update CLAUDE.md managed block (ask on setup, silent on sync)
|
|
1827
|
+
if [ "$MODE" = "setup" ]; then
|
|
1828
|
+
update_claudemd_block "$target_dir" "ask"
|
|
1829
|
+
else
|
|
1830
|
+
update_claudemd_block "$target_dir" "silent"
|
|
1831
|
+
fi
|
|
1832
|
+
|
|
1833
|
+
# Make hooks executable
|
|
1834
|
+
for hook in "$target_dir/.claude/hooks/"*.sh; do
|
|
1835
|
+
if [ -f "$hook" ] || [ -L "$hook" ]; then
|
|
1836
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
1837
|
+
fi
|
|
1838
|
+
done
|
|
1839
|
+
|
|
1840
|
+
# Write config
|
|
1841
|
+
write_config "$target_dir" "$new_categories"
|
|
1842
|
+
|
|
1843
|
+
# Post-install record (manifest.txt of symlinks)
|
|
1844
|
+
post_install_record "$target_dir"
|
|
1845
|
+
|
|
1846
|
+
# Summary
|
|
1847
|
+
echo ""
|
|
1848
|
+
echo -e "${BOLD}Done!${NC}"
|
|
1849
|
+
echo -e " Created: ${GREEN}$CREATED${NC}"
|
|
1850
|
+
echo -e " Updated: ${GREEN}$UPDATED${NC}"
|
|
1851
|
+
echo -e " Skipped: ${YELLOW}$SKIPPED${NC} (project overrides)"
|
|
1852
|
+
if [ "$REMOVED" -gt 0 ]; then
|
|
1853
|
+
echo -e " Removed: ${YELLOW}$REMOVED${NC} (deselected)"
|
|
1854
|
+
fi
|
|
1855
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
1856
|
+
echo -e " Errors: ${RED}$ERRORS${NC}"
|
|
1857
|
+
fi
|
|
1858
|
+
|
|
1859
|
+
# Post-setup guidance based on project state
|
|
1860
|
+
echo ""
|
|
1861
|
+
case "$project_state" in
|
|
1862
|
+
greenfield)
|
|
1863
|
+
echo -e "${BOLD}Your project is ready!${NC} Here's what was set up:"
|
|
1864
|
+
echo ""
|
|
1865
|
+
echo " Agents: AI-powered specialists (architect, code-reviewer, design, PM, QA)"
|
|
1866
|
+
echo " Skills: Workflow automations (/planning, /implement, /pr, /issue)"
|
|
1867
|
+
echo " Hooks: Auto-sync on session start + cost-saving triage router"
|
|
1868
|
+
echo ""
|
|
1869
|
+
echo "Start with:"
|
|
1870
|
+
echo " - Run ${CYAN}claude${NC} to begin — the triage router auto-delegates to cheap agents"
|
|
1871
|
+
echo " - Use ${CYAN}/planning <feature>${NC} to plan a feature with a team of agents"
|
|
1872
|
+
echo " - Use ${CYAN}/pr${NC} to create a PR with automated QA"
|
|
1873
|
+
echo " - Use ${CYAN}/sync status${NC} to see what's installed"
|
|
1874
|
+
;;
|
|
1875
|
+
minimal)
|
|
1876
|
+
echo -e "${BOLD}Tools installed alongside your existing setup.${NC}"
|
|
1877
|
+
echo ""
|
|
1878
|
+
echo " Your existing agents/skills/hooks are auto-discovered by the triage"
|
|
1879
|
+
echo " router and will be included in routing decisions alongside ours."
|
|
1880
|
+
echo ""
|
|
1881
|
+
echo " Use ${CYAN}/sync status${NC} to see everything that's linked."
|
|
1882
|
+
echo " Use ${CYAN}/sync setup${NC} to change your selections later."
|
|
1883
|
+
;;
|
|
1884
|
+
established)
|
|
1885
|
+
echo -e "${BOLD}Portable tools symlinked — your project files are untouched.${NC}"
|
|
1886
|
+
echo ""
|
|
1887
|
+
echo " The triage router sees both your tools and ours. Routing is automatic."
|
|
1888
|
+
echo " Use ${CYAN}/sync status${NC} to review. Use ${CYAN}/sync setup${NC} to adjust."
|
|
1889
|
+
;;
|
|
1890
|
+
esac
|
|
1891
|
+
|
|
1892
|
+
# Cross-sell clade/arth if available
|
|
1893
|
+
if command -v clade &>/dev/null || command -v arth &>/dev/null; then
|
|
1894
|
+
echo ""
|
|
1895
|
+
echo -e "${BOLD}Also available:${NC}"
|
|
1896
|
+
if command -v clade &>/dev/null; then
|
|
1897
|
+
echo -e " ${CYAN}clade sync${NC} Translate toolkit to Cursor/Codex/Kiro/Gemini"
|
|
1898
|
+
fi
|
|
1899
|
+
if command -v arth &>/dev/null; then
|
|
1900
|
+
echo -e " ${CYAN}arth setup .${NC} Full setup: toolkit + multi-engine + dashboard"
|
|
1901
|
+
fi
|
|
1902
|
+
fi
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
# ============================================================================
|
|
1906
|
+
# Sync From Config (Phase 6)
|
|
1907
|
+
# ============================================================================
|
|
1908
|
+
# Silent sync respecting previously configured categories.
|
|
1909
|
+
|
|
1910
|
+
handle_sync_from_config() {
|
|
1911
|
+
local target_dir="$1"
|
|
1912
|
+
|
|
1913
|
+
if ! config_exists "$target_dir"; then
|
|
1914
|
+
# No config — fall through to full sync (backwards compat)
|
|
1915
|
+
return 1
|
|
1916
|
+
fi
|
|
1917
|
+
|
|
1918
|
+
load_config "$target_dir"
|
|
1919
|
+
|
|
1920
|
+
local filter_items
|
|
1921
|
+
filter_items=$(items_for_categories "$CONF_INSTALLED_CATEGORIES")
|
|
1922
|
+
|
|
1923
|
+
# Ensure directories
|
|
1924
|
+
mkdir -p "$target_dir/.claude/agents"
|
|
1925
|
+
mkdir -p "$target_dir/.claude/skills"
|
|
1926
|
+
mkdir -p "$target_dir/.claude/hooks"
|
|
1927
|
+
|
|
1928
|
+
echo -e "${BOLD}Claude Agents${NC} — Installing into: ${CYAN}$target_dir${NC}"
|
|
1929
|
+
echo ""
|
|
1930
|
+
echo -e "Mode: ${GREEN}sync${NC} (safe symlinks — won't touch regular files)"
|
|
1931
|
+
|
|
1932
|
+
process_manifest "$target_dir" "safe" "false" "$filter_items"
|
|
1933
|
+
|
|
1934
|
+
# Merge settings.json hooks (ensure all configured hooks are registered)
|
|
1935
|
+
local hooks_to_add
|
|
1936
|
+
hooks_to_add=$(hooks_for_categories "$CONF_INSTALLED_CATEGORIES")
|
|
1937
|
+
if [ -n "$hooks_to_add" ]; then
|
|
1938
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "add" "$hooks_to_add"
|
|
1939
|
+
fi
|
|
1940
|
+
|
|
1941
|
+
# Update CLAUDE.md managed block (silent — only updates existing markers)
|
|
1942
|
+
update_claudemd_block "$target_dir" "silent"
|
|
1943
|
+
|
|
1944
|
+
# Make hooks executable
|
|
1945
|
+
for hook in "$target_dir/.claude/hooks/"*.sh; do
|
|
1946
|
+
if [ -f "$hook" ] || [ -L "$hook" ]; then
|
|
1947
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
1948
|
+
fi
|
|
1949
|
+
done
|
|
1950
|
+
|
|
1951
|
+
# Update manifest.txt
|
|
1952
|
+
post_install_record "$target_dir"
|
|
1953
|
+
|
|
1954
|
+
# Summary
|
|
1955
|
+
echo ""
|
|
1956
|
+
echo -e "${BOLD}Done!${NC}"
|
|
1957
|
+
echo -e " Created: ${GREEN}$CREATED${NC}"
|
|
1958
|
+
echo -e " Updated: ${GREEN}$UPDATED${NC}"
|
|
1959
|
+
echo -e " Skipped: ${YELLOW}$SKIPPED${NC} (project overrides)"
|
|
1960
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
1961
|
+
echo -e " Errors: ${RED}$ERRORS${NC}"
|
|
1962
|
+
fi
|
|
1963
|
+
|
|
1964
|
+
return 0
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
# ============================================================================
|
|
1968
|
+
# Status Report
|
|
1969
|
+
# ============================================================================
|
|
1970
|
+
|
|
1971
|
+
show_status() {
|
|
1972
|
+
local target_dir="$1"
|
|
1973
|
+
local include_railway="$2"
|
|
1974
|
+
|
|
1975
|
+
echo -e "${BOLD}Claude Agents — Status Report${NC}"
|
|
1976
|
+
echo -e "Project: ${CYAN}$target_dir${NC}"
|
|
1977
|
+
echo -e "Repo: ${CYAN}$SCRIPT_DIR${NC}"
|
|
1978
|
+
echo ""
|
|
1979
|
+
|
|
1980
|
+
# Show config info if present
|
|
1981
|
+
if config_exists "$target_dir"; then
|
|
1982
|
+
load_config "$target_dir"
|
|
1983
|
+
echo -e "Config: ${GREEN}found${NC} (categories: $CONF_INSTALLED_CATEGORIES)"
|
|
1984
|
+
echo ""
|
|
1985
|
+
else
|
|
1986
|
+
echo -e "Config: ${YELLOW}none${NC} (using full manifest)"
|
|
1987
|
+
echo ""
|
|
1988
|
+
fi
|
|
1989
|
+
|
|
1990
|
+
local linked=0
|
|
1991
|
+
local overridden=0
|
|
1992
|
+
local missing=0
|
|
1993
|
+
|
|
1994
|
+
printf "%-14s %-30s %s\n" "TYPE" "NAME" "STATUS"
|
|
1995
|
+
printf "%-14s %-30s %s\n" "----" "----" "------"
|
|
1996
|
+
|
|
1997
|
+
while IFS= read -r line; do
|
|
1998
|
+
[[ "$line" =~ ^#.*$ ]] && continue
|
|
1999
|
+
[[ -z "$line" ]] && continue
|
|
2000
|
+
|
|
2001
|
+
local entry_type entry_path
|
|
2002
|
+
entry_type=$(echo "$line" | awk '{print $1}')
|
|
2003
|
+
entry_path=$(echo "$line" | awk '{print $2}')
|
|
2004
|
+
|
|
2005
|
+
if [ "$entry_type" = "railway-skill-dir" ] && [ "$include_railway" != "true" ]; then
|
|
2006
|
+
continue
|
|
2007
|
+
fi
|
|
2008
|
+
|
|
2009
|
+
local target_abs=""
|
|
2010
|
+
local display_name
|
|
2011
|
+
display_name=$(basename "$entry_path")
|
|
2012
|
+
|
|
2013
|
+
case "$entry_type" in
|
|
2014
|
+
agent) target_abs="$target_dir/.claude/agents/$display_name" ;;
|
|
2015
|
+
skill) target_abs="$target_dir/.claude/skills/$display_name" ;;
|
|
2016
|
+
skill-dir) target_abs="$target_dir/.claude/skills/$display_name" ;;
|
|
2017
|
+
hook) target_abs="$target_dir/.claude/hooks/$display_name" ;;
|
|
2018
|
+
railway-skill-dir)
|
|
2019
|
+
entry_type="railway"
|
|
2020
|
+
target_abs="$target_dir/.claude/skills/$display_name"
|
|
2021
|
+
;;
|
|
2022
|
+
esac
|
|
2023
|
+
|
|
2024
|
+
local status=""
|
|
2025
|
+
if [ -L "$target_abs" ]; then
|
|
2026
|
+
status="${GREEN}symlinked${NC}"
|
|
2027
|
+
linked=$((linked + 1))
|
|
2028
|
+
elif [ -e "$target_abs" ]; then
|
|
2029
|
+
status="${YELLOW}override${NC}"
|
|
2030
|
+
overridden=$((overridden + 1))
|
|
2031
|
+
else
|
|
2032
|
+
status="${RED}missing${NC}"
|
|
2033
|
+
missing=$((missing + 1))
|
|
2034
|
+
fi
|
|
2035
|
+
|
|
2036
|
+
printf "%-14s %-30s " "$entry_type" "$display_name"
|
|
2037
|
+
echo -e "$status"
|
|
2038
|
+
done < "$MANIFEST"
|
|
2039
|
+
|
|
2040
|
+
echo ""
|
|
2041
|
+
echo -e "Symlinked: ${GREEN}$linked${NC} Overridden: ${YELLOW}$overridden${NC} Missing: ${RED}$missing${NC}"
|
|
2042
|
+
|
|
2043
|
+
# Show project-specific items (auto-discovered)
|
|
2044
|
+
local project_agents=0
|
|
2045
|
+
local project_skills=0
|
|
2046
|
+
if [ -d "$target_dir/.claude/agents" ]; then
|
|
2047
|
+
for f in "$target_dir/.claude/agents"/*.md; do
|
|
2048
|
+
[ -f "$f" ] && [ ! -L "$f" ] && project_agents=$((project_agents + 1))
|
|
2049
|
+
done
|
|
2050
|
+
fi
|
|
2051
|
+
if [ -d "$target_dir/.claude/skills" ]; then
|
|
2052
|
+
for d in "$target_dir/.claude/skills"/*/; do
|
|
2053
|
+
[ -d "$d" ] && [ ! -L "${d%/}" ] && project_skills=$((project_skills + 1))
|
|
2054
|
+
done
|
|
2055
|
+
fi
|
|
2056
|
+
if [ $((project_agents + project_skills)) -gt 0 ]; then
|
|
2057
|
+
echo ""
|
|
2058
|
+
echo -e "${DIM}Project-specific (auto-discovered by triage router):${NC}"
|
|
2059
|
+
if [ -d "$target_dir/.claude/agents" ]; then
|
|
2060
|
+
for f in "$target_dir/.claude/agents"/*.md; do
|
|
2061
|
+
[ -f "$f" ] && [ ! -L "$f" ] && printf " %-14s %s\n" "agent" "$(basename "$f")"
|
|
2062
|
+
done
|
|
2063
|
+
fi
|
|
2064
|
+
if [ -d "$target_dir/.claude/skills" ]; then
|
|
2065
|
+
for d in "$target_dir/.claude/skills"/*/; do
|
|
2066
|
+
[ -d "$d" ] && [ ! -L "${d%/}" ] && printf " %-14s %s\n" "skill" "$(basename "$d")"
|
|
2067
|
+
done
|
|
2068
|
+
fi
|
|
2069
|
+
fi
|
|
2070
|
+
|
|
2071
|
+
# License status
|
|
2072
|
+
echo ""
|
|
2073
|
+
if [ -f "$LICENSE_FILE" ]; then
|
|
2074
|
+
local key
|
|
2075
|
+
key=$(cat "$LICENSE_FILE")
|
|
2076
|
+
local masked="${key:0:9}****-****"
|
|
2077
|
+
echo -e "License: ${GREEN}$masked${NC}"
|
|
2078
|
+
else
|
|
2079
|
+
echo -e "License: ${RED}not configured${NC}"
|
|
2080
|
+
fi
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
# ============================================================================
|
|
2084
|
+
# Scaffold (Greenfield)
|
|
2085
|
+
# ============================================================================
|
|
2086
|
+
|
|
2087
|
+
scaffold_project() {
|
|
2088
|
+
local target_dir="$1"
|
|
2089
|
+
|
|
2090
|
+
echo -e "${BOLD}Scaffolding new project...${NC}"
|
|
2091
|
+
|
|
2092
|
+
# Create directory structure
|
|
2093
|
+
mkdir -p "$target_dir/.claude/agents"
|
|
2094
|
+
mkdir -p "$target_dir/.claude/skills"
|
|
2095
|
+
mkdir -p "$target_dir/.claude/hooks"
|
|
2096
|
+
mkdir -p "$target_dir/.claude/plans"
|
|
2097
|
+
mkdir -p "$target_dir/.claude/qa-knowledge"
|
|
2098
|
+
|
|
2099
|
+
# Copy settings template if doesn't exist
|
|
2100
|
+
if [ ! -f "$target_dir/.claude/settings.json" ]; then
|
|
2101
|
+
cp "$SCRIPT_DIR/templates/settings.json" "$target_dir/.claude/settings.json"
|
|
2102
|
+
echo -e " ${GREEN}Created${NC} .claude/settings.json"
|
|
2103
|
+
else
|
|
2104
|
+
echo -e " ${YELLOW}Exists${NC} .claude/settings.json"
|
|
2105
|
+
fi
|
|
2106
|
+
|
|
2107
|
+
# Copy CLAUDE.md template if doesn't exist
|
|
2108
|
+
if [ ! -f "$target_dir/CLAUDE.md" ]; then
|
|
2109
|
+
local project_name
|
|
2110
|
+
project_name=$(basename "$target_dir")
|
|
2111
|
+
sed "s/{{PROJECT_NAME}}/$project_name/g" "$SCRIPT_DIR/templates/CLAUDE.md.template" > "$target_dir/CLAUDE.md"
|
|
2112
|
+
# Inject toolkit managed block into freshly created CLAUDE.md
|
|
2113
|
+
if [ -f "$CLAUDEMD_MANAGED_TEMPLATE" ]; then
|
|
2114
|
+
local tmp block_content
|
|
2115
|
+
tmp=$(mktemp)
|
|
2116
|
+
block_content="$CLAUDEMD_MARKER_START"$'\n'
|
|
2117
|
+
block_content+=$(cat "$CLAUDEMD_MANAGED_TEMPLATE")
|
|
2118
|
+
block_content+=$'\n'"$CLAUDEMD_MARKER_END"
|
|
2119
|
+
local first_line=true
|
|
2120
|
+
while IFS= read -r line; do
|
|
2121
|
+
echo "$line" >> "$tmp"
|
|
2122
|
+
if $first_line; then
|
|
2123
|
+
first_line=false
|
|
2124
|
+
echo "" >> "$tmp"
|
|
2125
|
+
echo "$block_content" >> "$tmp"
|
|
2126
|
+
fi
|
|
2127
|
+
done < "$target_dir/CLAUDE.md"
|
|
2128
|
+
mv "$tmp" "$target_dir/CLAUDE.md"
|
|
2129
|
+
fi
|
|
2130
|
+
echo -e " ${GREEN}Created${NC} CLAUDE.md (edit the TODOs)"
|
|
2131
|
+
else
|
|
2132
|
+
echo -e " ${YELLOW}Exists${NC} CLAUDE.md"
|
|
2133
|
+
fi
|
|
2134
|
+
|
|
2135
|
+
# Copy example agents for reference
|
|
2136
|
+
if [ -d "$SCRIPT_DIR/examples/agents" ]; then
|
|
2137
|
+
echo ""
|
|
2138
|
+
echo "Example project-specific agents available at:"
|
|
2139
|
+
echo " $SCRIPT_DIR/examples/agents/"
|
|
2140
|
+
echo " Copy any you need to: $target_dir/.claude/agents/"
|
|
2141
|
+
fi
|
|
2142
|
+
|
|
2143
|
+
echo ""
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
# ============================================================================
|
|
2147
|
+
# Brownfield Assessment (--assess)
|
|
2148
|
+
# ============================================================================
|
|
2149
|
+
|
|
2150
|
+
handle_assess() {
|
|
2151
|
+
local target_dir="$1"
|
|
2152
|
+
|
|
2153
|
+
echo -e "${BOLD}Claude Agents — Brownfield Assessment${NC}"
|
|
2154
|
+
echo -e "Project: ${CYAN}$target_dir${NC}"
|
|
2155
|
+
echo ""
|
|
2156
|
+
|
|
2157
|
+
# Legend
|
|
2158
|
+
echo -e "${BOLD}Legend:${NC}"
|
|
2159
|
+
echo -e " ✓ ${GREEN}SYMLINK${NC} Already managed by toolkit. Auto-updated on sync."
|
|
2160
|
+
echo -e " = ${CYAN}IDENTICAL${NC} Same content as toolkit. Safe to convert to symlink."
|
|
2161
|
+
echo -e " ⚠ ${YELLOW}STALE${NC} Much smaller than toolkit version (<70% size)."
|
|
2162
|
+
echo -e " Could be: outdated copy OR completely different file sharing a name."
|
|
2163
|
+
echo -e " ★ ${BOLD}CUSTOMIZED${NC} Much larger than toolkit (>150% size). Your project-specific content."
|
|
2164
|
+
echo -e " ~ ${DIM}MODIFIED${NC} Similar size but different content. Your edits preserved."
|
|
2165
|
+
echo -e " ◆ PROJECT-ONLY No toolkit equivalent. Unique to your project."
|
|
2166
|
+
echo ""
|
|
2167
|
+
|
|
2168
|
+
local section_symlinks="" section_identical="" section_stale=""
|
|
2169
|
+
local section_customized="" section_modified="" section_missing=""
|
|
2170
|
+
local count_symlinks=0 count_identical=0 count_stale=0
|
|
2171
|
+
local count_customized=0 count_modified=0 count_missing=0
|
|
2172
|
+
local stale_data=()
|
|
2173
|
+
|
|
2174
|
+
while IFS= read -r line; do
|
|
2175
|
+
[[ "$line" =~ ^#.*$ ]] && continue
|
|
2176
|
+
[[ -z "$line" ]] && continue
|
|
2177
|
+
|
|
2178
|
+
local entry_type entry_path
|
|
2179
|
+
entry_type=$(echo "$line" | awk '{print $1}')
|
|
2180
|
+
entry_path=$(echo "$line" | awk '{print $2}')
|
|
2181
|
+
|
|
2182
|
+
local source_abs="$SCRIPT_DIR/$entry_path"
|
|
2183
|
+
local target_abs=""
|
|
2184
|
+
local display_name
|
|
2185
|
+
display_name=$(basename "$entry_path")
|
|
2186
|
+
|
|
2187
|
+
case "$entry_type" in
|
|
2188
|
+
agent) target_abs="$target_dir/.claude/agents/$display_name" ;;
|
|
2189
|
+
skill|skill-dir) target_abs="$target_dir/.claude/skills/$display_name" ;;
|
|
2190
|
+
hook) target_abs="$target_dir/.claude/hooks/$display_name" ;;
|
|
2191
|
+
railway-skill-dir) target_abs="$target_dir/.claude/skills/$display_name" ;;
|
|
2192
|
+
*) continue ;;
|
|
2193
|
+
esac
|
|
2194
|
+
|
|
2195
|
+
if [ ! -e "$source_abs" ]; then
|
|
2196
|
+
continue
|
|
2197
|
+
fi
|
|
2198
|
+
|
|
2199
|
+
local classification
|
|
2200
|
+
classification=$(classify_file "$source_abs" "$target_abs")
|
|
2201
|
+
|
|
2202
|
+
local source_size target_size
|
|
2203
|
+
source_size=$(get_size "$source_abs")
|
|
2204
|
+
target_size=$(get_size "$target_abs")
|
|
2205
|
+
|
|
2206
|
+
case "$classification" in
|
|
2207
|
+
SYMLINK)
|
|
2208
|
+
local link_target
|
|
2209
|
+
link_target=$(readlink "$target_abs" 2>/dev/null || true)
|
|
2210
|
+
section_symlinks="${section_symlinks} ✓ $display_name → $link_target\n"
|
|
2211
|
+
count_symlinks=$((count_symlinks + 1))
|
|
2212
|
+
;;
|
|
2213
|
+
IDENTICAL)
|
|
2214
|
+
section_identical="${section_identical} = $display_name (${target_size}B = ${source_size}B)\n"
|
|
2215
|
+
count_identical=$((count_identical + 1))
|
|
2216
|
+
;;
|
|
2217
|
+
STALE)
|
|
2218
|
+
local ratio=$((target_size * 100 / source_size))
|
|
2219
|
+
stale_data+=("$source_abs|$target_abs|$display_name|$source_size|$target_size|$ratio")
|
|
2220
|
+
section_stale="${section_stale} ⚠ $display_name (${target_size}B vs ${source_size}B toolkit — ${ratio}%)\n"
|
|
2221
|
+
count_stale=$((count_stale + 1))
|
|
2222
|
+
;;
|
|
2223
|
+
CUSTOMIZED)
|
|
2224
|
+
local ratio=$((target_size * 100 / source_size))
|
|
2225
|
+
section_customized="${section_customized} ★ $display_name (${target_size}B vs ${source_size}B — ${ratio}%)\n"
|
|
2226
|
+
count_customized=$((count_customized + 1))
|
|
2227
|
+
;;
|
|
2228
|
+
MODIFIED)
|
|
2229
|
+
local ratio=$((target_size * 100 / source_size))
|
|
2230
|
+
section_modified="${section_modified} ~ $display_name (${target_size}B vs ${source_size}B — ${ratio}%)\n"
|
|
2231
|
+
count_modified=$((count_modified + 1))
|
|
2232
|
+
;;
|
|
2233
|
+
MISSING)
|
|
2234
|
+
section_missing="${section_missing} - $display_name\n"
|
|
2235
|
+
count_missing=$((count_missing + 1))
|
|
2236
|
+
;;
|
|
2237
|
+
esac
|
|
2238
|
+
done < "$MANIFEST"
|
|
2239
|
+
|
|
2240
|
+
# Display sections
|
|
2241
|
+
if [ $count_symlinks -gt 0 ]; then
|
|
2242
|
+
echo -e " ${GREEN}SYMLINKS${NC} (managed by toolkit — auto-updated):"
|
|
2243
|
+
echo -e "$section_symlinks"
|
|
2244
|
+
fi
|
|
2245
|
+
|
|
2246
|
+
if [ $count_identical -gt 0 ]; then
|
|
2247
|
+
echo -e " ${CYAN}IDENTICAL${NC} (can safely convert to symlink):"
|
|
2248
|
+
echo -e "$section_identical"
|
|
2249
|
+
fi
|
|
2250
|
+
|
|
2251
|
+
if [ ${#stale_data[@]} -gt 0 ]; then
|
|
2252
|
+
echo -e " ${YELLOW}STALE${NC} (recommend upgrade — review diffs below):"
|
|
2253
|
+
for _stale_entry in "${stale_data[@]}"; do
|
|
2254
|
+
IFS='|' read -r _src _tgt _dname _ssize _tsize _ratio <<< "$_stale_entry"
|
|
2255
|
+
echo -e " ⚠ $_dname (${_tsize}B vs ${_ssize}B toolkit — ${_ratio}%)"
|
|
2256
|
+
# Show preview of first line from each version
|
|
2257
|
+
local _your_preview="" _toolkit_preview=""
|
|
2258
|
+
if [ -f "$_tgt" ]; then
|
|
2259
|
+
_your_preview=$(head -5 "$_tgt" 2>/dev/null | grep -v '^$' | grep -v '^---$' | head -1 | cut -c1-80)
|
|
2260
|
+
elif [ -d "$_tgt" ]; then
|
|
2261
|
+
_your_preview="[directory: $(ls "$_tgt" 2>/dev/null | wc -l | tr -d ' ') files]"
|
|
2262
|
+
fi
|
|
2263
|
+
if [ -f "$_src" ]; then
|
|
2264
|
+
_toolkit_preview=$(head -5 "$_src" 2>/dev/null | grep -v '^$' | grep -v '^---$' | head -1 | cut -c1-80)
|
|
2265
|
+
elif [ -d "$_src" ]; then
|
|
2266
|
+
_toolkit_preview="[directory: $(ls "$_src" 2>/dev/null | wc -l | tr -d ' ') files]"
|
|
2267
|
+
fi
|
|
2268
|
+
echo -e " ${DIM}--- your version ---${NC}"
|
|
2269
|
+
echo -e " $_your_preview"
|
|
2270
|
+
echo -e " ${DIM}--- toolkit version ---${NC}"
|
|
2271
|
+
echo -e " $_toolkit_preview"
|
|
2272
|
+
echo -e " ${DIM}→ --upgrade will ASK before converting this file.${NC}"
|
|
2273
|
+
echo ""
|
|
2274
|
+
done
|
|
2275
|
+
fi
|
|
2276
|
+
|
|
2277
|
+
if [ $count_customized -gt 0 ]; then
|
|
2278
|
+
echo -e " ${BOLD}CUSTOMIZED${NC} (project-specific — will not touch):"
|
|
2279
|
+
echo -e "$section_customized"
|
|
2280
|
+
fi
|
|
2281
|
+
|
|
2282
|
+
if [ $count_modified -gt 0 ]; then
|
|
2283
|
+
echo -e " ${DIM}MODIFIED${NC} (similar size but different — will not touch):"
|
|
2284
|
+
echo -e "$section_modified"
|
|
2285
|
+
fi
|
|
2286
|
+
|
|
2287
|
+
if [ $count_missing -gt 0 ]; then
|
|
2288
|
+
echo -e " ${RED}MISSING${NC} (not installed):"
|
|
2289
|
+
echo -e "$section_missing"
|
|
2290
|
+
fi
|
|
2291
|
+
|
|
2292
|
+
# Scan for project-only items (exist in .claude/ but NOT in manifest)
|
|
2293
|
+
local section_project_only=""
|
|
2294
|
+
local count_project_only=0
|
|
2295
|
+
if [ -d "$target_dir/.claude/agents" ]; then
|
|
2296
|
+
for f in "$target_dir/.claude/agents"/*.md; do
|
|
2297
|
+
[ ! -f "$f" ] && continue
|
|
2298
|
+
[ -L "$f" ] && continue
|
|
2299
|
+
local bname
|
|
2300
|
+
bname=$(basename "$f")
|
|
2301
|
+
if ! grep -q "agents/$bname" "$MANIFEST" 2>/dev/null; then
|
|
2302
|
+
section_project_only="${section_project_only} ◆ agents/$bname\n"
|
|
2303
|
+
count_project_only=$((count_project_only + 1))
|
|
2304
|
+
fi
|
|
2305
|
+
done
|
|
2306
|
+
fi
|
|
2307
|
+
if [ -d "$target_dir/.claude/skills" ]; then
|
|
2308
|
+
for d in "$target_dir/.claude/skills"/*/; do
|
|
2309
|
+
[ ! -d "$d" ] && continue
|
|
2310
|
+
[ -L "${d%/}" ] && continue
|
|
2311
|
+
local bname
|
|
2312
|
+
bname=$(basename "$d")
|
|
2313
|
+
if ! grep -q "skills/$bname" "$MANIFEST" 2>/dev/null; then
|
|
2314
|
+
section_project_only="${section_project_only} ◆ skills/$bname\n"
|
|
2315
|
+
count_project_only=$((count_project_only + 1))
|
|
2316
|
+
fi
|
|
2317
|
+
done
|
|
2318
|
+
fi
|
|
2319
|
+
if [ -d "$target_dir/.claude/hooks" ]; then
|
|
2320
|
+
for f in "$target_dir/.claude/hooks"/*.sh; do
|
|
2321
|
+
[ ! -f "$f" ] && continue
|
|
2322
|
+
[ -L "$f" ] && continue
|
|
2323
|
+
local bname
|
|
2324
|
+
bname=$(basename "$f")
|
|
2325
|
+
if ! grep -q "hooks/$bname" "$MANIFEST" 2>/dev/null; then
|
|
2326
|
+
section_project_only="${section_project_only} ◆ hooks/$bname\n"
|
|
2327
|
+
count_project_only=$((count_project_only + 1))
|
|
2328
|
+
fi
|
|
2329
|
+
done
|
|
2330
|
+
fi
|
|
2331
|
+
|
|
2332
|
+
if [ $count_project_only -gt 0 ]; then
|
|
2333
|
+
echo -e " ${DIM}PROJECT-ONLY${NC} (no toolkit equivalent):"
|
|
2334
|
+
echo -e "$section_project_only"
|
|
2335
|
+
fi
|
|
2336
|
+
|
|
2337
|
+
# Summary box
|
|
2338
|
+
local _skip_count=$((count_customized + count_modified + count_project_only))
|
|
2339
|
+
echo ""
|
|
2340
|
+
echo "─── What --upgrade would do ────────────────────────────────"
|
|
2341
|
+
printf " Auto-convert: %3d (identical files → symlink, backed up)\n" "$count_identical"
|
|
2342
|
+
printf " Ask you first: %3d (stale files — you decide per file)\n" "$count_stale"
|
|
2343
|
+
printf " Create new: %3d (missing toolkit items)\n" "$count_missing"
|
|
2344
|
+
printf " Skip: %3d (modified + customized + project-only)\n" "$_skip_count"
|
|
2345
|
+
echo ""
|
|
2346
|
+
echo -e " Backups saved to: ${CYAN}.claude/$BACKUP_DIR_NAME/${NC}"
|
|
2347
|
+
echo " Each converted file gets a .pre-upgrade copy."
|
|
2348
|
+
echo "────────────────────────────────────────────────────────────"
|
|
2349
|
+
|
|
2350
|
+
# Config status
|
|
2351
|
+
if config_exists "$target_dir"; then
|
|
2352
|
+
load_config "$target_dir"
|
|
2353
|
+
echo -e " CONFIG: ${GREEN}found${NC} (categories: $CONF_INSTALLED_CATEGORIES)"
|
|
2354
|
+
else
|
|
2355
|
+
echo -e " CONFIG: ${YELLOW}⚠ No .claude-agents.conf found${NC}"
|
|
2356
|
+
local derived
|
|
2357
|
+
derived=$(generate_config_from_existing "$target_dir" "true")
|
|
2358
|
+
if [ -n "$derived" ]; then
|
|
2359
|
+
echo " → Will generate from existing files"
|
|
2360
|
+
echo " → Derived categories: $derived"
|
|
2361
|
+
fi
|
|
2362
|
+
fi
|
|
2363
|
+
|
|
2364
|
+
echo ""
|
|
2365
|
+
echo -e "${DIM}Run install.sh --upgrade to apply changes.${NC}"
|
|
2366
|
+
echo -e "${DIM} Add --yes to auto-approve all stale conversions (CI/non-interactive mode).${NC}"
|
|
2367
|
+
echo -e "${DIM} Add --skip file1,file2 to exclude specific items.${NC}"
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
# ============================================================================
|
|
2371
|
+
# Brownfield Upgrade (--upgrade)
|
|
2372
|
+
# ============================================================================
|
|
2373
|
+
|
|
2374
|
+
handle_upgrade() {
|
|
2375
|
+
local target_dir="$1"
|
|
2376
|
+
|
|
2377
|
+
echo -e "${BOLD}Claude Agents — Brownfield Upgrade${NC}"
|
|
2378
|
+
echo -e "Target: ${CYAN}$target_dir${NC}"
|
|
2379
|
+
echo ""
|
|
2380
|
+
|
|
2381
|
+
# Step 1: Assess current state
|
|
2382
|
+
echo -e "${BOLD}Step 1/4:${NC} Assessing current state..."
|
|
2383
|
+
|
|
2384
|
+
# Step 2: Generate config if missing
|
|
2385
|
+
echo -e "${BOLD}Step 2/4:${NC} Generating config..."
|
|
2386
|
+
if ! config_exists "$target_dir"; then
|
|
2387
|
+
local derived_categories
|
|
2388
|
+
derived_categories=$(generate_config_from_existing "$target_dir")
|
|
2389
|
+
echo -e " Derived categories: ${GREEN}$derived_categories${NC}"
|
|
2390
|
+
else
|
|
2391
|
+
load_config "$target_dir"
|
|
2392
|
+
echo -e " Using existing config: ${GREEN}$CONF_INSTALLED_CATEGORIES${NC}"
|
|
2393
|
+
fi
|
|
2394
|
+
echo ""
|
|
2395
|
+
|
|
2396
|
+
# Load config (either existing or newly generated)
|
|
2397
|
+
load_config "$target_dir"
|
|
2398
|
+
|
|
2399
|
+
local filter_items
|
|
2400
|
+
filter_items=$(items_for_categories "$CONF_INSTALLED_CATEGORIES")
|
|
2401
|
+
|
|
2402
|
+
# Ensure directories
|
|
2403
|
+
mkdir -p "$target_dir/.claude/agents"
|
|
2404
|
+
mkdir -p "$target_dir/.claude/skills"
|
|
2405
|
+
mkdir -p "$target_dir/.claude/hooks"
|
|
2406
|
+
|
|
2407
|
+
# Snapshot backup
|
|
2408
|
+
snapshot_pre_install "$target_dir"
|
|
2409
|
+
|
|
2410
|
+
# Step 3: Converting files
|
|
2411
|
+
echo -e "${BOLD}Step 3/4:${NC} Converting files..."
|
|
2412
|
+
echo ""
|
|
2413
|
+
|
|
2414
|
+
# Reset deferred stale array
|
|
2415
|
+
STALE_DEFERRED=()
|
|
2416
|
+
|
|
2417
|
+
# Process manifest with smart_sync (identical auto-converts, stale deferred)
|
|
2418
|
+
process_manifest "$target_dir" "smart" "false" "$filter_items"
|
|
2419
|
+
|
|
2420
|
+
# Report auto-converted identical files
|
|
2421
|
+
if [ $CONVERTED -gt 0 ]; then
|
|
2422
|
+
echo -e " ${CYAN}IDENTICAL files${NC} (auto-converted — content is the same):"
|
|
2423
|
+
echo -e " ✓ Converted $CONVERTED identical files to symlinks"
|
|
2424
|
+
echo ""
|
|
2425
|
+
fi
|
|
2426
|
+
|
|
2427
|
+
# Handle stale items interactively (or auto with --yes)
|
|
2428
|
+
if [ ${#STALE_DEFERRED[@]} -gt 0 ]; then
|
|
2429
|
+
echo -e " ${YELLOW}STALE files${NC} (asking per file):"
|
|
2430
|
+
echo ""
|
|
2431
|
+
for entry in "${STALE_DEFERRED[@]}"; do
|
|
2432
|
+
IFS='|' read -r src tgt name <<< "$entry"
|
|
2433
|
+
# Check --skip list
|
|
2434
|
+
if [ -n "$SKIP_ITEMS" ] && echo ",$SKIP_ITEMS," | grep -q ",$name,"; then
|
|
2435
|
+
echo -e " ${DIM}Skipped${NC} $name (--skip)"
|
|
2436
|
+
SKIPPED=$((SKIPPED + 1))
|
|
2437
|
+
continue
|
|
2438
|
+
fi
|
|
2439
|
+
prompt_stale_conversion "$src" "$tgt" "$name"
|
|
2440
|
+
done
|
|
2441
|
+
echo ""
|
|
2442
|
+
fi
|
|
2443
|
+
|
|
2444
|
+
# Report created items
|
|
2445
|
+
if [ $CREATED -gt 0 ]; then
|
|
2446
|
+
echo -e " Creating missing toolkit items..."
|
|
2447
|
+
echo -e " ✓ Created $CREATED new symlinks"
|
|
2448
|
+
echo ""
|
|
2449
|
+
fi
|
|
2450
|
+
|
|
2451
|
+
# Merge settings.json hooks
|
|
2452
|
+
local hooks_to_add
|
|
2453
|
+
hooks_to_add=$(hooks_for_categories "$CONF_INSTALLED_CATEGORIES")
|
|
2454
|
+
if [ -n "$hooks_to_add" ]; then
|
|
2455
|
+
merge_settings_hooks "$target_dir/.claude/settings.json" "add" "$hooks_to_add"
|
|
2456
|
+
fi
|
|
2457
|
+
|
|
2458
|
+
# Update .gitignore
|
|
2459
|
+
update_gitignore_block "$target_dir" "$filter_items"
|
|
2460
|
+
|
|
2461
|
+
# Update CLAUDE.md managed block (ask — offer to inject if not present)
|
|
2462
|
+
update_claudemd_block "$target_dir" "ask"
|
|
2463
|
+
|
|
2464
|
+
# Make hooks executable
|
|
2465
|
+
for hook in "$target_dir/.claude/hooks/"*.sh; do
|
|
2466
|
+
if [ -f "$hook" ] || [ -L "$hook" ]; then
|
|
2467
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
2468
|
+
fi
|
|
2469
|
+
done
|
|
2470
|
+
|
|
2471
|
+
# Post-install record
|
|
2472
|
+
post_install_record "$target_dir"
|
|
2473
|
+
|
|
2474
|
+
# Step 4: Summary
|
|
2475
|
+
echo ""
|
|
2476
|
+
echo -e "${BOLD}Step 4/4: Done!${NC}"
|
|
2477
|
+
echo -e " Converted: ${GREEN}$CONVERTED${NC} (identical → symlink)"
|
|
2478
|
+
echo -e " Upgraded: ${GREEN}$UPGRADED${NC} (stale → symlink, user approved)"
|
|
2479
|
+
echo -e " Kept: ${CYAN}$KEPT${NC} (stale → user chose to keep)"
|
|
2480
|
+
echo -e " Created: ${GREEN}$CREATED${NC} (new)"
|
|
2481
|
+
echo -e " Skipped: ${YELLOW}$SKIPPED${NC} (modified/customized)"
|
|
2482
|
+
echo -e " Updated: ${GREEN}$UPDATED${NC}"
|
|
2483
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
2484
|
+
echo -e " Errors: ${RED}$ERRORS${NC}"
|
|
2485
|
+
fi
|
|
2486
|
+
echo ""
|
|
2487
|
+
echo -e " Backups at: ${CYAN}.claude/$BACKUP_DIR_NAME/${NC}"
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
# ============================================================================
|
|
2491
|
+
# Generate Config (--generate-config)
|
|
2492
|
+
# ============================================================================
|
|
2493
|
+
|
|
2494
|
+
handle_generate_config() {
|
|
2495
|
+
local target_dir="$1"
|
|
2496
|
+
|
|
2497
|
+
if config_exists "$target_dir"; then
|
|
2498
|
+
echo -e "${YELLOW}Config already exists at$(config_path "$target_dir")${NC}"
|
|
2499
|
+
echo "Use --setup to change selections."
|
|
2500
|
+
return 0
|
|
2501
|
+
fi
|
|
2502
|
+
|
|
2503
|
+
local derived
|
|
2504
|
+
derived=$(generate_config_from_existing "$target_dir")
|
|
2505
|
+
echo -e "${GREEN}Generated config${NC} with categories: $derived"
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
# ============================================================================
|
|
2509
|
+
# Main
|
|
2510
|
+
# ============================================================================
|
|
2511
|
+
|
|
2512
|
+
TARGET_DIR=""
|
|
2513
|
+
MODE="sync" # sync | setup | uninstall | sync-from-config | init | convert | status | assess | upgrade | generate-config | store-key | check-license
|
|
2514
|
+
INSTALL_RAILWAY=false
|
|
2515
|
+
LICENSE_KEY=""
|
|
2516
|
+
CATEGORIES_ARG=""
|
|
2517
|
+
JSON_OUTPUT=false
|
|
2518
|
+
|
|
2519
|
+
# Parse arguments
|
|
2520
|
+
while [ $# -gt 0 ]; do
|
|
2521
|
+
case $1 in
|
|
2522
|
+
--setup)
|
|
2523
|
+
MODE="setup"
|
|
2524
|
+
shift
|
|
2525
|
+
;;
|
|
2526
|
+
--uninstall)
|
|
2527
|
+
MODE="uninstall"
|
|
2528
|
+
shift
|
|
2529
|
+
;;
|
|
2530
|
+
--sync-from-config)
|
|
2531
|
+
MODE="sync-from-config"
|
|
2532
|
+
shift
|
|
2533
|
+
;;
|
|
2534
|
+
--init)
|
|
2535
|
+
MODE="init"
|
|
2536
|
+
shift
|
|
2537
|
+
;;
|
|
2538
|
+
--convert)
|
|
2539
|
+
MODE="convert"
|
|
2540
|
+
shift
|
|
2541
|
+
;;
|
|
2542
|
+
--assess)
|
|
2543
|
+
MODE="assess"
|
|
2544
|
+
shift
|
|
2545
|
+
;;
|
|
2546
|
+
--upgrade)
|
|
2547
|
+
MODE="upgrade"
|
|
2548
|
+
shift
|
|
2549
|
+
;;
|
|
2550
|
+
--skip)
|
|
2551
|
+
SKIP_ITEMS="${2:-}"
|
|
2552
|
+
shift
|
|
2553
|
+
[ $# -gt 0 ] && shift
|
|
2554
|
+
;;
|
|
2555
|
+
--yes|-y)
|
|
2556
|
+
FORCE_YES=true
|
|
2557
|
+
shift
|
|
2558
|
+
;;
|
|
2559
|
+
--generate-config)
|
|
2560
|
+
MODE="generate-config"
|
|
2561
|
+
shift
|
|
2562
|
+
;;
|
|
2563
|
+
--status)
|
|
2564
|
+
MODE="status"
|
|
2565
|
+
shift
|
|
2566
|
+
;;
|
|
2567
|
+
--key)
|
|
2568
|
+
MODE="store-key"
|
|
2569
|
+
LICENSE_KEY="${2:-}"
|
|
2570
|
+
shift
|
|
2571
|
+
[ $# -gt 0 ] && shift
|
|
2572
|
+
;;
|
|
2573
|
+
--railway)
|
|
2574
|
+
INSTALL_RAILWAY=true
|
|
2575
|
+
shift
|
|
2576
|
+
;;
|
|
2577
|
+
--check-license-only)
|
|
2578
|
+
MODE="check-license"
|
|
2579
|
+
shift
|
|
2580
|
+
;;
|
|
2581
|
+
--force-remove-hooks)
|
|
2582
|
+
MODE="force-remove-hooks"
|
|
2583
|
+
FORCE_HOOKS_TARGET="${2:-}"
|
|
2584
|
+
FORCE_HOOKS_LIST="${3:-}"
|
|
2585
|
+
shift
|
|
2586
|
+
[ $# -gt 0 ] && shift
|
|
2587
|
+
[ $# -gt 0 ] && shift
|
|
2588
|
+
;;
|
|
2589
|
+
--revoke)
|
|
2590
|
+
MODE="revoke"
|
|
2591
|
+
REVOKE_LABEL="${2:-}"
|
|
2592
|
+
shift
|
|
2593
|
+
[ $# -gt 0 ] && shift
|
|
2594
|
+
;;
|
|
2595
|
+
--add-user)
|
|
2596
|
+
MODE="add-user"
|
|
2597
|
+
ADD_USER_LABEL="${2:-}"
|
|
2598
|
+
shift
|
|
2599
|
+
[ $# -gt 0 ] && shift
|
|
2600
|
+
;;
|
|
2601
|
+
--list-users)
|
|
2602
|
+
MODE="list-users"
|
|
2603
|
+
shift
|
|
2604
|
+
;;
|
|
2605
|
+
--categories)
|
|
2606
|
+
CATEGORIES_ARG="${2:-}"
|
|
2607
|
+
shift
|
|
2608
|
+
[ $# -gt 0 ] && shift
|
|
2609
|
+
;;
|
|
2610
|
+
--bundle)
|
|
2611
|
+
# Install a named bundle (e.g., forge, spark, prime)
|
|
2612
|
+
# Delegates to bin/cli.js install for compiled plugin mode.
|
|
2613
|
+
BUNDLE_NAME="${2:-}"
|
|
2614
|
+
if [ -z "$BUNDLE_NAME" ]; then
|
|
2615
|
+
echo "Error: --bundle requires a bundle name" >&2
|
|
2616
|
+
echo "Available bundles: forge spark scalpel sentinel prism canvas compass counsel shield cruise prime" >&2
|
|
2617
|
+
exit 1
|
|
2618
|
+
fi
|
|
2619
|
+
shift
|
|
2620
|
+
[ $# -gt 0 ] && shift
|
|
2621
|
+
# Determine target path (next arg if present and not a flag)
|
|
2622
|
+
BUNDLE_TARGET="${1:-}"
|
|
2623
|
+
if [[ -n "$BUNDLE_TARGET" && ! "$BUNDLE_TARGET" =~ ^-- ]]; then
|
|
2624
|
+
shift
|
|
2625
|
+
else
|
|
2626
|
+
BUNDLE_TARGET="."
|
|
2627
|
+
fi
|
|
2628
|
+
BUNDLE_TARGET="$(cd "$BUNDLE_TARGET" && pwd)"
|
|
2629
|
+
CLI_JS="$SCRIPT_DIR/bin/cli.js"
|
|
2630
|
+
if command -v node &>/dev/null && [ -f "$CLI_JS" ]; then
|
|
2631
|
+
exec node "$CLI_JS" install "$BUNDLE_NAME" "$BUNDLE_TARGET"
|
|
2632
|
+
else
|
|
2633
|
+
echo "Error: node not found or bin/cli.js missing. Cannot install bundle." >&2
|
|
2634
|
+
echo "Ensure Node.js is installed and the toolkit is fully set up." >&2
|
|
2635
|
+
exit 1
|
|
2636
|
+
fi
|
|
2637
|
+
;;
|
|
2638
|
+
--json-output)
|
|
2639
|
+
JSON_OUTPUT=true
|
|
2640
|
+
shift
|
|
2641
|
+
;;
|
|
2642
|
+
--help|-h)
|
|
2643
|
+
echo -e "${BOLD}Claude Agents — Symlink Install System${NC}"
|
|
2644
|
+
echo ""
|
|
2645
|
+
echo "Usage:"
|
|
2646
|
+
echo " install.sh [path] Sync (auto-detects if setup needed)"
|
|
2647
|
+
echo " install.sh --setup [path] Interactive setup (pick categories)"
|
|
2648
|
+
echo " install.sh --uninstall [path] Clean uninstall (restore everything)"
|
|
2649
|
+
echo " install.sh --sync-from-config [path] Sync configured categories only"
|
|
2650
|
+
echo " install.sh --init [path] Scaffold new project + sync"
|
|
2651
|
+
echo " install.sh --convert [path] Replace matching copies with symlinks"
|
|
2652
|
+
echo " install.sh --assess [path] Brownfield assessment (read-only)"
|
|
2653
|
+
echo " install.sh --upgrade [path] Brownfield upgrade (converts stale/identical)"
|
|
2654
|
+
echo " --yes Auto-approve all stale conversions (CI mode)"
|
|
2655
|
+
echo " --skip file1,file2 Exclude specific items from conversion"
|
|
2656
|
+
echo " install.sh --generate-config [path] Generate config from existing files"
|
|
2657
|
+
echo " install.sh --status [path] Show what's linked/overridden/missing"
|
|
2658
|
+
echo " install.sh --key KEY Store your license key"
|
|
2659
|
+
echo " install.sh --railway Include Railway deployment skills"
|
|
2660
|
+
echo " install.sh --categories cat1,cat2 Install specific categories (non-interactive)"
|
|
2661
|
+
echo " install.sh --bundle NAME [path] Install a compiled plugin bundle (requires node)"
|
|
2662
|
+
echo " install.sh --json-output Output JSON summary on completion"
|
|
2663
|
+
echo ""
|
|
2664
|
+
echo "Admin commands (Arth employees only — requires repo write access):"
|
|
2665
|
+
echo " install.sh --add-user NAME Generate a license key for a new user"
|
|
2666
|
+
echo " install.sh --revoke NAME Revoke a user's license (removes on next session)"
|
|
2667
|
+
echo " install.sh --list-users List all licensed users"
|
|
2668
|
+
echo ""
|
|
2669
|
+
echo " install.sh --help Show this help"
|
|
2670
|
+
echo ""
|
|
2671
|
+
echo "Categories:"
|
|
2672
|
+
echo " core Sync skill, explore-light agent, auto-update hook (always included)"
|
|
2673
|
+
echo " strategy Strategy & Design agents (architect, PM, GTM, design-studio)"
|
|
2674
|
+
echo " development Development agents + planning, implement, PR, issue skills"
|
|
2675
|
+
echo " quality QA agents + QA skills"
|
|
2676
|
+
echo " operations SRE agent + operations skills"
|
|
2677
|
+
echo " hooks Triage router + git worktree sync"
|
|
2678
|
+
echo " guardrails Safety hooks + onboard, autopilot skills"
|
|
2679
|
+
echo " railway Railway deployment skills"
|
|
2680
|
+
echo " superpowers Superpowers + deep explore skills"
|
|
2681
|
+
echo ""
|
|
2682
|
+
echo "First install auto-launches interactive setup."
|
|
2683
|
+
echo "Subsequent runs do silent sync respecting your selections."
|
|
2684
|
+
echo ""
|
|
2685
|
+
echo "Symlink behavior (sync):"
|
|
2686
|
+
echo " Symlink exists → Update (managed by us)"
|
|
2687
|
+
echo " Regular file exists → Skip (your project override)"
|
|
2688
|
+
echo " Doesn't exist → Create symlink"
|
|
2689
|
+
exit 0
|
|
2690
|
+
;;
|
|
2691
|
+
*)
|
|
2692
|
+
TARGET_DIR="$1"
|
|
2693
|
+
shift
|
|
2694
|
+
;;
|
|
2695
|
+
esac
|
|
2696
|
+
done
|
|
2697
|
+
|
|
2698
|
+
# Handle --key mode (no license check needed)
|
|
2699
|
+
if [ "$MODE" = "store-key" ]; then
|
|
2700
|
+
if [ -z "$LICENSE_KEY" ]; then
|
|
2701
|
+
echo -e "${RED}Usage: $0 --key ARTH-XXXX-XXXX-XXXX-XXXX${NC}"
|
|
2702
|
+
exit 1
|
|
2703
|
+
fi
|
|
2704
|
+
echo "$LICENSE_KEY" > "$LICENSE_FILE"
|
|
2705
|
+
echo -e "${GREEN}License key stored.${NC}"
|
|
2706
|
+
|
|
2707
|
+
# Validate immediately
|
|
2708
|
+
if check_license 2>/dev/null; then
|
|
2709
|
+
echo -e "${GREEN}Key validated successfully.${NC}"
|
|
2710
|
+
fi
|
|
2711
|
+
exit 0
|
|
2712
|
+
fi
|
|
2713
|
+
|
|
2714
|
+
# Handle --check-license-only mode
|
|
2715
|
+
if [ "$MODE" = "check-license" ]; then
|
|
2716
|
+
check_license
|
|
2717
|
+
exit 0
|
|
2718
|
+
fi
|
|
2719
|
+
|
|
2720
|
+
# Handle --force-remove-hooks (used by sync-agents.sh during revocation)
|
|
2721
|
+
if [ "$MODE" = "force-remove-hooks" ]; then
|
|
2722
|
+
if [ -n "$FORCE_HOOKS_TARGET" ] && [ -f "$FORCE_HOOKS_TARGET/.claude/settings.json" ] && [ -n "$FORCE_HOOKS_LIST" ]; then
|
|
2723
|
+
merge_settings_hooks "$FORCE_HOOKS_TARGET/.claude/settings.json" "remove" "$FORCE_HOOKS_LIST"
|
|
2724
|
+
fi
|
|
2725
|
+
exit 0
|
|
2726
|
+
fi
|
|
2727
|
+
|
|
2728
|
+
# ============================================================================
|
|
2729
|
+
# Admin Commands (Arth employees only — requires repo write access)
|
|
2730
|
+
# ============================================================================
|
|
2731
|
+
|
|
2732
|
+
# Handle --list-users
|
|
2733
|
+
if [ "$MODE" = "list-users" ]; then
|
|
2734
|
+
if [ ! -f "$AUTHORIZED_KEYS" ]; then
|
|
2735
|
+
echo -e "${RED}authorized-keys.txt not found.${NC}"
|
|
2736
|
+
exit 1
|
|
2737
|
+
fi
|
|
2738
|
+
echo -e "${BOLD}Licensed Users${NC}"
|
|
2739
|
+
echo "---"
|
|
2740
|
+
current_label=""
|
|
2741
|
+
while IFS= read -r line; do
|
|
2742
|
+
# Skip empty lines
|
|
2743
|
+
[ -z "$line" ] && continue
|
|
2744
|
+
# Comment line = label
|
|
2745
|
+
if echo "$line" | grep -q '^#'; then
|
|
2746
|
+
current_label=$(echo "$line" | sed 's/^#[[:space:]]*//')
|
|
2747
|
+
continue
|
|
2748
|
+
fi
|
|
2749
|
+
# Hash line
|
|
2750
|
+
hash="$line"
|
|
2751
|
+
short_hash="${hash:0:12}..."
|
|
2752
|
+
if [ -n "$current_label" ]; then
|
|
2753
|
+
echo -e " ${CYAN}$current_label${NC} ($short_hash)"
|
|
2754
|
+
current_label=""
|
|
2755
|
+
else
|
|
2756
|
+
echo -e " ${DIM}(unlabeled)${NC} ($short_hash)"
|
|
2757
|
+
fi
|
|
2758
|
+
done < "$AUTHORIZED_KEYS"
|
|
2759
|
+
echo ""
|
|
2760
|
+
echo "Total: $(grep -v '^#' "$AUTHORIZED_KEYS" | grep -v '^$' | wc -l | tr -d ' ') key(s)"
|
|
2761
|
+
exit 0
|
|
2762
|
+
fi
|
|
2763
|
+
|
|
2764
|
+
# Handle --add-user LABEL
|
|
2765
|
+
if [ "$MODE" = "add-user" ]; then
|
|
2766
|
+
if [ -z "$ADD_USER_LABEL" ]; then
|
|
2767
|
+
echo -e "${RED}Usage: $0 --add-user USERNAME${NC}"
|
|
2768
|
+
echo " Example: $0 --add-user john"
|
|
2769
|
+
exit 1
|
|
2770
|
+
fi
|
|
2771
|
+
if [ ! -f "$AUTHORIZED_KEYS" ]; then
|
|
2772
|
+
echo -e "${RED}authorized-keys.txt not found.${NC}"
|
|
2773
|
+
exit 1
|
|
2774
|
+
fi
|
|
2775
|
+
|
|
2776
|
+
# Check for duplicate label
|
|
2777
|
+
if grep -q "^# $ADD_USER_LABEL$" "$AUTHORIZED_KEYS" 2>/dev/null; then
|
|
2778
|
+
echo -e "${RED}User '$ADD_USER_LABEL' already exists in authorized-keys.txt${NC}"
|
|
2779
|
+
exit 1
|
|
2780
|
+
fi
|
|
2781
|
+
|
|
2782
|
+
# Generate key: ARTH-XXXX-XXXX-XXXX-XXXX (uppercase alphanumeric)
|
|
2783
|
+
GEN_KEY="ARTH"
|
|
2784
|
+
for i in 1 2 3 4; do
|
|
2785
|
+
SEGMENT=$(openssl rand -hex 2 | tr '[:lower:]' '[:upper:]')
|
|
2786
|
+
GEN_KEY="${GEN_KEY}-${SEGMENT}"
|
|
2787
|
+
done
|
|
2788
|
+
|
|
2789
|
+
# Hash the key
|
|
2790
|
+
GEN_HASH=$(echo -n "$GEN_KEY" | shasum -a 256 | awk '{print $1}')
|
|
2791
|
+
|
|
2792
|
+
# Append to authorized-keys.txt
|
|
2793
|
+
echo "" >> "$AUTHORIZED_KEYS"
|
|
2794
|
+
echo "# $ADD_USER_LABEL" >> "$AUTHORIZED_KEYS"
|
|
2795
|
+
echo "$GEN_HASH" >> "$AUTHORIZED_KEYS"
|
|
2796
|
+
|
|
2797
|
+
echo -e "${BOLD}User added: ${CYAN}$ADD_USER_LABEL${NC}"
|
|
2798
|
+
echo ""
|
|
2799
|
+
echo -e "${BOLD}License key (give to user — store securely, shown only once):${NC}"
|
|
2800
|
+
echo ""
|
|
2801
|
+
echo -e " ${GREEN}$GEN_KEY${NC}"
|
|
2802
|
+
echo ""
|
|
2803
|
+
echo -e "Hash: ${DIM}$GEN_HASH${NC}"
|
|
2804
|
+
echo ""
|
|
2805
|
+
echo -e "${YELLOW}Next steps:${NC}"
|
|
2806
|
+
echo " 1. Give the key above to $ADD_USER_LABEL (securely — not in plain text chat)"
|
|
2807
|
+
echo " 2. Commit: git add authorized-keys.txt && git commit -m 'add license: $ADD_USER_LABEL'"
|
|
2808
|
+
echo " 3. Push: git push"
|
|
2809
|
+
echo ""
|
|
2810
|
+
echo " User installs with: install.sh --key $GEN_KEY"
|
|
2811
|
+
exit 0
|
|
2812
|
+
fi
|
|
2813
|
+
|
|
2814
|
+
# Handle --revoke LABEL
|
|
2815
|
+
if [ "$MODE" = "revoke" ]; then
|
|
2816
|
+
if [ -z "$REVOKE_LABEL" ]; then
|
|
2817
|
+
echo -e "${RED}Usage: $0 --revoke USERNAME${NC}"
|
|
2818
|
+
echo " Example: $0 --revoke john"
|
|
2819
|
+
echo ""
|
|
2820
|
+
echo " List users: $0 --list-users"
|
|
2821
|
+
exit 1
|
|
2822
|
+
fi
|
|
2823
|
+
if [ ! -f "$AUTHORIZED_KEYS" ]; then
|
|
2824
|
+
echo -e "${RED}authorized-keys.txt not found.${NC}"
|
|
2825
|
+
exit 1
|
|
2826
|
+
fi
|
|
2827
|
+
|
|
2828
|
+
# Find the label
|
|
2829
|
+
if ! grep -q "^# $REVOKE_LABEL$" "$AUTHORIZED_KEYS" 2>/dev/null; then
|
|
2830
|
+
echo -e "${RED}User '$REVOKE_LABEL' not found in authorized-keys.txt${NC}"
|
|
2831
|
+
echo ""
|
|
2832
|
+
echo "Available users:"
|
|
2833
|
+
# Show only single-word labels (user names) — lines like "# username" with no spaces in the name
|
|
2834
|
+
grep -E '^# [a-zA-Z0-9_-]+$' "$AUTHORIZED_KEYS" | sed 's/^# / /'
|
|
2835
|
+
exit 1
|
|
2836
|
+
fi
|
|
2837
|
+
|
|
2838
|
+
# Remove the label line and the hash line immediately after it
|
|
2839
|
+
tmp=$(mktemp)
|
|
2840
|
+
skip_next=false
|
|
2841
|
+
while IFS= read -r line; do
|
|
2842
|
+
if $skip_next; then
|
|
2843
|
+
# This is the hash line — skip it
|
|
2844
|
+
skip_next=false
|
|
2845
|
+
continue
|
|
2846
|
+
fi
|
|
2847
|
+
if [ "$line" = "# $REVOKE_LABEL" ]; then
|
|
2848
|
+
skip_next=true
|
|
2849
|
+
continue
|
|
2850
|
+
fi
|
|
2851
|
+
echo "$line" >> "$tmp"
|
|
2852
|
+
done < "$AUTHORIZED_KEYS"
|
|
2853
|
+
mv "$tmp" "$AUTHORIZED_KEYS"
|
|
2854
|
+
|
|
2855
|
+
echo -e "${BOLD}License revoked: ${RED}$REVOKE_LABEL${NC}"
|
|
2856
|
+
echo ""
|
|
2857
|
+
echo -e "${YELLOW}What happens next:${NC}"
|
|
2858
|
+
echo " 1. Commit & push this change to propagate revocation"
|
|
2859
|
+
echo " 2. On their next Claude Code session, sync-agents.sh will:"
|
|
2860
|
+
echo " a. Pull the updated authorized-keys.txt"
|
|
2861
|
+
echo " b. Detect the license is no longer valid"
|
|
2862
|
+
echo " c. Remove all toolkit symlinks from their project"
|
|
2863
|
+
echo " d. Remove hook entries from settings.json"
|
|
2864
|
+
echo " e. Remove the managed CLAUDE.md block"
|
|
2865
|
+
echo " f. Display: 'License revoked. Toolkit has been disabled.'"
|
|
2866
|
+
echo ""
|
|
2867
|
+
echo -e "${YELLOW}To complete revocation:${NC}"
|
|
2868
|
+
echo " git add authorized-keys.txt && git commit -m 'revoke license: $REVOKE_LABEL' && git push"
|
|
2869
|
+
exit 0
|
|
2870
|
+
fi
|
|
2871
|
+
|
|
2872
|
+
# --status and --assess work WITHOUT a license (users can inspect before buying)
|
|
2873
|
+
if [ "$MODE" = "status" ] || [ "$MODE" = "assess" ]; then
|
|
2874
|
+
if [ -z "$TARGET_DIR" ]; then
|
|
2875
|
+
TARGET_DIR="$(pwd)"
|
|
2876
|
+
fi
|
|
2877
|
+
TARGET_DIR="$(cd "$TARGET_DIR" 2>/dev/null && pwd)" || {
|
|
2878
|
+
echo -e "${RED}Error: $TARGET_DIR is not a valid directory${NC}"
|
|
2879
|
+
exit 1
|
|
2880
|
+
}
|
|
2881
|
+
if [ "$MODE" = "status" ]; then
|
|
2882
|
+
show_status "$TARGET_DIR" "$INSTALL_RAILWAY"
|
|
2883
|
+
else
|
|
2884
|
+
handle_assess "$TARGET_DIR"
|
|
2885
|
+
fi
|
|
2886
|
+
exit 0
|
|
2887
|
+
fi
|
|
2888
|
+
|
|
2889
|
+
# All other modes require a valid license
|
|
2890
|
+
check_license
|
|
2891
|
+
|
|
2892
|
+
# Default target to current directory
|
|
2893
|
+
if [ -z "$TARGET_DIR" ]; then
|
|
2894
|
+
TARGET_DIR="$(pwd)"
|
|
2895
|
+
fi
|
|
2896
|
+
|
|
2897
|
+
# Resolve to absolute path
|
|
2898
|
+
TARGET_DIR="$(cd "$TARGET_DIR" 2>/dev/null && pwd)" || {
|
|
2899
|
+
echo -e "${RED}Error: $TARGET_DIR is not a valid directory${NC}"
|
|
2900
|
+
exit 1
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
# Verify manifest exists
|
|
2904
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
2905
|
+
echo -e "${RED}Error: portable.manifest not found. Re-clone the repo.${NC}"
|
|
2906
|
+
exit 1
|
|
2907
|
+
fi
|
|
2908
|
+
|
|
2909
|
+
# ---- Mode dispatch ----
|
|
2910
|
+
|
|
2911
|
+
# Handle --uninstall
|
|
2912
|
+
if [ "$MODE" = "uninstall" ]; then
|
|
2913
|
+
handle_uninstall "$TARGET_DIR"
|
|
2914
|
+
exit 0
|
|
2915
|
+
fi
|
|
2916
|
+
|
|
2917
|
+
# Handle --setup (explicit interactive setup)
|
|
2918
|
+
if [ "$MODE" = "setup" ]; then
|
|
2919
|
+
handle_setup "$TARGET_DIR"
|
|
2920
|
+
exit 0
|
|
2921
|
+
fi
|
|
2922
|
+
|
|
2923
|
+
# Handle --upgrade (brownfield upgrade)
|
|
2924
|
+
if [ "$MODE" = "upgrade" ]; then
|
|
2925
|
+
handle_upgrade "$TARGET_DIR"
|
|
2926
|
+
exit 0
|
|
2927
|
+
fi
|
|
2928
|
+
|
|
2929
|
+
# Handle --generate-config (generate config from existing files)
|
|
2930
|
+
if [ "$MODE" = "generate-config" ]; then
|
|
2931
|
+
handle_generate_config "$TARGET_DIR"
|
|
2932
|
+
exit 0
|
|
2933
|
+
fi
|
|
2934
|
+
|
|
2935
|
+
# Handle --sync-from-config (used by sync hook)
|
|
2936
|
+
if [ "$MODE" = "sync-from-config" ]; then
|
|
2937
|
+
if handle_sync_from_config "$TARGET_DIR"; then
|
|
2938
|
+
exit 0
|
|
2939
|
+
fi
|
|
2940
|
+
# Falls through to full sync if no config found
|
|
2941
|
+
MODE="sync"
|
|
2942
|
+
fi
|
|
2943
|
+
|
|
2944
|
+
# Handle --init
|
|
2945
|
+
if [ "$MODE" = "init" ]; then
|
|
2946
|
+
echo -e "${BOLD}Claude Agents${NC} — Installing into: ${CYAN}$TARGET_DIR${NC}"
|
|
2947
|
+
echo ""
|
|
2948
|
+
scaffold_project "$TARGET_DIR"
|
|
2949
|
+
|
|
2950
|
+
# After scaffolding, run setup if interactive
|
|
2951
|
+
if [ -t 0 ]; then
|
|
2952
|
+
handle_setup "$TARGET_DIR"
|
|
2953
|
+
else
|
|
2954
|
+
# Non-interactive init — install everything
|
|
2955
|
+
mkdir -p "$TARGET_DIR/.claude/agents"
|
|
2956
|
+
mkdir -p "$TARGET_DIR/.claude/skills"
|
|
2957
|
+
mkdir -p "$TARGET_DIR/.claude/hooks"
|
|
2958
|
+
process_manifest "$TARGET_DIR" "safe" "$INSTALL_RAILWAY"
|
|
2959
|
+
|
|
2960
|
+
for hook in "$TARGET_DIR/.claude/hooks/"*.sh; do
|
|
2961
|
+
if [ -f "$hook" ] || [ -L "$hook" ]; then
|
|
2962
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
2963
|
+
fi
|
|
2964
|
+
done
|
|
2965
|
+
|
|
2966
|
+
echo ""
|
|
2967
|
+
echo -e "${BOLD}Done!${NC}"
|
|
2968
|
+
echo -e " Created: ${GREEN}$CREATED${NC}"
|
|
2969
|
+
echo -e " Updated: ${GREEN}$UPDATED${NC}"
|
|
2970
|
+
echo -e " Skipped: ${YELLOW}$SKIPPED${NC} (project overrides)"
|
|
2971
|
+
|
|
2972
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
2973
|
+
emit_json_summary "init" "$TARGET_DIR"
|
|
2974
|
+
elif [ -t 1 ]; then
|
|
2975
|
+
echo ""
|
|
2976
|
+
echo -e "${BOLD}Next steps:${NC}"
|
|
2977
|
+
echo -e " ${CYAN}cd $(basename "$TARGET_DIR") && claude${NC} Start Claude Code in this project"
|
|
2978
|
+
echo -e " ${CYAN}/calibrate${NC} Deep-learn your project (first session — run this once)"
|
|
2979
|
+
echo ""
|
|
2980
|
+
echo -e " ${DIM}/calibrate reads your source code to learn architecture patterns, coding"
|
|
2981
|
+
echo -e " conventions, and domain language. It then recommends and installs MCP servers,"
|
|
2982
|
+
echo -e " agents, skills, and workflows tailored to your project.${NC}"
|
|
2983
|
+
echo ""
|
|
2984
|
+
if command -v clade &>/dev/null; then
|
|
2985
|
+
echo -e " ${CYAN}clade sync${NC} Translate toolkit to Cursor/Codex/Kiro/Gemini"
|
|
2986
|
+
fi
|
|
2987
|
+
if command -v arth &>/dev/null; then
|
|
2988
|
+
echo -e " ${CYAN}arth setup .${NC} Full setup: toolkit + multi-engine + dashboard"
|
|
2989
|
+
fi
|
|
2990
|
+
echo -e " ${CYAN}install.sh --status .${NC} See what's linked/overridden"
|
|
2991
|
+
echo -e " ${CYAN}install.sh --setup .${NC} Change category selections"
|
|
2992
|
+
fi
|
|
2993
|
+
fi
|
|
2994
|
+
exit 0
|
|
2995
|
+
fi
|
|
2996
|
+
|
|
2997
|
+
# ---- Default sync mode ----
|
|
2998
|
+
# Smart first-run detection:
|
|
2999
|
+
# config exists → sync from config (silent)
|
|
3000
|
+
# --categories provided → use those directly (non-interactive)
|
|
3001
|
+
# no .claude/ AND no config → greenfield, run init flow (interactive)
|
|
3002
|
+
# .claude/ exists, no config → suggest --assess (interactive)
|
|
3003
|
+
# interactive + no config → launch setup
|
|
3004
|
+
|
|
3005
|
+
if config_exists "$TARGET_DIR"; then
|
|
3006
|
+
# Config exists — sync from config silently
|
|
3007
|
+
if handle_sync_from_config "$TARGET_DIR"; then
|
|
3008
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
3009
|
+
emit_json_summary "sync-from-config" "$TARGET_DIR"
|
|
3010
|
+
fi
|
|
3011
|
+
exit 0
|
|
3012
|
+
fi
|
|
3013
|
+
fi
|
|
3014
|
+
|
|
3015
|
+
# --categories provided → use those directly (non-interactive)
|
|
3016
|
+
if [ -n "$CATEGORIES_ARG" ]; then
|
|
3017
|
+
cats="${CATEGORIES_ARG//,/ }"
|
|
3018
|
+
# Validate categories
|
|
3019
|
+
for cat in $cats; do
|
|
3020
|
+
label=$(get_category_label "$cat")
|
|
3021
|
+
if [ -z "$label" ]; then
|
|
3022
|
+
echo -e "${RED}Unknown category: $cat${NC}" >&2
|
|
3023
|
+
echo "Valid categories: core, strategy, development, quality, operations, hooks, guardrails, railway, superpowers" >&2
|
|
3024
|
+
echo "" >&2
|
|
3025
|
+
echo "Example: install.sh --categories strategy,development,railway ." >&2
|
|
3026
|
+
exit 1
|
|
3027
|
+
fi
|
|
3028
|
+
done
|
|
3029
|
+
# Ensure core is always included
|
|
3030
|
+
if ! echo " $cats " | grep -q " core "; then
|
|
3031
|
+
cats="core $cats"
|
|
3032
|
+
fi
|
|
3033
|
+
echo -e "${BOLD}Claude Agents${NC} — Installing into: ${CYAN}$TARGET_DIR${NC}"
|
|
3034
|
+
echo -e "Categories: ${GREEN}$cats${NC}"
|
|
3035
|
+
echo ""
|
|
3036
|
+
mkdir -p "$TARGET_DIR/.claude/agents"
|
|
3037
|
+
mkdir -p "$TARGET_DIR/.claude/skills"
|
|
3038
|
+
mkdir -p "$TARGET_DIR/.claude/hooks"
|
|
3039
|
+
snapshot_pre_install "$TARGET_DIR"
|
|
3040
|
+
write_config "$TARGET_DIR" "$cats"
|
|
3041
|
+
handle_sync_from_config "$TARGET_DIR"
|
|
3042
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
3043
|
+
emit_json_summary "categories" "$TARGET_DIR"
|
|
3044
|
+
elif [ -t 1 ]; then
|
|
3045
|
+
echo ""
|
|
3046
|
+
echo -e "${DIM}Change selections: ${CYAN}install.sh --setup .${NC}${DIM} | See status: ${CYAN}install.sh --status .${NC}"
|
|
3047
|
+
fi
|
|
3048
|
+
exit 0
|
|
3049
|
+
fi
|
|
3050
|
+
|
|
3051
|
+
if [ ! -d "$TARGET_DIR/.claude" ] && [ ! -f "$(config_path "$TARGET_DIR")" ]; then
|
|
3052
|
+
# No .claude/ AND no config → greenfield, run init flow
|
|
3053
|
+
if [ -t 0 ]; then
|
|
3054
|
+
echo -e "${BOLD}Claude Agents${NC} — New project detected: ${CYAN}$TARGET_DIR${NC}"
|
|
3055
|
+
echo ""
|
|
3056
|
+
scaffold_project "$TARGET_DIR"
|
|
3057
|
+
handle_setup "$TARGET_DIR"
|
|
3058
|
+
exit 0
|
|
3059
|
+
fi
|
|
3060
|
+
fi
|
|
3061
|
+
|
|
3062
|
+
if [ -d "$TARGET_DIR/.claude" ] && [ ! -f "$(config_path "$TARGET_DIR")" ]; then
|
|
3063
|
+
# .claude/ exists but no config → suggest --assess
|
|
3064
|
+
if [ -t 0 ]; then
|
|
3065
|
+
echo -e "${YELLOW}Existing .claude/ directory found but no claude-agents config.${NC}"
|
|
3066
|
+
echo ""
|
|
3067
|
+
echo "This project may have manually-created .claude/ files."
|
|
3068
|
+
echo "Options:"
|
|
3069
|
+
echo " install.sh --setup . Interactive setup — pick categories to install"
|
|
3070
|
+
echo " install.sh --categories cat1,cat2 . Non-interactive install of specific categories"
|
|
3071
|
+
echo " install.sh --assess . See what you have vs the toolkit"
|
|
3072
|
+
echo " install.sh --generate-config . Generate config from existing files"
|
|
3073
|
+
echo ""
|
|
3074
|
+
echo "Available categories: core (always), strategy, development, quality,"
|
|
3075
|
+
echo " operations, hooks, guardrails, railway, superpowers"
|
|
3076
|
+
exit 0
|
|
3077
|
+
fi
|
|
3078
|
+
fi
|
|
3079
|
+
|
|
3080
|
+
if [ -t 0 ] && [ ! -f "$(config_path "$TARGET_DIR")" ]; then
|
|
3081
|
+
# Interactive + no config → launch setup
|
|
3082
|
+
handle_setup "$TARGET_DIR"
|
|
3083
|
+
exit 0
|
|
3084
|
+
fi
|
|
3085
|
+
|
|
3086
|
+
# Fallback: no config, non-interactive — install defaults with category awareness
|
|
3087
|
+
# Use default categories (all on except railway, superpowers)
|
|
3088
|
+
FALLBACK_CATEGORIES="core strategy development quality operations hooks guardrails"
|
|
3089
|
+
if [ "$INSTALL_RAILWAY" = "true" ]; then
|
|
3090
|
+
FALLBACK_CATEGORIES="$FALLBACK_CATEGORIES railway"
|
|
3091
|
+
fi
|
|
3092
|
+
|
|
3093
|
+
echo -e "${BOLD}Claude Agents${NC} — Installing into: ${CYAN}$TARGET_DIR${NC}"
|
|
3094
|
+
echo ""
|
|
3095
|
+
echo -e "${DIM}Installing default categories: $FALLBACK_CATEGORIES${NC}"
|
|
3096
|
+
echo -e "${DIM}Tip: Use ${CYAN}--setup${DIM} for interactive selection or ${CYAN}--categories${DIM} for specific categories${NC}"
|
|
3097
|
+
echo ""
|
|
3098
|
+
|
|
3099
|
+
# Build filter list from fallback categories
|
|
3100
|
+
FALLBACK_ITEMS=$(items_for_categories "$FALLBACK_CATEGORIES")
|
|
3101
|
+
|
|
3102
|
+
# Ensure base directories exist
|
|
3103
|
+
mkdir -p "$TARGET_DIR/.claude/agents"
|
|
3104
|
+
mkdir -p "$TARGET_DIR/.claude/skills"
|
|
3105
|
+
mkdir -p "$TARGET_DIR/.claude/hooks"
|
|
3106
|
+
|
|
3107
|
+
if [ "$MODE" = "convert" ]; then
|
|
3108
|
+
echo -e "Mode: ${YELLOW}convert${NC} (replacing matching copies with symlinks)"
|
|
3109
|
+
process_manifest "$TARGET_DIR" "convert" "false" "$FALLBACK_ITEMS"
|
|
3110
|
+
else
|
|
3111
|
+
echo -e "Mode: ${GREEN}sync${NC} (safe symlinks — won't touch regular files)"
|
|
3112
|
+
process_manifest "$TARGET_DIR" "safe" "false" "$FALLBACK_ITEMS"
|
|
3113
|
+
fi
|
|
3114
|
+
|
|
3115
|
+
# Write config so subsequent runs use sync-from-config
|
|
3116
|
+
write_config "$TARGET_DIR" "$FALLBACK_CATEGORIES"
|
|
3117
|
+
|
|
3118
|
+
# Make hooks executable
|
|
3119
|
+
for hook in "$TARGET_DIR/.claude/hooks/"*.sh; do
|
|
3120
|
+
if [ -f "$hook" ] || [ -L "$hook" ]; then
|
|
3121
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
3122
|
+
fi
|
|
3123
|
+
done
|
|
3124
|
+
|
|
3125
|
+
# Summary
|
|
3126
|
+
echo ""
|
|
3127
|
+
echo -e "${BOLD}Done!${NC}"
|
|
3128
|
+
echo -e " Created: ${GREEN}$CREATED${NC}"
|
|
3129
|
+
echo -e " Updated: ${GREEN}$UPDATED${NC}"
|
|
3130
|
+
if [ "$MODE" = "convert" ]; then
|
|
3131
|
+
echo -e " Converted: ${GREEN}$CONVERTED${NC}"
|
|
3132
|
+
fi
|
|
3133
|
+
echo -e " Skipped: ${YELLOW}$SKIPPED${NC} (project overrides)"
|
|
3134
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
3135
|
+
echo -e " Errors: ${RED}$ERRORS${NC}"
|
|
3136
|
+
fi
|
|
3137
|
+
|
|
3138
|
+
# Next steps (only in interactive mode, not with --json-output)
|
|
3139
|
+
if [ "$JSON_OUTPUT" != "true" ] && [ -t 1 ]; then
|
|
3140
|
+
echo ""
|
|
3141
|
+
# Check if project has been calibrated
|
|
3142
|
+
local has_profile=false
|
|
3143
|
+
[ -f "$TARGET_DIR/.claude/project-profile.md" ] && has_profile=true
|
|
3144
|
+
|
|
3145
|
+
if ! $has_profile; then
|
|
3146
|
+
echo -e "${BOLD}First time? Calibrate the toolkit to your project:${NC}"
|
|
3147
|
+
echo -e " ${CYAN}claude${NC} Start Claude Code"
|
|
3148
|
+
echo -e " ${CYAN}/calibrate${NC} Deep-learn your project (run once in Claude Code)"
|
|
3149
|
+
echo ""
|
|
3150
|
+
echo -e " ${DIM}/calibrate reads source code to learn your architecture, conventions, and domain."
|
|
3151
|
+
echo -e " It recommends and installs MCP servers, agents, skills, and workflows.${NC}"
|
|
3152
|
+
echo ""
|
|
3153
|
+
fi
|
|
3154
|
+
|
|
3155
|
+
echo -e "${BOLD}Customize your install:${NC}"
|
|
3156
|
+
echo -e " ${CYAN}install.sh --setup .${NC} Interactive category picker"
|
|
3157
|
+
echo -e " ${CYAN}install.sh --categories railway,superpowers .${NC}"
|
|
3158
|
+
echo -e " Add optional categories (non-interactive)"
|
|
3159
|
+
echo ""
|
|
3160
|
+
echo -e "${BOLD}Available categories:${NC}"
|
|
3161
|
+
echo -e " ${GREEN}Installed:${NC} core, strategy, development, quality, operations, hooks, guardrails"
|
|
3162
|
+
echo -e " ${YELLOW}Optional:${NC} railway (Railway deployment skills), superpowers (advanced skills)"
|
|
3163
|
+
echo ""
|
|
3164
|
+
echo -e "${BOLD}Other commands:${NC}"
|
|
3165
|
+
echo -e " ${CYAN}claude${NC} Start Claude Code in this project"
|
|
3166
|
+
|
|
3167
|
+
if command -v clade &>/dev/null; then
|
|
3168
|
+
echo -e " ${CYAN}clade sync${NC} Translate toolkit to Cursor/Codex/Kiro/Gemini"
|
|
3169
|
+
fi
|
|
3170
|
+
|
|
3171
|
+
if command -v arth &>/dev/null; then
|
|
3172
|
+
echo -e " ${CYAN}arth setup .${NC} Full setup: toolkit + multi-engine + dashboard"
|
|
3173
|
+
fi
|
|
3174
|
+
|
|
3175
|
+
echo -e " ${CYAN}install.sh --status .${NC} See what's linked/overridden"
|
|
3176
|
+
|
|
3177
|
+
if $has_profile; then
|
|
3178
|
+
echo -e " ${CYAN}/calibrate rescan${NC} Re-scan project after major changes"
|
|
3179
|
+
fi
|
|
3180
|
+
fi
|
|
3181
|
+
|
|
3182
|
+
# JSON summary (last line, parseable by callers)
|
|
3183
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
3184
|
+
emit_json_summary "sync" "$TARGET_DIR"
|
|
3185
|
+
fi
|