@kensaurus/skills 0.0.0-stage → 2.1.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/.claude-plugin/marketplace.json +53 -0
- package/.claude-plugin/plugin.json +40 -0
- package/.cursor-plugin/plugin.json +38 -0
- package/.mcp.json +28 -0
- package/CHANGELOG.md +1790 -0
- package/LICENSE +21 -0
- package/LICENSE-APACHE +62 -0
- package/NOTICE +13 -0
- package/README.md +821 -2
- package/SECURITY.md +55 -0
- package/agents/code-reviewer.md +60 -0
- package/agents/completion-judge.md +89 -0
- package/agents/db-migrator.md +125 -0
- package/agents/debugger.md +47 -0
- package/agents/deploy-checker.md +100 -0
- package/agents/perf-monitor.md +74 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-light.png +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +6 -0
- package/assets/og.png +0 -0
- package/bin/install.mjs +1127 -0
- package/bin/kenji.js +2 -0
- package/commands/adr.md +17 -0
- package/commands/aeo-plan.md +18 -0
- package/commands/arch-boundaries.md +17 -0
- package/commands/aso-plan.md +18 -0
- package/commands/auth-flows.md +19 -0
- package/commands/backup-plan.md +17 -0
- package/commands/burndown-full.md +25 -0
- package/commands/capacitor-plan.md +18 -0
- package/commands/codemod-safety.md +22 -0
- package/commands/commit.md +18 -0
- package/commands/complete-everything.md +40 -0
- package/commands/cost-plan.md +19 -0
- package/commands/deadcode-plan.md +26 -0
- package/commands/deadcode.md +32 -0
- package/commands/debug-issue.md +17 -0
- package/commands/deps-plan.md +18 -0
- package/commands/docs-plan.md +17 -0
- package/commands/doctrine.md +19 -0
- package/commands/error-plan.md +19 -0
- package/commands/feedback-to-closure.md +36 -0
- package/commands/fix-issue.md +76 -0
- package/commands/gate-logic.md +26 -0
- package/commands/green-repo.md +36 -0
- package/commands/grill-me.md +19 -0
- package/commands/gtm-plan.md +21 -0
- package/commands/gtm-weekly.md +17 -0
- package/commands/gtm.md +22 -0
- package/commands/handoff.md +15 -0
- package/commands/housekeep-backlog.md +18 -0
- package/commands/housekeep-files.md +22 -0
- package/commands/housekeep-gates.md +18 -0
- package/commands/instant-nav.md +11 -0
- package/commands/integrity-plan.md +19 -0
- package/commands/launch-kit.md +16 -0
- package/commands/mcp-guide.md +40 -0
- package/commands/mobile-plan.md +19 -0
- package/commands/native-rn-monorepo/README.md +78 -0
- package/commands/native-rn-monorepo/android-build.md +26 -0
- package/commands/native-rn-monorepo/android-install.md +32 -0
- package/commands/native-rn-monorepo/android-logcat.md +37 -0
- package/commands/native-rn-monorepo/ios-ci-logs.md +56 -0
- package/commands/native-rn-monorepo/ios-ci-status.md +52 -0
- package/commands/native-rn-monorepo/ios-ci-trigger.md +59 -0
- package/commands/native-rn-monorepo/rn-reset.md +50 -0
- package/commands/native-rn-monorepo/rn-ship-ios.md +67 -0
- package/commands/native-rn-monorepo/rn-verify.md +53 -0
- package/commands/perf-plan.md +18 -0
- package/commands/plan-mode.md +74 -0
- package/commands/pr.md +16 -0
- package/commands/pricing-plan.md +20 -0
- package/commands/privacy-plan.md +18 -0
- package/commands/readability.md +12 -0
- package/commands/readme.md +15 -0
- package/commands/refactor.md +15 -0
- package/commands/release-prep.md +17 -0
- package/commands/research.md +25 -0
- package/commands/responsive-audit.md +21 -0
- package/commands/review-code.md +18 -0
- package/commands/rls-plan.md +18 -0
- package/commands/secrets-plan.md +18 -0
- package/commands/security-plan.md +19 -0
- package/commands/ship-and-observe.md +36 -0
- package/commands/skill-conflicts.md +19 -0
- package/commands/slop-plan.md +18 -0
- package/commands/stub-plan.md +18 -0
- package/commands/test-mutation.md +16 -0
- package/commands/test-plan.md +17 -0
- package/commands/test.md +29 -0
- package/commands/thirdparty-web-interface-guidelines.md +185 -0
- package/commands/uiux-plan.md +18 -0
- package/commands/uiux.md +45 -0
- package/commands/update-deps.md +21 -0
- package/commands/validation-plan.md +19 -0
- package/commands-portable/fix-issue.md +72 -0
- package/commands-portable/plan-mode.md +92 -0
- package/commands-portable/research.md +91 -0
- package/docs/screenshots/README.md +5 -0
- package/docs/screenshots/audit-dark.png +0 -0
- package/docs/screenshots/build-dark.png +0 -0
- package/docs/screenshots/grill-dark.png +0 -0
- package/docs/screenshots/hero-dark.png +0 -0
- package/docs/screenshots/hero-light.png +0 -0
- package/docs/screenshots/ship-dark.png +0 -0
- package/docs/screenshots/src/showcase.html +320 -0
- package/hooks/completion-gate.mjs +258 -0
- package/hooks/cursor-hooks.json +13 -0
- package/hooks/hooks.json +15 -0
- package/install.sh +21 -0
- package/llms.txt +40 -0
- package/mcp/README.md +266 -0
- package/mcp/VERSIONS.md +41 -0
- package/mcp/mcp-full.json.template +124 -0
- package/mcp/mcp.json.template +29 -0
- package/mcp/pinned-versions.json +27 -0
- package/package.json +94 -4
- package/rules/approved-plan-execution.mdc +65 -0
- package/rules/full-stack-ship-discipline.mdc +37 -0
- package/rules/native-rn-monorepo/README.md +63 -0
- package/rules/native-rn-monorepo/_project.mdc +69 -0
- package/rules/native-rn-monorepo/native-android.mdc +72 -0
- package/rules/native-rn-monorepo/native-ios.mdc +61 -0
- package/rules/native-rn-monorepo/react-native-js.mdc +78 -0
- package/rules/native-rn-monorepo/web.mdc +60 -0
- package/rules/project-starter/components.mdc +54 -0
- package/rules/project-starter/data-fetching.mdc +77 -0
- package/rules/project-starter/git.mdc +41 -0
- package/rules/project-starter/supabase.mdc +37 -0
- package/rules/project-starter/tailwind.mdc +48 -0
- package/rules/project-starter/typescript.mdc +36 -0
- package/rules/project-starter/web-performance.mdc +42 -0
- package/rules/senior-engineer.mdc +30 -0
- package/rules/shell-first-search.mdc +19 -0
- package/rules/skill-workflows.mdc +35 -0
- package/rules/verification-before-completion.mdc +57 -0
- package/skills/audit-accessibility/SKILL.md +441 -0
- package/skills/audit-agent-speed/SKILL.md +181 -0
- package/skills/audit-agent-speed/scripts/stop-typecheck.mjs +151 -0
- package/skills/audit-analytics/SKILL.md +138 -0
- package/skills/audit-auth-flows/SKILL.md +267 -0
- package/skills/audit-backend-architecture/SKILL.md +266 -0
- package/skills/audit-backend-architecture/references/patterns.md +386 -0
- package/skills/audit-bundle-size/SKILL.md +295 -0
- package/skills/audit-cicd/SKILL.md +218 -0
- package/skills/audit-code-quality/SKILL.md +314 -0
- package/skills/audit-code-review/SKILL.md +289 -0
- package/skills/audit-codemod-safety/SKILL.md +159 -0
- package/skills/audit-db-schema/SKILL.md +465 -0
- package/skills/audit-db-schema/references/details.md +110 -0
- package/skills/audit-doctrine/SKILL.md +189 -0
- package/skills/audit-env-parity/SKILL.md +133 -0
- package/skills/audit-fe-api/SKILL.md +458 -0
- package/skills/audit-gate-logic/SKILL.md +219 -0
- package/skills/audit-i18n/SKILL.md +339 -0
- package/skills/audit-infra-cost/SKILL.md +142 -0
- package/skills/audit-langfuse-llm/SKILL.md +468 -0
- package/skills/audit-langfuse-llm/references/details.md +226 -0
- package/skills/audit-llm-security/SKILL.md +147 -0
- package/skills/audit-monetization-iap/SKILL.md +137 -0
- package/skills/audit-payment-system/SKILL.md +268 -0
- package/skills/audit-payment-system/references/checklist.md +283 -0
- package/skills/audit-performance/SKILL.md +383 -0
- package/skills/audit-performance/references/loading-priority-2026.md +81 -0
- package/skills/audit-realworld/SKILL.md +287 -0
- package/skills/audit-registry-listing/SKILL.md +122 -0
- package/skills/audit-resilience/SKILL.md +153 -0
- package/skills/audit-responsive/SKILL.md +221 -0
- package/skills/audit-responsive/references/checklist.md +166 -0
- package/skills/audit-security/SKILL.md +289 -0
- package/skills/audit-skill-conflicts/SKILL.md +178 -0
- package/skills/audit-ui-states/SKILL.md +146 -0
- package/skills/audit-uiux-design-system/SKILL.md +475 -0
- package/skills/audit-uiux-design-system/references/details.md +71 -0
- package/skills/audit-ux/SKILL.md +381 -0
- package/skills/audit-ux/references/details.md +245 -0
- package/skills/audit-ux-journeys/SKILL.md +215 -0
- package/skills/audit-ux-journeys/references/checklist.md +179 -0
- package/skills/backend-db-performance/SKILL.md +441 -0
- package/skills/backend-error-handling/SKILL.md +489 -0
- package/skills/backend-error-handling/references/details.md +58 -0
- package/skills/backend-observability/SKILL.md +88 -0
- package/skills/backend-patterns/SKILL.md +499 -0
- package/skills/backend-patterns/references/architecture-patterns.md +298 -0
- package/skills/backend-realtime/SKILL.md +403 -0
- package/skills/backend-realtime/references/patterns.md +74 -0
- package/skills/burndown-full/SKILL.md +174 -0
- package/skills/complete-everything/SKILL.md +295 -0
- package/skills/data-pipeline/SKILL.md +109 -0
- package/skills/data-visualization/SKILL.md +488 -0
- package/skills/debug-error/SKILL.md +322 -0
- package/skills/debug-fe-be-integration/SKILL.md +459 -0
- package/skills/debug-sentry-monitor/SKILL.md +497 -0
- package/skills/debug-sentry-monitor/references/details.md +165 -0
- package/skills/deploy-npm/SKILL.md +394 -0
- package/skills/deploy-npm/references/example-mushi-mushi.md +52 -0
- package/skills/deploy-verify/SKILL.md +489 -0
- package/skills/design-api/SKILL.md +379 -0
- package/skills/design-canvas/SKILL.md +155 -0
- package/skills/design-email/SKILL.md +370 -0
- package/skills/design-frontend/SKILL.md +143 -0
- package/skills/design-generative-art/SKILL.md +474 -0
- package/skills/design-mobile-first/SKILL.md +506 -0
- package/skills/design-motion/SKILL.md +333 -0
- package/skills/design-motion/references/delight-interactions.md +191 -0
- package/skills/design-prd/SKILL.md +443 -0
- package/skills/design-system/SKILL.md +457 -0
- package/skills/design-theme/SKILL.md +226 -0
- package/skills/design-theme/themes/tsumagoi-ranch.md +150 -0
- package/skills/docs-adr/SKILL.md +168 -0
- package/skills/docs-coauthor/SKILL.md +368 -0
- package/skills/docs-comparison-pages/SKILL.md +117 -0
- package/skills/docs-domain-modeling/SKILL.md +97 -0
- package/skills/docs-launch-kit/SKILL.md +139 -0
- package/skills/docs-writer/SKILL.md +469 -0
- package/skills/enhance-agent-guardrails/SKILL.md +164 -0
- package/skills/enhance-arch-boundaries/SKILL.md +154 -0
- package/skills/enhance-capacitor-ui/SKILL.md +463 -0
- package/skills/enhance-capacitor-ui/references/details.md +750 -0
- package/skills/enhance-email-deliverability/SKILL.md +143 -0
- package/skills/enhance-growth-loops/SKILL.md +122 -0
- package/skills/enhance-lifecycle-email/SKILL.md +130 -0
- package/skills/enhance-motion/SKILL.md +193 -0
- package/skills/enhance-onboarding/SKILL.md +148 -0
- package/skills/enhance-pwa/SKILL.md +303 -0
- package/skills/enhance-readability/SKILL.md +146 -0
- package/skills/enhance-readme/SKILL.md +496 -0
- package/skills/enhance-readme/package-lock.json +187 -0
- package/skills/enhance-readme/package.json +17 -0
- package/skills/enhance-readme/scripts/generate-readme-blocks.mjs +199 -0
- package/skills/enhance-readme/scripts/record-readme-tour.mjs +442 -0
- package/skills/enhance-skill-prompts/SKILL.md +167 -0
- package/skills/enhance-skill-prompts/references/exemplar-audit-auth-flows.md +311 -0
- package/skills/enhance-ux-laws/SKILL.md +409 -0
- package/skills/enhance-web-conversion/SKILL.md +155 -0
- package/skills/enhance-web-forms/SKILL.md +154 -0
- package/skills/enhance-web-instant-nav/SKILL.md +138 -0
- package/skills/enhance-web-instant-nav/references/bfcache-blockers.md +23 -0
- package/skills/enhance-web-instant-nav/references/early-hints.md +33 -0
- package/skills/enhance-web-instant-nav/references/speculation-rules.md +44 -0
- package/skills/enhance-web-landing/SKILL.md +459 -0
- package/skills/enhance-web-landing/references/details.md +773 -0
- package/skills/enhance-web-redesign/SKILL.md +228 -0
- package/skills/enhance-web-seo/SKILL.md +276 -0
- package/skills/enhance-web-ui/SKILL.md +473 -0
- package/skills/enhance-web-ui/references/details.md +674 -0
- package/skills/enhance-web-ux/HEURISTICS.md +248 -0
- package/skills/enhance-web-ux/PATTERNS.md +375 -0
- package/skills/enhance-web-ux/SKILL.md +464 -0
- package/skills/enhance-web-ux/examples.md +222 -0
- package/skills/enhance-web-ux/references/details.md +406 -0
- package/skills/enhance-web-web3d/SKILL.md +397 -0
- package/skills/enhance-web-web3d/references/css-canvas-effects.md +180 -0
- package/skills/handoff/SKILL.md +66 -0
- package/skills/housekeep-backlog/SKILL.md +149 -0
- package/skills/housekeep-dead-code/SKILL.md +387 -0
- package/skills/housekeep-dead-code/references/ratchet-ci.md +205 -0
- package/skills/housekeep-dead-code/references/supabase-hygiene.md +152 -0
- package/skills/housekeep-design/SKILL.md +207 -0
- package/skills/housekeep-files/SKILL.md +220 -0
- package/skills/housekeep-files/references/naming-and-catalog.md +86 -0
- package/skills/housekeep-files/scripts/housekeep-files.ps1 +360 -0
- package/skills/housekeep-files/scripts/housekeep-files.sh +238 -0
- package/skills/housekeep-gates/SKILL.md +174 -0
- package/skills/iterate-agent-harness/SKILL.md +137 -0
- package/skills/iterate-gtm-weekly/SKILL.md +103 -0
- package/skills/iterate-post-launch/SKILL.md +292 -0
- package/skills/meta-mcp-builder/SKILL.md +313 -0
- package/skills/meta-skill-creator/SKILL.md +304 -0
- package/skills/mobile-capacitor-platform/SKILL.md +103 -0
- package/skills/mobile-emulator-start/SKILL.md +296 -0
- package/skills/mobile-emulator-test/SKILL.md +491 -0
- package/skills/mobile-emulator-test/references/details.md +478 -0
- package/skills/mobile-rn-performance/SKILL.md +107 -0
- package/skills/mobile-rn-screen/SKILL.md +476 -0
- package/skills/mobile-rn-screen/references/details.md +785 -0
- package/skills/mushi-health/SKILL.md +206 -0
- package/skills/mushi-integration/SKILL.md +257 -0
- package/skills/plan-aeo-readiness/SKILL.md +166 -0
- package/skills/plan-antislop/SKILL.md +281 -0
- package/skills/plan-aso/SKILL.md +149 -0
- package/skills/plan-backup-dr/SKILL.md +131 -0
- package/skills/plan-capacitor-hardening/SKILL.md +217 -0
- package/skills/plan-data-integrity/SKILL.md +187 -0
- package/skills/plan-dead-code/SKILL.md +386 -0
- package/skills/plan-dead-code/references/knip-config.md +214 -0
- package/skills/plan-dead-code/references/output-templates.md +133 -0
- package/skills/plan-dead-code/references/preservation-contract.md +50 -0
- package/skills/plan-dead-code/references/residue-greps.md +84 -0
- package/skills/plan-dependency-provenance/SKILL.md +200 -0
- package/skills/plan-docs-sync/SKILL.md +143 -0
- package/skills/plan-docs-sync/references/drift-taxonomy.md +43 -0
- package/skills/plan-docs-sync/references/output-templates.md +33 -0
- package/skills/plan-docs-sync/references/preservation-contract.md +17 -0
- package/skills/plan-error-handling/SKILL.md +205 -0
- package/skills/plan-gtm/SKILL.md +276 -0
- package/skills/plan-gtm/references/benchmarks-2026.md +183 -0
- package/skills/plan-input-validation/SKILL.md +179 -0
- package/skills/plan-llm-cost-guardrails/SKILL.md +176 -0
- package/skills/plan-mobile-readiness/SKILL.md +171 -0
- package/skills/plan-perf-audit/SKILL.md +145 -0
- package/skills/plan-perf-audit/references/audit-scope.md +51 -0
- package/skills/plan-perf-audit/references/output-templates.md +33 -0
- package/skills/plan-perf-audit/references/preservation-contract.md +13 -0
- package/skills/plan-pricing/SKILL.md +173 -0
- package/skills/plan-privacy-compliance/SKILL.md +148 -0
- package/skills/plan-rls-audit/SKILL.md +231 -0
- package/skills/plan-secrets-audit/SKILL.md +181 -0
- package/skills/plan-security-audit/SKILL.md +168 -0
- package/skills/plan-security-audit/references/output-templates.md +36 -0
- package/skills/plan-security-audit/references/owasp-supabase-scope.md +55 -0
- package/skills/plan-security-audit/references/preservation-contract.md +18 -0
- package/skills/plan-stub-checker/SKILL.md +216 -0
- package/skills/plan-stub-checker/references/detection-methodology.md +75 -0
- package/skills/plan-stub-checker/references/detection-taxonomy.md +34 -0
- package/skills/plan-stub-checker/references/output-templates.md +63 -0
- package/skills/plan-stub-checker/references/preservation-contract.md +24 -0
- package/skills/plan-test-coverage/SKILL.md +170 -0
- package/skills/plan-test-coverage/references/methodology.md +54 -0
- package/skills/plan-test-coverage/references/output-templates.md +34 -0
- package/skills/plan-test-coverage/references/preservation-contract.md +15 -0
- package/skills/plan-uiux-unification/SKILL.md +230 -0
- package/skills/plan-uiux-unification/references/output-templates.md +67 -0
- package/skills/plan-uiux-unification/references/phase-workbook.md +85 -0
- package/skills/plan-uiux-unification/references/preservation-contract.md +24 -0
- package/skills/protocol-browser-anti-stall/SKILL.md +211 -0
- package/skills/protocol-browser-anti-stall/references/mcp-to-cli-map.md +113 -0
- package/skills/protocol-browser-anti-stall/references/playwright-session-coordination.md +170 -0
- package/skills/research/SKILL.md +422 -0
- package/skills/test-exploratory/SKILL.md +165 -0
- package/skills/test-exploratory/references/charter-template.md +29 -0
- package/skills/test-load/SKILL.md +126 -0
- package/skills/test-mutation/SKILL.md +160 -0
- package/skills/test-playwright/SKILL.md +354 -0
- package/skills/test-qa/SKILL.md +364 -0
- package/skills/test-qa/references/details.md +268 -0
- package/skills/test-red-team/SKILL.md +386 -0
- package/skills/test-red-team/references/owasp-attack-checklist.md +193 -0
- package/skills/test-unit/SKILL.md +259 -0
- package/skills/test-unit/references/details.md +267 -0
- package/skills/test-visual-regression/SKILL.md +132 -0
- package/skills/thirdparty-emil-design-eng/ATTRIBUTION.md +20 -0
- package/skills/thirdparty-emil-design-eng/SKILL.md +21 -0
- package/skills/thirdparty-emil-design-eng/references/emil-design-eng.md +676 -0
- package/skills/thirdparty-ui-ux-pro-max/ATTRIBUTION.md +22 -0
- package/skills/thirdparty-ui-ux-pro-max/SKILL.md +304 -0
- package/skills/thirdparty-ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/thirdparty-ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/thirdparty-ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/thirdparty-ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/thirdparty-ui-ux-pro-max/data/products.csv +97 -0
- package/skills/thirdparty-ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/thirdparty-ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/thirdparty-ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/thirdparty-ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/thirdparty-ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/thirdparty-ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/thirdparty-ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/thirdparty-ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/thirdparty-web-interface-guidelines/ATTRIBUTION.md +23 -0
- package/skills/thirdparty-web-interface-guidelines/SKILL.md +190 -0
- package/skills/workflow-build-feature/SKILL.md +118 -0
- package/skills/workflow-coding-discipline/SKILL.md +140 -0
- package/skills/workflow-environment-ready/SKILL.md +128 -0
- package/skills/workflow-feature-flag/SKILL.md +262 -0
- package/skills/workflow-feedback-to-closure/SKILL.md +165 -0
- package/skills/workflow-fix-and-ship/SKILL.md +136 -0
- package/skills/workflow-git-commit/SKILL.md +200 -0
- package/skills/workflow-green-repo/SKILL.md +166 -0
- package/skills/workflow-grilling/SKILL.md +73 -0
- package/skills/workflow-gtm/SKILL.md +153 -0
- package/skills/workflow-housekeep/SKILL.md +453 -0
- package/skills/workflow-housekeep/references/templates.md +109 -0
- package/skills/workflow-launch-ready/SKILL.md +145 -0
- package/skills/workflow-merge-conflicts/SKILL.md +62 -0
- package/skills/workflow-onboard/SKILL.md +99 -0
- package/skills/workflow-parallel-agents/SKILL.md +164 -0
- package/skills/workflow-pr/SKILL.md +197 -0
- package/skills/workflow-quality-gate/SKILL.md +147 -0
- package/skills/workflow-refactor/SKILL.md +274 -0
- package/skills/workflow-release-prep/SKILL.md +207 -0
- package/skills/workflow-ship-and-observe/SKILL.md +164 -0
- package/skills/workflow-spec-tdd/SKILL.md +141 -0
- package/skills/workflow-spec-tdd/references/spec-template.md +126 -0
- package/skills/workflow-spec-tdd/references/tdd-patterns.md +167 -0
- package/skills-cursor/babysit/SKILL.md +17 -0
- package/skills-cursor/canvas/SKILL.md +142 -0
- package/skills-cursor/canvas/sdk/canvas-tokens.d.ts +235 -0
- package/skills-cursor/canvas/sdk/chart-primitives.d.ts +200 -0
- package/skills-cursor/canvas/sdk/dag-layout.d.ts +102 -0
- package/skills-cursor/canvas/sdk/diff-view.d.ts +130 -0
- package/skills-cursor/canvas/sdk/form-primitives.d.ts +194 -0
- package/skills-cursor/canvas/sdk/hooks.d.ts +117 -0
- package/skills-cursor/canvas/sdk/index.d.ts +47 -0
- package/skills-cursor/canvas/sdk/theme.d.ts +61 -0
- package/skills-cursor/canvas/sdk/todo-list.d.ts +49 -0
- package/skills-cursor/canvas/sdk/ui-primitives.d.ts +549 -0
- package/skills-cursor/canvas/sdk/ui-primitives.test.d.ts +2 -0
- package/skills-cursor/create-hook/SKILL.md +238 -0
- package/skills-cursor/create-rule/SKILL.md +185 -0
- package/skills-cursor/create-skill/SKILL.md +269 -0
- package/skills-cursor/create-skill/references/authoring-guide.md +182 -0
- package/skills-cursor/create-subagent/SKILL.md +228 -0
- package/skills-cursor/migrate-to-skills/SKILL.md +121 -0
- package/skills-cursor/shell/SKILL.md +22 -0
- package/skills-cursor/split-to-prs/SKILL.md +47 -0
- package/skills-cursor/statusline/SKILL.md +193 -0
- package/skills-cursor/update-cli-config/SKILL.md +85 -0
- package/skills-cursor/update-cursor-settings/SKILL.md +137 -0
- package/skills.sh.json +297 -0
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kenji installer
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @kensaurus/skills Merge-install into ~/.cursor/ AND ~/.agents/skills/
|
|
7
|
+
* npx @kensaurus/skills --auto Detect installed tools and install to each
|
|
8
|
+
* npx @kensaurus/skills --claude Install for Claude Code (~/.claude/) instead
|
|
9
|
+
* npx @kensaurus/skills --codex Install for Codex CLI (~/.codex/AGENTS.md + prompts)
|
|
10
|
+
* npx @kensaurus/skills --gemini Install for Gemini CLI (~/.gemini/GEMINI.md + commands)
|
|
11
|
+
* npx @kensaurus/skills --all Install for all four supported tools
|
|
12
|
+
* npx @kensaurus/skills --clean Mirror: make target paths match this repo exactly
|
|
13
|
+
* npx @kensaurus/skills --only skills Install only some groups (csv)
|
|
14
|
+
* npx @kensaurus/skills --skill audit-ux Install a single skill
|
|
15
|
+
* npx @kensaurus/skills --link Dev mode: symlink instead of copy
|
|
16
|
+
* npx @kensaurus/skills --restore [stamp] Restore a previous --clean backup
|
|
17
|
+
* npx @kensaurus/skills --dry-run Preview without changing anything
|
|
18
|
+
* npx @kensaurus/skills --verify Hash-check dests against this package (no writes)
|
|
19
|
+
* npx @kensaurus/skills --help
|
|
20
|
+
*
|
|
21
|
+
* Why two Cursor paths?
|
|
22
|
+
* ~/.cursor/skills/ — read by the Cursor agent at runtime
|
|
23
|
+
* ~/.agents/skills/ — historically indexed by the Cursor Skills UI panel
|
|
24
|
+
* Default still mirrors both. Pass --no-agents-mirror to skip the UI copy.
|
|
25
|
+
* skills-cursor names that Cursor already ships as managed builtins are not
|
|
26
|
+
* copied into ~/.cursor/skills (Claude still gets the portable copies).
|
|
27
|
+
* RENAMED_SKILLS prunes old skill directories after a rename so existing
|
|
28
|
+
* installs do not keep two near-identical copies. Honors --dry-run; skipped
|
|
29
|
+
* when --skill is scoped. RENAMED_RULES does the same for renamed rule files.
|
|
30
|
+
*
|
|
31
|
+
* Claude Code paths:
|
|
32
|
+
* ~/.claude/skills/ — global skills, appear as /slash-commands
|
|
33
|
+
* ~/.claude/commands/ — custom slash commands
|
|
34
|
+
* ~/.claude/agents/ — subagent definitions
|
|
35
|
+
* ~/.claude/rules/ — rules (.mdc sources installed as .md)
|
|
36
|
+
* ~/.claude/settings.json — merged completion Stop hook (same script as Cursor)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
existsSync, mkdirSync, cpSync, rmSync, symlinkSync, readdirSync, statSync,
|
|
41
|
+
readFileSync, writeFileSync,
|
|
42
|
+
} from 'node:fs';
|
|
43
|
+
import { createHash } from 'node:crypto';
|
|
44
|
+
import { join, resolve, dirname } from 'node:path';
|
|
45
|
+
import { homedir, platform } from 'node:os';
|
|
46
|
+
import { fileURLToPath } from 'node:url';
|
|
47
|
+
|
|
48
|
+
const __dir = fileURLToPath(new URL('..', import.meta.url));
|
|
49
|
+
const argv = process.argv.slice(2);
|
|
50
|
+
|
|
51
|
+
// ---- tiny arg parser (supports `--flag`, `--key value`, `--key=value`) ----
|
|
52
|
+
const flags = new Set();
|
|
53
|
+
const opts = {};
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const a = argv[i];
|
|
56
|
+
if (!a.startsWith('--')) continue;
|
|
57
|
+
const [k, inlineVal] = a.slice(2).split('=');
|
|
58
|
+
if (inlineVal !== undefined) { opts[k] = inlineVal; continue; }
|
|
59
|
+
const next = argv[i + 1];
|
|
60
|
+
if (next && !next.startsWith('--')) { opts[k] = next; i++; } else { flags.add(k); }
|
|
61
|
+
}
|
|
62
|
+
const has = (...names) => names.some((n) => flags.has(n) || n in opts);
|
|
63
|
+
|
|
64
|
+
if (has('help', 'h')) {
|
|
65
|
+
console.log(`
|
|
66
|
+
kenji installer
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npx @kensaurus/skills Merge-install into ~/.cursor/ + ~/.agents/skills/
|
|
70
|
+
npx @kensaurus/skills --auto Detect installed tools and install to each
|
|
71
|
+
npx @kensaurus/skills --claude Install for Claude Code (~/.claude/) instead
|
|
72
|
+
npx @kensaurus/skills --codex Install for Codex CLI (~/.codex/AGENTS.md)
|
|
73
|
+
npx @kensaurus/skills --gemini Install for Gemini CLI (~/.gemini/GEMINI.md)
|
|
74
|
+
npx @kensaurus/skills --all Install for all four supported tools
|
|
75
|
+
npx @kensaurus/skills --clean Mirror: wipe and rebuild target paths from this repo
|
|
76
|
+
npx @kensaurus/skills --only skills Install only some groups (skills,commands,agents,rules,hooks)
|
|
77
|
+
npx @kensaurus/skills --skill <name> Install one skill by name
|
|
78
|
+
npx @kensaurus/skills --link Dev mode: symlink repo into ~/.cursor (live edits)
|
|
79
|
+
npx @kensaurus/skills --restore [stamp] Restore a previous --clean backup (latest if omitted)
|
|
80
|
+
npx @kensaurus/skills --dry-run Preview without changing anything
|
|
81
|
+
npx @kensaurus/skills --verify Hash-check dests against this package (no writes)
|
|
82
|
+
|
|
83
|
+
Flags:
|
|
84
|
+
--auto Detect installed tools (~/.cursor, ~/.claude, ~/.codex, ~/.gemini) and
|
|
85
|
+
install to each; falls back to Cursor if none are found.
|
|
86
|
+
--cursor Target Cursor explicitly (same as the bare default).
|
|
87
|
+
--claude Target Claude Code (~/.claude/) instead of Cursor.
|
|
88
|
+
--codex Target Codex CLI — merged rules → ~/.codex/AGENTS.md + prompt ports.
|
|
89
|
+
--gemini Target Gemini CLI — merged rules → ~/.gemini/GEMINI.md + command ports.
|
|
90
|
+
--all Target all four supported tools in one run.
|
|
91
|
+
--clean, --mirror Wipe managed dirs first so target paths exactly mirror this repo.
|
|
92
|
+
--no-backup Skip the timestamped backup taken before a --clean wipe.
|
|
93
|
+
--only <csv> Limit to a subset of: skills, commands, agents, rules, hooks.
|
|
94
|
+
--skill <name> Install a single skill (implies --only skills).
|
|
95
|
+
--link Symlink (junction on Windows) instead of copying — for repo dev.
|
|
96
|
+
--restore [stamp] Copy a backup under <target>/.kenji-backups/ back into place.
|
|
97
|
+
--typecheck-hook [script]
|
|
98
|
+
Run from a repo root: add a background typecheck Stop hook
|
|
99
|
+
(asyncRewake) to ./.claude/settings.json. Script defaults to
|
|
100
|
+
typecheck or type-check. See skill audit-agent-speed.
|
|
101
|
+
--dry-run Show what would happen; make no changes.
|
|
102
|
+
--verify Read-only: fail if any packaged file is missing or
|
|
103
|
+
hash-mismatched at the destination (merge-compatible:
|
|
104
|
+
extra personal files are allowed).
|
|
105
|
+
--quiet Suppress install logs (errors still print).
|
|
106
|
+
--no-agents-mirror Skip copying skills to ~/.agents/skills.
|
|
107
|
+
|
|
108
|
+
What gets installed (Cursor):
|
|
109
|
+
~/.cursor/skills/ ← agent skills at runtime (skills/ + non-builtin skills-cursor/)
|
|
110
|
+
~/.agents/skills/ ← optional Skills UI mirror (same content; skip with --no-agents-mirror)
|
|
111
|
+
~/.cursor/commands/ ← slash commands
|
|
112
|
+
~/.cursor/agents/ ← subagent definitions
|
|
113
|
+
~/.cursor/rules/ ← project rules starter pack
|
|
114
|
+
~/.cursor/hooks.json ← merged completion stop hook (preserves existing hooks)
|
|
115
|
+
~/.cursor/mcp.json ← MCP server template (only if missing; never overwritten)
|
|
116
|
+
|
|
117
|
+
What gets installed (Claude Code, with --claude or --all):
|
|
118
|
+
~/.claude/skills/ ← global skills (appear as /slash-commands)
|
|
119
|
+
~/.claude/commands/ ← custom slash commands
|
|
120
|
+
~/.claude/agents/ ← subagent definitions
|
|
121
|
+
~/.claude/rules/ ← rules (.mdc installed as .md)
|
|
122
|
+
~/.claude/settings.json ← merged completion Stop hook (preserves existing hooks)
|
|
123
|
+
|
|
124
|
+
What gets installed (Codex CLI / Gemini CLI — no skills system):
|
|
125
|
+
~/.codex/AGENTS.md ← rules merged into one context file (auto-loaded)
|
|
126
|
+
~/.codex/prompts/ ← portable commands as custom prompts (plan, research, fix-issue)
|
|
127
|
+
~/.gemini/GEMINI.md ← rules merged into one context file (auto-loaded)
|
|
128
|
+
~/.gemini/commands/ ← portable commands as TOML commands
|
|
129
|
+
These tools have no skills loader, so skills/agents are not written there.
|
|
130
|
+
Existing AGENTS.md/GEMINI.md is backed up (.bak-<stamp>) before an update.
|
|
131
|
+
`.trim());
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (has('quiet', 'q')) {
|
|
136
|
+
console.log = () => {};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isDryRun = has('dry-run');
|
|
140
|
+
const isVerifyOnly = has('verify');
|
|
141
|
+
const isClean = has('clean', 'mirror', 'force');
|
|
142
|
+
const noBackup = has('no-backup');
|
|
143
|
+
const useLink = has('link');
|
|
144
|
+
const noAgentsMirror = has('no-agents-mirror');
|
|
145
|
+
const verifyFailures = [];
|
|
146
|
+
|
|
147
|
+
/** Cursor already ships these as managed builtins. Do not copy into
|
|
148
|
+
* ~/.cursor/skills — a same-name user copy shadows the builtin and
|
|
149
|
+
* shows up twice in the `/` picker. Claude still gets portable copies. */
|
|
150
|
+
const CURSOR_MANAGED_BUILTIN_SKILLS = new Set([
|
|
151
|
+
'babysit',
|
|
152
|
+
'canvas',
|
|
153
|
+
'create-hook',
|
|
154
|
+
'create-rule',
|
|
155
|
+
'create-skill',
|
|
156
|
+
'create-subagent',
|
|
157
|
+
'migrate-to-skills',
|
|
158
|
+
'shell',
|
|
159
|
+
'split-to-prs',
|
|
160
|
+
'statusline',
|
|
161
|
+
'update-cli-config',
|
|
162
|
+
'update-cursor-settings',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
/** Old skill directory → new skill directory. Empty until a rename ships.
|
|
166
|
+
* Tests may inject pairs via KENJI_RENAMED_SKILLS=old:new,old2:new2. */
|
|
167
|
+
const RENAMED_SKILLS = {
|
|
168
|
+
'domain-modeling': 'docs-domain-modeling',
|
|
169
|
+
'grilling': 'workflow-grilling',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function renamedSkillMap() {
|
|
173
|
+
const map = { ...RENAMED_SKILLS };
|
|
174
|
+
const extra = process.env.KENJI_RENAMED_SKILLS;
|
|
175
|
+
if (!extra) return map;
|
|
176
|
+
for (const pair of extra.split(',')) {
|
|
177
|
+
const [oldName, newName] = pair.split(':').map((s) => s.trim());
|
|
178
|
+
if (oldName && newName) map[oldName] = newName;
|
|
179
|
+
}
|
|
180
|
+
return map;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldPruneRenamedSkills() {
|
|
184
|
+
if (skillName) return false;
|
|
185
|
+
if (onlyGroups && !onlyGroups.has('skills')) return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function pruneRenamedSkills(skillsDest, label) {
|
|
190
|
+
if (!shouldPruneRenamedSkills() || !skillsDest) return;
|
|
191
|
+
const map = renamedSkillMap();
|
|
192
|
+
for (const [oldName, newName] of Object.entries(map)) {
|
|
193
|
+
const stale = join(skillsDest, oldName);
|
|
194
|
+
if (!existsSync(stale)) continue;
|
|
195
|
+
if (isDryRun) {
|
|
196
|
+
console.log(` [dry-run] prune renamed ${label} skill ${oldName} → ${newName}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
rmSync(stale, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Old rule file → new rule file. Pruned in ~/.cursor/rules (.mdc) and
|
|
204
|
+
* ~/.claude/rules (.md) on merge installs, mirroring RENAMED_SKILLS. */
|
|
205
|
+
const RENAMED_RULES = {
|
|
206
|
+
'composer-2.5-execution.mdc': 'approved-plan-execution.mdc',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function pruneRenamedRules(rulesDest, label, { renameMdc = false } = {}) {
|
|
210
|
+
if (skillName || (onlyGroups && !onlyGroups.has('rules')) || !rulesDest) return;
|
|
211
|
+
for (const [oldName, newName] of Object.entries(RENAMED_RULES)) {
|
|
212
|
+
const file = renameMdc ? oldName.replace(/\.mdc$/, '.md') : oldName;
|
|
213
|
+
const stale = join(rulesDest, file);
|
|
214
|
+
if (!existsSync(stale)) continue;
|
|
215
|
+
if (isDryRun) { console.log(` [dry-run] prune renamed ${label} rule ${file} → ${newName}`); continue; }
|
|
216
|
+
rmSync(stale, { force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Old command file → new name. A stale copy is pruned only while it still
|
|
221
|
+
* carries a description this pack shipped, so a user's own file of the same
|
|
222
|
+
* name stays. */
|
|
223
|
+
const RENAMED_COMMANDS = {
|
|
224
|
+
'plan.md': {
|
|
225
|
+
to: 'plan-mode.md',
|
|
226
|
+
descriptions: [
|
|
227
|
+
'Research, clarify requirements, and produce an approved implementation plan before writing code (Cursor Plan Mode)',
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Merge installs never delete, so leftovers from older releases stay unless
|
|
233
|
+
// pruned: renamed commands, and per-project command bundles that older
|
|
234
|
+
// installers copied into the global commands dir (where they register as
|
|
235
|
+
// /<bundle>:<name> in every project). Only files this pack ships are removed.
|
|
236
|
+
function pruneStaleCommands(commandsDest, label) {
|
|
237
|
+
if (skillName || (onlyGroups && !onlyGroups.has('commands')) || !commandsDest || !existsSync(commandsDest)) return;
|
|
238
|
+
for (const [oldName, { to, descriptions }] of Object.entries(RENAMED_COMMANDS)) {
|
|
239
|
+
const stale = join(commandsDest, oldName);
|
|
240
|
+
if (!existsSync(stale)) continue;
|
|
241
|
+
const { description } = parseFrontmatter(readFileSync(stale, 'utf8'));
|
|
242
|
+
if (!descriptions.includes(description)) continue;
|
|
243
|
+
if (isDryRun) { console.log(` [dry-run] prune renamed ${label} command ${oldName} → ${to}`); continue; }
|
|
244
|
+
rmSync(stale, { force: true });
|
|
245
|
+
}
|
|
246
|
+
for (const bundle of PROJECT_RULE_BUNDLES) {
|
|
247
|
+
const src = resolve(__dir, 'commands', bundle);
|
|
248
|
+
const dest = join(commandsDest, bundle);
|
|
249
|
+
if (!existsSync(src) || !existsSync(dest) || !statSync(dest).isDirectory()) continue;
|
|
250
|
+
for (const f of readdirSync(src)) {
|
|
251
|
+
const p = join(dest, f);
|
|
252
|
+
if (!existsSync(p)) continue;
|
|
253
|
+
if (isDryRun) { console.log(` [dry-run] prune per-project ${label} command ${bundle}/${f}`); continue; }
|
|
254
|
+
rmSync(p, { force: true });
|
|
255
|
+
}
|
|
256
|
+
if (!isDryRun && readdirSync(dest).length === 0) rmSync(dest, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---- target resolution -----------------------------------------------------
|
|
261
|
+
// Backward compatible: a bare invocation still installs Cursor only.
|
|
262
|
+
// --auto detect installed tools (~/.cursor, ~/.claude, ~/.codex, ~/.gemini)
|
|
263
|
+
// and install to each; falls back to Cursor if none are found.
|
|
264
|
+
// --all force all four supported targets, installed or not.
|
|
265
|
+
// --cursor/--claude/--codex/--gemini explicit target(s); combine freely.
|
|
266
|
+
const codexBase = join(homedir(), '.codex'); // Codex CLI reads ~/.codex/AGENTS.md
|
|
267
|
+
const geminiBase = join(homedir(), '.gemini'); // Gemini CLI reads ~/.gemini/GEMINI.md
|
|
268
|
+
const cursorBase = join(homedir(), '.cursor');
|
|
269
|
+
const agentsBase = join(homedir(), '.agents'); // Cursor Skills UI reads ~/.agents/skills/
|
|
270
|
+
const claudeBase = join(homedir(), '.claude');
|
|
271
|
+
|
|
272
|
+
const explicitTargets = ['cursor', 'claude', 'codex', 'gemini'].filter((t) => has(t));
|
|
273
|
+
let targets;
|
|
274
|
+
if (has('all')) {
|
|
275
|
+
targets = new Set(['cursor', 'claude', 'codex', 'gemini']);
|
|
276
|
+
} else if (has('auto')) {
|
|
277
|
+
targets = detectTools();
|
|
278
|
+
if (targets.size === 0) targets = new Set(['cursor']);
|
|
279
|
+
} else if (explicitTargets.length) {
|
|
280
|
+
targets = new Set(explicitTargets);
|
|
281
|
+
} else {
|
|
282
|
+
targets = new Set(['cursor']); // backward-compatible default
|
|
283
|
+
}
|
|
284
|
+
const wantCursor = targets.has('cursor');
|
|
285
|
+
const wantClaude = targets.has('claude');
|
|
286
|
+
const wantCodex = targets.has('codex');
|
|
287
|
+
const wantGemini = targets.has('gemini');
|
|
288
|
+
|
|
289
|
+
if (has('auto')) {
|
|
290
|
+
console.log(`Auto-detected targets: ${[...targets].join(', ') || '(none — defaulting to Cursor)'}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ALL_DIRS = [
|
|
294
|
+
{ src: 'skills', dest: 'skills' },
|
|
295
|
+
{ src: 'skills-cursor', dest: 'skills' }, // merge cursor-specific skills
|
|
296
|
+
{ src: 'commands', dest: 'commands' },
|
|
297
|
+
{ src: 'agents', dest: 'agents' },
|
|
298
|
+
{ src: 'rules', dest: 'rules' },
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
// Per-project bundles: copied into a specific repo's .cursor/rules and
|
|
302
|
+
// .cursor/commands by the user (see each bundle's README), never into the
|
|
303
|
+
// global dirs — their always/glob rules would fire in every project, and a
|
|
304
|
+
// global commands copy registers the bundle's README.md as /<bundle>:README.
|
|
305
|
+
const PROJECT_RULE_BUNDLES = new Set(['native-rn-monorepo', 'project-starter']);
|
|
306
|
+
|
|
307
|
+
// Installed folder names. Releases before 2.0.0 shipped as cursor-kenji and
|
|
308
|
+
// used the legacy names; installs migrate them (ADR-0013).
|
|
309
|
+
const HOOK_DIR = 'kenji-hooks';
|
|
310
|
+
const LEGACY_HOOK_DIR = 'cursor-kenji-hooks';
|
|
311
|
+
const BACKUPS_DIR = '.kenji-backups';
|
|
312
|
+
const LEGACY_BACKUPS_DIR = '.cursor-kenji-backups';
|
|
313
|
+
// Matches the managed completion-gate entry under either folder name.
|
|
314
|
+
const MANAGED_GATE = /(?:cursor-)?kenji-hooks[\\/]completion-gate\.mjs/;
|
|
315
|
+
|
|
316
|
+
// ---- typecheck-hook mode: per-repo background typecheck (audit-agent-speed) --
|
|
317
|
+
// Writes into the current repo only, never a global config: a global Stop hook
|
|
318
|
+
// would run in every repo, including ones without the script.
|
|
319
|
+
if (has('typecheck-hook')) {
|
|
320
|
+
const repo = process.cwd();
|
|
321
|
+
const pkgPath = join(repo, 'package.json');
|
|
322
|
+
if (!existsSync(join(repo, '.git')) || !existsSync(pkgPath)) {
|
|
323
|
+
console.error('--typecheck-hook: run it from a repo root that has .git and package.json.');
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const scripts = JSON.parse(readFileSync(pkgPath, 'utf8')).scripts ?? {};
|
|
327
|
+
const script = typeof opts['typecheck-hook'] === 'string'
|
|
328
|
+
? opts['typecheck-hook']
|
|
329
|
+
: ['typecheck', 'type-check'].find((name) => name in scripts);
|
|
330
|
+
if (!script || !(script in scripts)) {
|
|
331
|
+
console.error(`--typecheck-hook: package.json has no ${script ? `"${script}"` : '"typecheck" or "type-check"'} script. Pass the name: --typecheck-hook <script>.`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const source = resolve(__dir, 'skills', 'audit-agent-speed', 'scripts', 'stop-typecheck.mjs');
|
|
336
|
+
const destScript = join(repo, '.claude', 'hooks', 'stop-typecheck.mjs');
|
|
337
|
+
const settingsPath = join(repo, '.claude', 'settings.json');
|
|
338
|
+
let settings = {};
|
|
339
|
+
if (existsSync(settingsPath)) {
|
|
340
|
+
try {
|
|
341
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
342
|
+
} catch {
|
|
343
|
+
console.error(`--typecheck-hook: ${settingsPath} is not valid JSON; nothing changed.`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
348
|
+
const managed = /stop-typecheck\.mjs/;
|
|
349
|
+
const groups = (Array.isArray(settings.hooks.Stop) ? settings.hooks.Stop : [])
|
|
350
|
+
.map((group) => ({ ...group, hooks: (group?.hooks ?? []).filter((h) => !managed.test(String(h?.command ?? ''))) }))
|
|
351
|
+
.filter((group) => group.hooks.length > 0);
|
|
352
|
+
const blocking = groups.flatMap((g) => g.hooks).filter((h) => !h.async && !h.asyncRewake && /typecheck|type-check|tsc\b/.test(String(h.command ?? '')));
|
|
353
|
+
settings.hooks.Stop = [
|
|
354
|
+
...groups,
|
|
355
|
+
{ hooks: [{ type: 'command', command: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-typecheck.mjs" ${script}`, asyncRewake: true, timeout: 600 }] },
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
if (isDryRun) {
|
|
359
|
+
console.log(` [dry-run] ${source} → ${destScript}`);
|
|
360
|
+
} else {
|
|
361
|
+
mkdirSync(dirname(destScript), { recursive: true });
|
|
362
|
+
writeFileSync(destScript, readFileSync(source));
|
|
363
|
+
}
|
|
364
|
+
const status = writeManagedFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
365
|
+
console.log(`${isDryRun ? '[dry-run] ' : '✓ '}Background typecheck Stop hook (${script}) → ${settingsPath} (${status})`);
|
|
366
|
+
for (const h of blocking) {
|
|
367
|
+
console.log(` [!] A blocking typecheck Stop hook is still registered: ${h.command}\n Remove it; the background hook replaces it.`);
|
|
368
|
+
}
|
|
369
|
+
process.exit(0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- restore mode ----------------------------------------------------------
|
|
373
|
+
if (has('restore')) {
|
|
374
|
+
const restoreBase = wantClaude && !wantCursor ? claudeBase : cursorBase;
|
|
375
|
+
const backupsRoot = [BACKUPS_DIR, LEGACY_BACKUPS_DIR]
|
|
376
|
+
.map((dir) => join(restoreBase, dir))
|
|
377
|
+
.find((dir) => existsSync(dir)) ?? join(restoreBase, BACKUPS_DIR);
|
|
378
|
+
const stamp = typeof opts.restore === 'string'
|
|
379
|
+
? opts.restore
|
|
380
|
+
: (existsSync(backupsRoot)
|
|
381
|
+
? readdirSync(backupsRoot).filter((n) => statSync(join(backupsRoot, n)).isDirectory()).sort().pop()
|
|
382
|
+
: null);
|
|
383
|
+
if (!stamp) { console.error('No backups found under ' + backupsRoot); process.exit(1); }
|
|
384
|
+
const snap = join(backupsRoot, stamp);
|
|
385
|
+
if (!existsSync(snap)) { console.error('Backup not found: ' + snap); process.exit(1); }
|
|
386
|
+
|
|
387
|
+
let restored = 0;
|
|
388
|
+
for (const dest of readdirSync(snap)) {
|
|
389
|
+
const from = join(snap, dest);
|
|
390
|
+
const to = join(restoreBase, dest);
|
|
391
|
+
if (isDryRun) {
|
|
392
|
+
console.log(` [dry-run] restore ${from} → ${to}`);
|
|
393
|
+
} else {
|
|
394
|
+
rmSync(to, { recursive: true, force: true });
|
|
395
|
+
cpSync(from, to, { recursive: true });
|
|
396
|
+
}
|
|
397
|
+
restored++;
|
|
398
|
+
}
|
|
399
|
+
console.log(`${isDryRun ? '[dry-run] ' : '✓ '}Restored ${restored} dir(s) from ${snap}`);
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---- selection (--only / --skill) ------------------------------------------
|
|
404
|
+
const skillName = typeof opts.skill === 'string' ? opts.skill : null;
|
|
405
|
+
let onlyGroups = null;
|
|
406
|
+
if (typeof opts.only === 'string') onlyGroups = new Set(opts.only.split(',').map((s) => s.trim()).filter(Boolean));
|
|
407
|
+
if (skillName) onlyGroups = new Set(['skills']);
|
|
408
|
+
|
|
409
|
+
const DIRS = ALL_DIRS.filter((d) => !onlyGroups || onlyGroups.has(d.dest));
|
|
410
|
+
const managedDests = [...new Set(DIRS.map((d) => d.dest))];
|
|
411
|
+
|
|
412
|
+
// Text files are hashed with line endings normalized: a Windows checkout with
|
|
413
|
+
// core.autocrlf=true carries CRLF while the npm tarball (built on Linux) is
|
|
414
|
+
// LF, and --verify must not report identical content as a mismatch.
|
|
415
|
+
const TEXT_EXT = /\.(md|mdc|mjs|js|cjs|ts|tsx|json|jsonc|txt|toml|ya?ml|sh|cmd|svg|css|html)$/i;
|
|
416
|
+
function fileHash(p) {
|
|
417
|
+
const buf = readFileSync(p);
|
|
418
|
+
const data = TEXT_EXT.test(p) ? buf.toString('utf8').replace(/\r\n/g, '\n') : buf;
|
|
419
|
+
return createHash('sha256').update(data).digest('hex');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function walkRelFiles(root) {
|
|
423
|
+
const out = [];
|
|
424
|
+
const walk = (rel) => {
|
|
425
|
+
const p = rel ? join(root, rel) : root;
|
|
426
|
+
if (!existsSync(p)) return;
|
|
427
|
+
const st = statSync(p);
|
|
428
|
+
if (st.isDirectory()) {
|
|
429
|
+
for (const name of readdirSync(p).sort()) {
|
|
430
|
+
walk(rel ? `${rel}/${name}` : name);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
out.push(rel.replace(/\\/g, '/'));
|
|
435
|
+
};
|
|
436
|
+
walk('');
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Source tree must be a subset of dest with matching file hashes. Extra dest files are allowed (merge). */
|
|
441
|
+
function treesMatch(src, dest) {
|
|
442
|
+
if (!existsSync(dest)) return { ok: false, reason: 'missing destination' };
|
|
443
|
+
const sDir = statSync(src).isDirectory();
|
|
444
|
+
const dDir = statSync(dest).isDirectory();
|
|
445
|
+
if (sDir !== dDir) return { ok: false, reason: 'file/directory type mismatch' };
|
|
446
|
+
if (!sDir) {
|
|
447
|
+
return fileHash(src) === fileHash(dest)
|
|
448
|
+
? { ok: true }
|
|
449
|
+
: { ok: false, reason: 'content hash mismatch' };
|
|
450
|
+
}
|
|
451
|
+
for (const rel of walkRelFiles(src)) {
|
|
452
|
+
const from = join(src, rel);
|
|
453
|
+
const to = join(dest, rel);
|
|
454
|
+
if (!existsSync(to)) return { ok: false, reason: `missing ${rel}` };
|
|
455
|
+
if (statSync(to).isDirectory()) return { ok: false, reason: `${rel} is a directory at dest` };
|
|
456
|
+
if (fileHash(from) !== fileHash(to)) return { ok: false, reason: `content hash mismatch in ${rel}` };
|
|
457
|
+
}
|
|
458
|
+
return { ok: true };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function listInstallItems({ renameMdc = false, skipCursorBuiltins = false } = {}) {
|
|
462
|
+
const items = [];
|
|
463
|
+
for (const { src, dest } of DIRS) {
|
|
464
|
+
const srcPath = resolve(__dir, src);
|
|
465
|
+
if (!existsSync(srcPath)) continue;
|
|
466
|
+
for (const item of readdirSync(srcPath)) {
|
|
467
|
+
if (skillName && item !== skillName) continue;
|
|
468
|
+
if (
|
|
469
|
+
skipCursorBuiltins &&
|
|
470
|
+
src === 'skills-cursor' &&
|
|
471
|
+
dest === 'skills' &&
|
|
472
|
+
CURSOR_MANAGED_BUILTIN_SKILLS.has(item) &&
|
|
473
|
+
item !== skillName
|
|
474
|
+
) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const itemSrc = join(srcPath, item);
|
|
478
|
+
const isDir = statSync(itemSrc).isDirectory();
|
|
479
|
+
if (isDir && PROJECT_RULE_BUNDLES.has(item) && (dest === 'rules' || dest === 'commands')) continue;
|
|
480
|
+
if (!renameMdc && dest === 'rules' && !isDir && item.endsWith('.md')) continue;
|
|
481
|
+
let outName = item;
|
|
482
|
+
if (renameMdc && dest === 'rules' && !isDir && item.endsWith('.mdc')) {
|
|
483
|
+
outName = item.slice(0, -4) + '.md';
|
|
484
|
+
}
|
|
485
|
+
items.push({ dest, outName, itemSrc, isDir });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return items;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function recordVerify(src, dest, label) {
|
|
492
|
+
const match = treesMatch(src, dest);
|
|
493
|
+
if (!match.ok) verifyFailures.push(`${label}: ${match.reason}`);
|
|
494
|
+
return match.ok;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function dualSkillCommandNames() {
|
|
498
|
+
const skillNames = new Set();
|
|
499
|
+
for (const group of ['skills', 'skills-cursor']) {
|
|
500
|
+
const dir = resolve(__dir, group);
|
|
501
|
+
if (!existsSync(dir)) continue;
|
|
502
|
+
for (const name of readdirSync(dir)) skillNames.add(name);
|
|
503
|
+
}
|
|
504
|
+
const cmdDir = resolve(__dir, 'commands');
|
|
505
|
+
if (!existsSync(cmdDir)) return [];
|
|
506
|
+
return readdirSync(cmdDir)
|
|
507
|
+
.filter((f) => f.endsWith('.md'))
|
|
508
|
+
.map((f) => f.slice(0, -3))
|
|
509
|
+
.filter((n) => skillNames.has(n))
|
|
510
|
+
.sort();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function assertNoCursorBuiltins(skillsDir) {
|
|
514
|
+
if (!existsSync(skillsDir)) return;
|
|
515
|
+
for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
|
|
516
|
+
if (skillName && name === skillName) continue;
|
|
517
|
+
if (existsSync(join(skillsDir, name))) {
|
|
518
|
+
verifyFailures.push(`skills/${name}: Cursor-managed builtin still present (should be absent)`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function verifyTarget(base, opts, label) {
|
|
524
|
+
for (const it of listInstallItems(opts)) {
|
|
525
|
+
recordVerify(it.itemSrc, join(base, it.dest, it.outName), `${label} ${it.dest}/${it.outName}`);
|
|
526
|
+
}
|
|
527
|
+
if (opts.skipCursorBuiltins) assertNoCursorBuiltins(join(base, 'skills'));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function failIfUnverified(context) {
|
|
531
|
+
if (!verifyFailures.length) return;
|
|
532
|
+
console.error(`✗ ${context} failed:`);
|
|
533
|
+
for (const f of verifyFailures) console.error(` - ${f}`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---- placement helper (copy or symlink) ------------------------------------
|
|
538
|
+
let linkFallbacks = 0;
|
|
539
|
+
function place(src, dest, isDir) {
|
|
540
|
+
if (isDryRun) { console.log(` [dry-run] ${useLink ? 'link' : 'copy'} ${src} → ${dest}`); return; }
|
|
541
|
+
if (useLink) {
|
|
542
|
+
rmSync(dest, { recursive: true, force: true });
|
|
543
|
+
try {
|
|
544
|
+
const type = isDir ? (platform() === 'win32' ? 'junction' : 'dir') : 'file';
|
|
545
|
+
symlinkSync(src, dest, type);
|
|
546
|
+
} catch {
|
|
547
|
+
cpSync(src, dest, { recursive: true }); // file symlinks may need privileges on Windows
|
|
548
|
+
linkFallbacks++;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
cpSync(src, dest, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
recordVerify(src, dest, dest);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---- mirror mode: back up, then wipe managed dirs under a target base ------
|
|
557
|
+
function backupAndWipe(base) {
|
|
558
|
+
let wiped = 0;
|
|
559
|
+
let backupRoot = null;
|
|
560
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
561
|
+
backupRoot = join(base, BACKUPS_DIR, stamp);
|
|
562
|
+
for (const dest of managedDests) {
|
|
563
|
+
const p = join(base, dest);
|
|
564
|
+
if (!existsSync(p)) continue;
|
|
565
|
+
if (isDryRun) {
|
|
566
|
+
console.log(` [dry-run] backup ${p} → ${join(backupRoot, dest)}`);
|
|
567
|
+
console.log(` [dry-run] wipe ${p}`);
|
|
568
|
+
} else {
|
|
569
|
+
if (!noBackup) { mkdirSync(backupRoot, { recursive: true }); cpSync(p, join(backupRoot, dest), { recursive: true }); }
|
|
570
|
+
rmSync(p, { recursive: true, force: true });
|
|
571
|
+
}
|
|
572
|
+
wiped++;
|
|
573
|
+
}
|
|
574
|
+
return { wiped, backupRoot };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---- copy / link one target base -------------------------------------------
|
|
578
|
+
// renameMdc: Claude Code reads .md rules; .mdc sources are installed as .md.
|
|
579
|
+
function installDirs(base, opts = {}) {
|
|
580
|
+
let copiedDirs = 0;
|
|
581
|
+
let copiedFiles = 0;
|
|
582
|
+
const ensured = new Set();
|
|
583
|
+
for (const it of listInstallItems(opts)) {
|
|
584
|
+
const destPath = join(base, it.dest);
|
|
585
|
+
if (!isDryRun && !ensured.has(destPath)) {
|
|
586
|
+
mkdirSync(destPath, { recursive: true });
|
|
587
|
+
ensured.add(destPath);
|
|
588
|
+
}
|
|
589
|
+
place(it.itemSrc, join(destPath, it.outName), it.isDir);
|
|
590
|
+
if (it.isDir) copiedDirs++; else copiedFiles++;
|
|
591
|
+
}
|
|
592
|
+
return { copiedDirs, copiedFiles };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// Codex CLI + Gemini CLI — tools with NO skills system.
|
|
597
|
+
// They read a single global context/instructions file, so we map:
|
|
598
|
+
// rules/ (minus the skill-routing index) → one merged Markdown file
|
|
599
|
+
// commands-portable/ → per-tool custom prompt/command files
|
|
600
|
+
// No SKILL.md dirs are written — nothing in these tools would load them.
|
|
601
|
+
// ============================================================================
|
|
602
|
+
|
|
603
|
+
// Detect installed tools by their home-dir config footprint.
|
|
604
|
+
function detectTools() {
|
|
605
|
+
const found = new Set();
|
|
606
|
+
if (existsSync(cursorBase)) found.add('cursor');
|
|
607
|
+
if (existsSync(claudeBase)) found.add('claude');
|
|
608
|
+
if (existsSync(codexBase)) found.add('codex');
|
|
609
|
+
if (existsSync(geminiBase)) found.add('gemini');
|
|
610
|
+
return found;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Rules merged into the global context file. The skill-routing index is
|
|
614
|
+
// excluded — it points at skills these tools cannot load (would be dead text).
|
|
615
|
+
const RULES_EXCLUDE = new Set(['skill-workflows.mdc']);
|
|
616
|
+
const RULES_ORDER = [
|
|
617
|
+
'senior-engineer.mdc',
|
|
618
|
+
'full-stack-ship-discipline.mdc',
|
|
619
|
+
'approved-plan-execution.mdc', // renamed from composer-2.5-execution.mdc
|
|
620
|
+
'shell-first-search.mdc', // was .md; now agent-requested in Cursor too
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
// Strip a leading `--- ... ---` YAML frontmatter block, returning the body.
|
|
624
|
+
function stripFrontmatter(text) {
|
|
625
|
+
if (!text.startsWith('---')) return text;
|
|
626
|
+
const end = text.indexOf('\n---', 3);
|
|
627
|
+
if (end === -1) return text;
|
|
628
|
+
const after = text.indexOf('\n', end + 1);
|
|
629
|
+
return text.slice(after + 1).replace(/^\s+/, '');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Parse `description:` out of frontmatter and return { description, body }.
|
|
633
|
+
function parseFrontmatter(text) {
|
|
634
|
+
let description = '';
|
|
635
|
+
if (text.startsWith('---')) {
|
|
636
|
+
const end = text.indexOf('\n---', 3);
|
|
637
|
+
if (end !== -1) {
|
|
638
|
+
const m = text.slice(3, end).match(/description:\s*"?(.*?)"?\s*$/m);
|
|
639
|
+
if (m) description = m[1];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { description, body: stripFrontmatter(text) };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Concatenate top-level rule files (non-recursive — skips project-starter/ etc.)
|
|
646
|
+
// into one deterministic Markdown document. No timestamp → regeneration is
|
|
647
|
+
// idempotent, so re-running the installer produces byte-identical output.
|
|
648
|
+
function buildMergedRules(toolLabel, loadPath) {
|
|
649
|
+
const rulesDir = resolve(__dir, 'rules');
|
|
650
|
+
for (const name of RULES_ORDER) {
|
|
651
|
+
if (!existsSync(join(rulesDir, name))) {
|
|
652
|
+
throw new Error(`RULES_ORDER names a missing rule: rules/${name} — update the list after a rename`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const files = readdirSync(rulesDir)
|
|
656
|
+
.filter((f) => (f.endsWith('.md') || f.endsWith('.mdc'))
|
|
657
|
+
&& !RULES_EXCLUDE.has(f)
|
|
658
|
+
&& statSync(join(rulesDir, f)).isFile())
|
|
659
|
+
.sort((a, b) => {
|
|
660
|
+
const ia = RULES_ORDER.indexOf(a); const ib = RULES_ORDER.indexOf(b);
|
|
661
|
+
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);
|
|
662
|
+
});
|
|
663
|
+
const header = `<!-- Generated by kenji — https://github.com/kensaurus/skills
|
|
664
|
+
Merged from rules/. The skill-routing index is omitted (this tool has no skills loader).
|
|
665
|
+
Applies to: ${toolLabel} — loaded automatically from ${loadPath}.
|
|
666
|
+
Regenerate: npx @kensaurus/skills --auto -->\n\n`;
|
|
667
|
+
const body = files
|
|
668
|
+
.map((f) => stripFrontmatter(readFileSync(join(rulesDir, f), 'utf8')).trim())
|
|
669
|
+
.join('\n\n---\n\n');
|
|
670
|
+
return header + body + '\n';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Load the tool-agnostic portable commands (single source of truth).
|
|
674
|
+
function portableCommands() {
|
|
675
|
+
const dir = resolve(__dir, 'commands-portable');
|
|
676
|
+
if (!existsSync(dir)) return [];
|
|
677
|
+
return readdirSync(dir)
|
|
678
|
+
.filter((f) => f.endsWith('.md'))
|
|
679
|
+
.sort()
|
|
680
|
+
.map((f) => {
|
|
681
|
+
const { description, body } = parseFrontmatter(readFileSync(join(dir, f), 'utf8'));
|
|
682
|
+
return { name: f.replace(/\.md$/, ''), description, body: body.trim() };
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Wrap a portable command as a Gemini TOML command. Uses a literal ''' string
|
|
687
|
+
// so shell backslashes/quotes pass through verbatim; fails loud if the body
|
|
688
|
+
// would collide with the delimiter rather than silently corrupting output.
|
|
689
|
+
function toGeminiToml({ name, description, body }) {
|
|
690
|
+
if (body.includes("'''")) {
|
|
691
|
+
throw new Error(`portable command '${name}' contains ''' — cannot wrap as a TOML literal string`);
|
|
692
|
+
}
|
|
693
|
+
const desc = String(description).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
694
|
+
return `description = "${desc}"\n\nprompt = '''\n${body}\n'''\n`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Write a single managed file. Idempotent (skips identical content), backs up
|
|
698
|
+
// on overwrite unless --no-backup, and honours --dry-run.
|
|
699
|
+
function writeManagedFile(destPath, content) {
|
|
700
|
+
if (isDryRun) { console.log(` [dry-run] write ${destPath}`); return 'dry-run'; }
|
|
701
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
702
|
+
if (existsSync(destPath)) {
|
|
703
|
+
if (readFileSync(destPath, 'utf8') === content) return 'unchanged';
|
|
704
|
+
if (!noBackup) {
|
|
705
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
706
|
+
cpSync(destPath, `${destPath}.bak-${stamp}`);
|
|
707
|
+
}
|
|
708
|
+
writeFileSync(destPath, content);
|
|
709
|
+
return 'updated';
|
|
710
|
+
}
|
|
711
|
+
writeFileSync(destPath, content);
|
|
712
|
+
return 'created';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Install the opt-in completion gate as a user hook. The hook itself is inert
|
|
716
|
+
// unless the active workspace has an unfinished durable closure-state file.
|
|
717
|
+
// Existing user hooks are preserved and this managed entry is replaced
|
|
718
|
+
// idempotently on upgrades.
|
|
719
|
+
// The managed entry now points at HOOK_DIR, so a pre-2.0.0 LEGACY_HOOK_DIR copy
|
|
720
|
+
// is orphaned. Remove it only when it holds nothing but the gate script.
|
|
721
|
+
function removeLegacyHookDir(base) {
|
|
722
|
+
const legacyDir = join(base, LEGACY_HOOK_DIR);
|
|
723
|
+
if (!existsSync(legacyDir)) return;
|
|
724
|
+
const entries = readdirSync(legacyDir);
|
|
725
|
+
if (entries.some((name) => name !== 'completion-gate.mjs')) return;
|
|
726
|
+
if (isDryRun) {
|
|
727
|
+
console.log(` [dry-run] remove legacy ${legacyDir}`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function installCursorCompletionHook() {
|
|
734
|
+
const sourceScript = resolve(__dir, 'hooks', 'completion-gate.mjs');
|
|
735
|
+
const hookDir = join(cursorBase, HOOK_DIR);
|
|
736
|
+
const destScript = join(hookDir, 'completion-gate.mjs');
|
|
737
|
+
const configPath = join(cursorBase, 'hooks.json');
|
|
738
|
+
if (!existsSync(sourceScript)) return 'source-missing';
|
|
739
|
+
|
|
740
|
+
if (isDryRun) {
|
|
741
|
+
console.log(` [dry-run] ${sourceScript} → ${destScript}`);
|
|
742
|
+
} else {
|
|
743
|
+
mkdirSync(hookDir, { recursive: true });
|
|
744
|
+
place(sourceScript, destScript, false);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let config = { version: 1, hooks: {} };
|
|
748
|
+
if (existsSync(configPath)) {
|
|
749
|
+
try {
|
|
750
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
751
|
+
} catch {
|
|
752
|
+
console.warn(` [!] Skipped completion hook: ${configPath} is not valid JSON.`);
|
|
753
|
+
return 'skipped-invalid-config';
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
758
|
+
if (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
759
|
+
config.hooks = {};
|
|
760
|
+
}
|
|
761
|
+
if (!('version' in config)) config.version = 1;
|
|
762
|
+
|
|
763
|
+
const existing = Array.isArray(config.hooks.stop) ? config.hooks.stop : [];
|
|
764
|
+
const preserved = existing.filter(
|
|
765
|
+
(entry) => !MANAGED_GATE.test(String(entry?.command ?? '')),
|
|
766
|
+
);
|
|
767
|
+
config.hooks.stop = [
|
|
768
|
+
...preserved,
|
|
769
|
+
{
|
|
770
|
+
command: `node ${HOOK_DIR}/completion-gate.mjs --host=cursor`,
|
|
771
|
+
timeout: 5,
|
|
772
|
+
loop_limit: 12,
|
|
773
|
+
failClosed: false,
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
|
|
777
|
+
const status = writeManagedFile(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
778
|
+
removeLegacyHookDir(cursorBase);
|
|
779
|
+
return status;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Claude Code reads Stop hooks from ~/.claude/settings.json. Same script,
|
|
783
|
+
// Claude schema; the managed entry is replaced idempotently and user hooks
|
|
784
|
+
// in other groups are preserved. Cursor loads this file too (Third-Party
|
|
785
|
+
// Imports), so `--host=claude` lets the script stand aside there when the
|
|
786
|
+
// native Cursor entry exists.
|
|
787
|
+
function installClaudeCompletionHook() {
|
|
788
|
+
const sourceScript = resolve(__dir, 'hooks', 'completion-gate.mjs');
|
|
789
|
+
const hookDir = join(claudeBase, HOOK_DIR);
|
|
790
|
+
const destScript = join(hookDir, 'completion-gate.mjs');
|
|
791
|
+
const settingsPath = join(claudeBase, 'settings.json');
|
|
792
|
+
if (!existsSync(sourceScript)) return 'source-missing';
|
|
793
|
+
|
|
794
|
+
if (isDryRun) {
|
|
795
|
+
console.log(` [dry-run] ${sourceScript} → ${destScript}`);
|
|
796
|
+
} else {
|
|
797
|
+
mkdirSync(hookDir, { recursive: true });
|
|
798
|
+
place(sourceScript, destScript, false);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
let settings = {};
|
|
802
|
+
if (existsSync(settingsPath)) {
|
|
803
|
+
try {
|
|
804
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
805
|
+
} catch {
|
|
806
|
+
console.warn(` [!] Skipped completion hook: ${settingsPath} is not valid JSON.`);
|
|
807
|
+
return 'skipped-invalid-config';
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) settings = {};
|
|
811
|
+
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
|
|
812
|
+
|
|
813
|
+
const existing = Array.isArray(settings.hooks.Stop) ? settings.hooks.Stop : [];
|
|
814
|
+
const preserved = existing
|
|
815
|
+
.map((group) => ({
|
|
816
|
+
...group,
|
|
817
|
+
hooks: (Array.isArray(group?.hooks) ? group.hooks : []).filter(
|
|
818
|
+
(h) => !MANAGED_GATE.test(String(h?.command ?? '')),
|
|
819
|
+
),
|
|
820
|
+
}))
|
|
821
|
+
.filter((group) => group.hooks.length > 0);
|
|
822
|
+
settings.hooks.Stop = [
|
|
823
|
+
...preserved,
|
|
824
|
+
{ hooks: [{ type: 'command', command: `node "${destScript.replace(/\\/g, '/')}" --host=claude`, timeout: 5 }] },
|
|
825
|
+
];
|
|
826
|
+
const status = writeManagedFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
827
|
+
removeLegacyHookDir(claudeBase);
|
|
828
|
+
return status;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Install a context-file tool (Codex / Gemini). `spec` describes its layout.
|
|
832
|
+
function installContextTool(spec) {
|
|
833
|
+
const { label, base, rulesFile, rulesLoadPath, commandsDir, commandExt, render } = spec;
|
|
834
|
+
console.log(`\n${isDryRun ? '[dry-run] ' : ''}${label} → ${base}`);
|
|
835
|
+
|
|
836
|
+
// --skill / --only skills has no meaning here (no skills loader).
|
|
837
|
+
const doRules = !onlyGroups || onlyGroups.has('rules');
|
|
838
|
+
const doCommands = (!onlyGroups || onlyGroups.has('commands')) && !skillName;
|
|
839
|
+
if (!doRules && !doCommands) {
|
|
840
|
+
console.log(' (no skills loader — nothing to install for the selected groups)');
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
let rulesStatus = 'skipped';
|
|
845
|
+
if (doRules) {
|
|
846
|
+
rulesStatus = writeManagedFile(join(base, rulesFile), buildMergedRules(label, rulesLoadPath));
|
|
847
|
+
}
|
|
848
|
+
let cmdCount = 0;
|
|
849
|
+
if (doCommands) {
|
|
850
|
+
for (const cmd of portableCommands()) {
|
|
851
|
+
writeManagedFile(join(base, commandsDir, `${cmd.name}${commandExt}`), render(cmd));
|
|
852
|
+
cmdCount++;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (!isDryRun) {
|
|
856
|
+
const parts = [];
|
|
857
|
+
if (doRules) parts.push(`${rulesFile} ${rulesStatus}`);
|
|
858
|
+
if (doCommands) parts.push(`${cmdCount} command(s) → ${commandsDir}/`);
|
|
859
|
+
console.log(` ✓ ${parts.join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function verifyContextTool(spec) {
|
|
864
|
+
const { label, base, rulesFile, rulesLoadPath, commandsDir, commandExt, render } = spec;
|
|
865
|
+
const doRules = !onlyGroups || onlyGroups.has('rules');
|
|
866
|
+
const doCommands = (!onlyGroups || onlyGroups.has('commands')) && !skillName;
|
|
867
|
+
if (doRules) {
|
|
868
|
+
const dest = join(base, rulesFile);
|
|
869
|
+
const expected = buildMergedRules(label, rulesLoadPath);
|
|
870
|
+
if (!existsSync(dest)) verifyFailures.push(`${label} ${rulesFile}: missing destination`);
|
|
871
|
+
else if (readFileSync(dest, 'utf8') !== expected) verifyFailures.push(`${label} ${rulesFile}: content hash mismatch`);
|
|
872
|
+
}
|
|
873
|
+
if (doCommands) {
|
|
874
|
+
for (const cmd of portableCommands()) {
|
|
875
|
+
const dest = join(base, commandsDir, `${cmd.name}${commandExt}`);
|
|
876
|
+
const expected = render(cmd);
|
|
877
|
+
if (!existsSync(dest)) verifyFailures.push(`${label} ${cmd.name}: missing destination`);
|
|
878
|
+
else if (readFileSync(dest, 'utf8') !== expected) verifyFailures.push(`${label} ${cmd.name}: content hash mismatch`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (isVerifyOnly) {
|
|
884
|
+
if (wantCursor) {
|
|
885
|
+
verifyTarget(cursorBase, { skipCursorBuiltins: true }, 'Cursor');
|
|
886
|
+
if (!noAgentsMirror && (!onlyGroups || onlyGroups.has('skills'))) {
|
|
887
|
+
const agentsSkillsDest = join(agentsBase, 'skills');
|
|
888
|
+
for (const it of listInstallItems({ skipCursorBuiltins: true })) {
|
|
889
|
+
if (it.dest !== 'skills') continue;
|
|
890
|
+
recordVerify(it.itemSrc, join(agentsSkillsDest, it.outName), `Agents ${it.outName}`);
|
|
891
|
+
}
|
|
892
|
+
assertNoCursorBuiltins(agentsSkillsDest);
|
|
893
|
+
}
|
|
894
|
+
if (!onlyGroups || onlyGroups.has('hooks')) {
|
|
895
|
+
const hookSrc = resolve(__dir, 'hooks', 'completion-gate.mjs');
|
|
896
|
+
const hookDest = join(cursorBase, HOOK_DIR, 'completion-gate.mjs');
|
|
897
|
+
if (existsSync(hookSrc)) recordVerify(hookSrc, hookDest, 'Cursor completion-gate.mjs');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (wantClaude) {
|
|
901
|
+
verifyTarget(claudeBase, { renameMdc: true }, 'Claude');
|
|
902
|
+
if (!onlyGroups || onlyGroups.has('hooks')) {
|
|
903
|
+
const hookSrc = resolve(__dir, 'hooks', 'completion-gate.mjs');
|
|
904
|
+
const hookDest = join(claudeBase, HOOK_DIR, 'completion-gate.mjs');
|
|
905
|
+
if (existsSync(hookSrc)) recordVerify(hookSrc, hookDest, 'Claude completion-gate.mjs');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (wantCodex) {
|
|
909
|
+
verifyContextTool({
|
|
910
|
+
label: 'Codex CLI (OpenAI)',
|
|
911
|
+
base: codexBase,
|
|
912
|
+
rulesFile: 'AGENTS.md',
|
|
913
|
+
rulesLoadPath: '~/.codex/AGENTS.md',
|
|
914
|
+
commandsDir: 'prompts',
|
|
915
|
+
commandExt: '.md',
|
|
916
|
+
render: (cmd) => (cmd.body.endsWith('\n') ? cmd.body : cmd.body + '\n'),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (wantGemini) {
|
|
920
|
+
verifyContextTool({
|
|
921
|
+
label: 'Gemini CLI',
|
|
922
|
+
base: geminiBase,
|
|
923
|
+
rulesFile: 'GEMINI.md',
|
|
924
|
+
rulesLoadPath: '~/.gemini/GEMINI.md',
|
|
925
|
+
commandsDir: 'commands',
|
|
926
|
+
commandExt: '.toml',
|
|
927
|
+
render: toGeminiToml,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
failIfUnverified('install verify');
|
|
931
|
+
console.log('✓ install verify passed — every packaged file matches the destination.');
|
|
932
|
+
process.exit(0);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const results = [];
|
|
936
|
+
|
|
937
|
+
// ---- Cursor ----------------------------------------------------------------
|
|
938
|
+
if (wantCursor) {
|
|
939
|
+
const clean = isClean ? backupAndWipe(cursorBase) : null;
|
|
940
|
+
const counts = installDirs(cursorBase, { skipCursorBuiltins: true });
|
|
941
|
+
if (!skillName && !isDryRun) {
|
|
942
|
+
const cursorSkills = join(cursorBase, 'skills');
|
|
943
|
+
for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
|
|
944
|
+
const stale = join(cursorSkills, name);
|
|
945
|
+
if (existsSync(stale)) rmSync(stale, { recursive: true, force: true });
|
|
946
|
+
}
|
|
947
|
+
assertNoCursorBuiltins(cursorSkills);
|
|
948
|
+
}
|
|
949
|
+
pruneRenamedSkills(join(cursorBase, 'skills'), 'Cursor');
|
|
950
|
+
pruneRenamedRules(join(cursorBase, 'rules'), 'Cursor');
|
|
951
|
+
pruneStaleCommands(join(cursorBase, 'commands'), 'Cursor');
|
|
952
|
+
const hookStatus =
|
|
953
|
+
!onlyGroups || onlyGroups.has('hooks')
|
|
954
|
+
? installCursorCompletionHook()
|
|
955
|
+
: 'skipped';
|
|
956
|
+
|
|
957
|
+
if (skillName && counts.copiedDirs + counts.copiedFiles === 0) {
|
|
958
|
+
console.error(`✗ Skill '${skillName}' not found in skills/ or skills-cursor/.`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Also write skills to ~/.agents/skills/ (Cursor Skills UI historically reads here).
|
|
963
|
+
if (!noAgentsMirror && (!onlyGroups || onlyGroups.has('skills'))) {
|
|
964
|
+
const agentsSkillsDest = join(agentsBase, 'skills');
|
|
965
|
+
const cursorSkillsSrc = join(cursorBase, 'skills');
|
|
966
|
+
if (isDryRun) {
|
|
967
|
+
console.log(` [dry-run] sync ${cursorSkillsSrc} → ${agentsSkillsDest}`);
|
|
968
|
+
} else if (existsSync(cursorSkillsSrc)) {
|
|
969
|
+
if (isClean) rmSync(agentsSkillsDest, { recursive: true, force: true });
|
|
970
|
+
mkdirSync(agentsSkillsDest, { recursive: true });
|
|
971
|
+
for (const item of readdirSync(cursorSkillsSrc)) {
|
|
972
|
+
if (skillName && item !== skillName) continue;
|
|
973
|
+
const from = join(cursorSkillsSrc, item);
|
|
974
|
+
const to = join(agentsSkillsDest, item);
|
|
975
|
+
cpSync(from, to, { recursive: true });
|
|
976
|
+
recordVerify(from, to, `Agents ${item}`);
|
|
977
|
+
}
|
|
978
|
+
if (!skillName) {
|
|
979
|
+
for (const name of CURSOR_MANAGED_BUILTIN_SKILLS) {
|
|
980
|
+
const stale = join(agentsSkillsDest, name);
|
|
981
|
+
if (existsSync(stale)) rmSync(stale, { recursive: true, force: true });
|
|
982
|
+
}
|
|
983
|
+
assertNoCursorBuiltins(agentsSkillsDest);
|
|
984
|
+
}
|
|
985
|
+
pruneRenamedSkills(agentsSkillsDest, 'Agents');
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// MCP config template (only if missing; never overwritten).
|
|
990
|
+
const mcpDest = join(cursorBase, 'mcp.json');
|
|
991
|
+
const mcpTemplate = resolve(__dir, 'mcp', 'mcp.json.template');
|
|
992
|
+
let mcpInstalled = false;
|
|
993
|
+
if (!onlyGroups && existsSync(mcpTemplate) && !existsSync(mcpDest)) {
|
|
994
|
+
if (isDryRun) console.log(` [dry-run] ${mcpTemplate} → ${mcpDest}`);
|
|
995
|
+
else { mkdirSync(cursorBase, { recursive: true }); cpSync(mcpTemplate, mcpDest); }
|
|
996
|
+
mcpInstalled = true;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
results.push({ target: 'Cursor', base: cursorBase, ...counts, clean, mcpInstalled, hookStatus });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ---- Claude Code -----------------------------------------------------------
|
|
1003
|
+
if (wantClaude) {
|
|
1004
|
+
const clean = isClean ? backupAndWipe(claudeBase) : null;
|
|
1005
|
+
const counts = installDirs(claudeBase, { renameMdc: true });
|
|
1006
|
+
pruneRenamedSkills(join(claudeBase, 'skills'), 'Claude');
|
|
1007
|
+
pruneRenamedRules(join(claudeBase, 'rules'), 'Claude', { renameMdc: true });
|
|
1008
|
+
pruneStaleCommands(join(claudeBase, 'commands'), 'Claude');
|
|
1009
|
+
const hookStatus = !onlyGroups || onlyGroups.has('hooks') ? installClaudeCompletionHook() : 'skipped';
|
|
1010
|
+
|
|
1011
|
+
if (skillName && counts.copiedDirs + counts.copiedFiles === 0) {
|
|
1012
|
+
console.error(`✗ Skill '${skillName}' not found in skills/ or skills-cursor/.`);
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (!isDryRun) {
|
|
1017
|
+
// Claude Code lists every model-invocable skill and command as
|
|
1018
|
+
// "- name: description" against 1% of the context window at 3 chars/token
|
|
1019
|
+
// for current models (30,000 chars on a 1M Opus 5.5 session), shared with
|
|
1020
|
+
// built-in skills; over budget the least-used entries drop to name only
|
|
1021
|
+
// (client 2.1.280, ADR-0010).
|
|
1022
|
+
let listingChars = 0;
|
|
1023
|
+
let entries = 0;
|
|
1024
|
+
const addEntry = (name, file) => {
|
|
1025
|
+
const text = readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
1026
|
+
const end = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
|
|
1027
|
+
if (end === -1) return;
|
|
1028
|
+
const front = text.slice(3, end);
|
|
1029
|
+
if (/^disable-model-invocation:\s*true\s*$/m.test(front)) return;
|
|
1030
|
+
const lines = front.split('\n');
|
|
1031
|
+
const s = lines.findIndex((l) => /^description:/.test(l));
|
|
1032
|
+
if (s === -1) return;
|
|
1033
|
+
const buf = [];
|
|
1034
|
+
const head = lines[s].slice('description:'.length).trim();
|
|
1035
|
+
if (head && !/^[>|][-+0-9]*$/.test(head)) buf.push(head);
|
|
1036
|
+
for (let j = s + 1; j < lines.length && !/^[A-Za-z0-9_-]+:/.test(lines[j]); j++) buf.push(lines[j].trim());
|
|
1037
|
+
const desc = buf.join(' ').replace(/\s+/g, ' ').replace(/^["']|["']$/g, '').trim();
|
|
1038
|
+
listingChars += name.length + 4 + Math.min(desc.length, 1536) + 1;
|
|
1039
|
+
entries++;
|
|
1040
|
+
};
|
|
1041
|
+
const skillsDir = join(claudeBase, 'skills');
|
|
1042
|
+
for (const d of existsSync(skillsDir) ? readdirSync(skillsDir) : []) {
|
|
1043
|
+
const f = join(skillsDir, d, 'SKILL.md');
|
|
1044
|
+
if (existsSync(f)) addEntry(d, f);
|
|
1045
|
+
}
|
|
1046
|
+
const cmdDir = join(claudeBase, 'commands');
|
|
1047
|
+
for (const f of existsSync(cmdDir) ? readdirSync(cmdDir) : []) {
|
|
1048
|
+
const p = join(cmdDir, f);
|
|
1049
|
+
if (f.endsWith('.md') && statSync(p).isFile()) addEntry(f.slice(0, -3), p);
|
|
1050
|
+
}
|
|
1051
|
+
console.log(` Claude Code skill listing from ~/.claude (all packs): ${entries} entries, ${listingChars} chars.`);
|
|
1052
|
+
console.log(' The default budget is 1% of the context window (30,000 chars on a 1M Opus 5.5 session), shared with built-in skills;');
|
|
1053
|
+
console.log(' over budget the least-used skills show by name only. To keep every description, set "skillListingBudgetFraction": 0.02 in ~/.claude/settings.json.');
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
results.push({ target: 'Claude Code', base: claudeBase, ...counts, clean, mcpInstalled: false, hookStatus });
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ---- Codex CLI (context file + prompt ports) -------------------------------
|
|
1060
|
+
if (wantCodex) {
|
|
1061
|
+
installContextTool({
|
|
1062
|
+
label: 'Codex CLI (OpenAI)',
|
|
1063
|
+
base: codexBase,
|
|
1064
|
+
rulesFile: 'AGENTS.md',
|
|
1065
|
+
rulesLoadPath: '~/.codex/AGENTS.md',
|
|
1066
|
+
commandsDir: 'prompts',
|
|
1067
|
+
commandExt: '.md',
|
|
1068
|
+
render: (cmd) => (cmd.body.endsWith('\n') ? cmd.body : cmd.body + '\n'),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ---- Gemini CLI (context file + TOML command ports) ------------------------
|
|
1073
|
+
if (wantGemini) {
|
|
1074
|
+
installContextTool({
|
|
1075
|
+
label: 'Gemini CLI',
|
|
1076
|
+
base: geminiBase,
|
|
1077
|
+
rulesFile: 'GEMINI.md',
|
|
1078
|
+
rulesLoadPath: '~/.gemini/GEMINI.md',
|
|
1079
|
+
commandsDir: 'commands',
|
|
1080
|
+
commandExt: '.toml',
|
|
1081
|
+
render: toGeminiToml,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ---- summary ---------------------------------------------------------------
|
|
1086
|
+
const mode = `${isClean ? 'mirror' : 'merge'}${useLink ? '+link' : ''}${skillName ? `:skill=${skillName}` : ''}`;
|
|
1087
|
+
const verb = useLink ? 'linked' : 'copied';
|
|
1088
|
+
|
|
1089
|
+
if (!isDryRun) failIfUnverified('post-install verify');
|
|
1090
|
+
|
|
1091
|
+
if (isDryRun) {
|
|
1092
|
+
for (const r of results) {
|
|
1093
|
+
console.log(
|
|
1094
|
+
`\n[dry-run] ${r.target} (${mode})` +
|
|
1095
|
+
(r.clean ? ` — would back up + wipe ${r.clean.wiped} managed dir(s)` : '') +
|
|
1096
|
+
`\n[dry-run] Would ${useLink ? 'link' : 'copy'} ${r.copiedDirs} directories and ${r.copiedFiles} files to ${r.base}` +
|
|
1097
|
+
(r.mcpInstalled ? ' (plus mcp.json template)' : '')
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
console.log('Run without --dry-run to apply.');
|
|
1101
|
+
} else {
|
|
1102
|
+
for (const r of results) {
|
|
1103
|
+
if (r.clean && r.clean.backupRoot && !noBackup && r.clean.wiped > 0) {
|
|
1104
|
+
console.log(`✓ Backed up previous ${r.base}/{${managedDests.join(',')}} → ${r.clean.backupRoot}`);
|
|
1105
|
+
}
|
|
1106
|
+
console.log(`\n✓ kenji installed for ${r.target} (${mode}) — ${r.copiedDirs} directories and ${r.copiedFiles} files ${verb} to ${r.base}`);
|
|
1107
|
+
if (r.target === 'Cursor' && (!onlyGroups || onlyGroups.has('skills'))) {
|
|
1108
|
+
const agentsCount = existsSync(join(agentsBase, 'skills')) ? readdirSync(join(agentsBase, 'skills')).length : 0;
|
|
1109
|
+
console.log(`✓ Skills synced to ${join(agentsBase, 'skills')} (${agentsCount} skills — Cursor UI path)`);
|
|
1110
|
+
}
|
|
1111
|
+
if (r.mcpInstalled) console.log(`✓ MCP template written to ${join(r.base, 'mcp.json')} — edit it to add your API keys.`);
|
|
1112
|
+
if (r.hookStatus && !r.hookStatus.startsWith('skip')) {
|
|
1113
|
+
const where = r.target === 'Cursor' ? 'hooks.json' : 'settings.json';
|
|
1114
|
+
console.log(`✓ Opt-in completion gate ${r.hookStatus} in ${join(r.base, where)}.`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (linkFallbacks) console.log(` (note: ${linkFallbacks} file(s) copied instead of linked — symlinks need elevated rights on this OS)`);
|
|
1118
|
+
const dual = dualSkillCommandNames();
|
|
1119
|
+
if (dual.length) {
|
|
1120
|
+
console.log(`Note: these names exist as both a skill and a /command (the picker may show two /entries): ${dual.join(', ')}`);
|
|
1121
|
+
}
|
|
1122
|
+
console.log('Re-check anytime with: npx @kensaurus/skills --verify');
|
|
1123
|
+
if (wantCursor) console.log('Restart Cursor to activate skills, commands, and agents.');
|
|
1124
|
+
if (wantClaude) console.log('Restart any active claude sessions — skills appear as /slash-commands.');
|
|
1125
|
+
if (wantCodex) console.log('Codex CLI loads ~/.codex/AGENTS.md automatically; prompts appear via the / picker.');
|
|
1126
|
+
if (wantGemini) console.log('Gemini CLI loads ~/.gemini/GEMINI.md automatically; run /memory refresh in an active session.');
|
|
1127
|
+
}
|