@workerstack/repo 0.0.0-stage → 0.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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Overlay after the hooks generator (`cli/sync/agents/hooks.ts`), for what hooks.jsonc cannot express.
|
|
4
|
+
* Union, never replace: the generator's own groups (scrub, gate, orchestrate-parent, session map)
|
|
5
|
+
* are left as generated.
|
|
6
|
+
*
|
|
7
|
+
* Claude/Codex native event is UserPromptSubmit (mapped from SoT beforeSubmitPrompt).
|
|
8
|
+
* Antigravity has neither beforeSubmitPrompt nor subagentStop.
|
|
9
|
+
* Grok SessionStart stays empty on orchestrate-parent.json.
|
|
10
|
+
* Claude/Codex SessionStart: session-map.ts (folder map) when shipped, first in the group.
|
|
11
|
+
* Antigravity SessionStart is the generator's flat entries, consumer extras appended flat.
|
|
12
|
+
* Cursor's .cursor/hooks.json is all flat entries; consumer extras are appended flat under its own tool name.
|
|
13
|
+
*
|
|
14
|
+
* Consumer extras (if those scripts exist): SessionStart, check-behind,
|
|
15
|
+
* validate+sim; Antigravity and Cursor get them under their own tool names.
|
|
16
|
+
* Path deny from consumer `.repo/permissions.json`.
|
|
17
|
+
* Claude WorktreeCreate / WorktreeRemove come from hooks.jsonc's claudecode block; --check needs them when worktree.ts ships.
|
|
18
|
+
* Every repo hook command is hookCommand(name): the installed `repo` bin, run as `repo hook <name>`.
|
|
19
|
+
* Every consumer script command is rootCmd(): the script in the project dir, else at its git top level.
|
|
20
|
+
*
|
|
21
|
+
* `bun repo sync` applies the union then checks it; `bun repo sync --check` runs the keep-list only.
|
|
22
|
+
*/
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { HOOK_SCRIPTS, hookCommand, hookNameOf, type HookName } from "./hook-command.ts";
|
|
26
|
+
import { stripJsonc } from "./jsonc.ts";
|
|
27
|
+
|
|
28
|
+
/** One hook command entry as this script writes it. */
|
|
29
|
+
interface HookEntry {
|
|
30
|
+
type: string;
|
|
31
|
+
command: string;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A `{matcher, hooks}` group as this script writes it. */
|
|
36
|
+
interface HookGroup {
|
|
37
|
+
matcher: string;
|
|
38
|
+
hooks: HookEntry[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A flat Antigravity entry: the consumer hook plus the event it joins. */
|
|
42
|
+
interface NamedHook {
|
|
43
|
+
event: string;
|
|
44
|
+
matcher?: string;
|
|
45
|
+
command: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type JsonObject = Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `workspaceRoot` is where the hook files are read and written. `sourceRoot` is the stock package whose
|
|
52
|
+
* `cli/scripts/` says which repo hooks ship. `consumerRoot` owns `scripts/` hooks, `scripts/git-hooks`
|
|
53
|
+
* and the path deny (`.repo/permissions.json`).
|
|
54
|
+
* Both default to `workspaceRoot`.
|
|
55
|
+
*/
|
|
56
|
+
export interface UnionOptions {
|
|
57
|
+
workspaceRoot?: string;
|
|
58
|
+
sourceRoot?: string;
|
|
59
|
+
consumerRoot?: string;
|
|
60
|
+
check?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The three roots one union run reads and writes. */
|
|
64
|
+
interface Roots {
|
|
65
|
+
ws: string;
|
|
66
|
+
src: string;
|
|
67
|
+
consumer: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface UnionResult {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
check: boolean;
|
|
73
|
+
error?: string;
|
|
74
|
+
failures?: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ROOT_PREFIX = 'd="${GROK_WORKSPACE_ROOT:-${CLAUDE_PROJECT_DIR:-.}}"; [ -e "$d/';
|
|
78
|
+
|
|
79
|
+
/** Hook command for `rel`: the script in the project dir, else at that dir's git top level, else in the hook cwd. */
|
|
80
|
+
export function rootCmd(rel: string, args?: string): string {
|
|
81
|
+
const cmd = `${ROOT_PREFIX}${rel}" ] || d="$(git -C "$d" rev-parse --show-toplevel 2>/dev/null || echo "$d")"; bun "$d/${rel}"`;
|
|
82
|
+
return args ? `${cmd} ${args}` : cmd;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const PULL: string = rootCmd("scripts/daily-pull-check.mjs");
|
|
86
|
+
const RTK: string = rootCmd("scripts/rtk-check.mjs");
|
|
87
|
+
const FLEET: string = rootCmd("scripts/daily-fleet-check.mjs");
|
|
88
|
+
export const BEHIND: string = rootCmd("scripts/check-behind.mjs", "--hook");
|
|
89
|
+
const VALIDATE: string = rootCmd("scripts/validate.mjs", "--hook");
|
|
90
|
+
const SIM: string = rootCmd("scripts/sim.mjs", "--hook");
|
|
91
|
+
export const MAP: string = hookCommand("session-map");
|
|
92
|
+
|
|
93
|
+
export const AUTORUN_NEEDLES: string[] = [
|
|
94
|
+
"daily-pull-check.mjs",
|
|
95
|
+
"rtk-check.mjs",
|
|
96
|
+
"daily-fleet-check.mjs",
|
|
97
|
+
"check-behind.mjs",
|
|
98
|
+
"validate.mjs",
|
|
99
|
+
"sim.mjs",
|
|
100
|
+
];
|
|
101
|
+
export const OWNED_HOOKS: readonly HookName[] = ["scrub-spawn-prompt", "gate-fat-tools"];
|
|
102
|
+
const GIT_HOOKS: string[] = [
|
|
103
|
+
"scripts/git-hooks/pre-commit",
|
|
104
|
+
"scripts/git-hooks/pre-commit.mjs",
|
|
105
|
+
"scripts/git-hooks/pre-push",
|
|
106
|
+
"scripts/git-hooks/pre-push.mjs",
|
|
107
|
+
];
|
|
108
|
+
export const POST_WRITE_MATCHER = "Write|Edit|write|search_replace";
|
|
109
|
+
/** Antigravity's write tools. Matchers the generator does not emit must use these names; it only translates its own. */
|
|
110
|
+
export const AGY_WRITE_MATCHER = "write_file|edit_file";
|
|
111
|
+
|
|
112
|
+
/** Cursor's write tool. Entries the generator does not emit must use its name; Cursor has no Edit tool. */
|
|
113
|
+
export const CURSOR_WRITE_MATCHER = "Write";
|
|
114
|
+
|
|
115
|
+
function isPlainObject(v: unknown): v is JsonObject {
|
|
116
|
+
return v != null && typeof v === "object" && !Array.isArray(v);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveRoots(opts: UnionOptions | string): Roots {
|
|
120
|
+
const given = (v: unknown): string | undefined => (typeof v === "string" && v ? v : undefined);
|
|
121
|
+
if (typeof opts === "string") return { ws: opts, src: opts, consumer: opts };
|
|
122
|
+
const ws = given(opts?.workspaceRoot) ?? process.cwd();
|
|
123
|
+
return { ws, src: given(opts?.sourceRoot) ?? ws, consumer: given(opts?.consumerRoot) ?? ws };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Consumer-owned extras live in the consumer's root `scripts/`. */
|
|
127
|
+
function hasScript(root: string, name: string): boolean {
|
|
128
|
+
return fs.existsSync(path.join(root, "scripts", name));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A repo hook ships while its script is copied into `cli/scripts/`. */
|
|
132
|
+
function ships(src: string, name: HookName): boolean {
|
|
133
|
+
return fs.existsSync(path.join(src, "cli", "scripts", HOOK_SCRIPTS[name]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function withoutWritePath(arr: unknown[]): string[] {
|
|
137
|
+
return arr.filter((x): x is string => typeof x === "string" && !x.startsWith("Write("));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadConsumerDeny({ consumer }: Roots): string[] {
|
|
141
|
+
const data = loadJson(path.join(consumer, ".repo", "permissions.json"));
|
|
142
|
+
return Array.isArray(data.deny) ? withoutWritePath(data.deny) : [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function presentAutorun(root: string): string[] {
|
|
146
|
+
return AUTORUN_NEEDLES.filter((n) => hasScript(root, n));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function hookCmd(command: string, timeout?: number): HookEntry {
|
|
150
|
+
const h: HookEntry = { type: "command", command };
|
|
151
|
+
if (timeout != null) h.timeout = timeout;
|
|
152
|
+
return h;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function commandsIn(group: unknown): string[] {
|
|
156
|
+
if (!isPlainObject(group)) return [];
|
|
157
|
+
const out: string[] = [];
|
|
158
|
+
if (typeof group.command === "string") out.push(group.command);
|
|
159
|
+
const hooks = Array.isArray(group.hooks) ? group.hooks : [];
|
|
160
|
+
for (const h of hooks) {
|
|
161
|
+
if (isPlainObject(h) && typeof h.command === "string") out.push(h.command);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** What makes two groups the same hook: the repo hook name in either command form, else the script file name. */
|
|
167
|
+
function needleOf(group: unknown): string {
|
|
168
|
+
for (const c of commandsIn(group)) {
|
|
169
|
+
const name = hookNameOf(c);
|
|
170
|
+
if (name) return `hook:${name}`;
|
|
171
|
+
const m = String(c).match(/([\w.-]+\.(?:ts|mjs|js)\b)/);
|
|
172
|
+
if (m) return m[1]!; // group 1 always participates in a match
|
|
173
|
+
}
|
|
174
|
+
return commandsIn(group)[0] || JSON.stringify(group);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function upsertGroups(existing: unknown, desired: unknown[]): unknown[] {
|
|
178
|
+
const prev = Array.isArray(existing) ? existing : [];
|
|
179
|
+
const desiredNeedles = new Set(desired.map(needleOf));
|
|
180
|
+
const extras = prev.filter((g) => !desiredNeedles.has(needleOf(g)));
|
|
181
|
+
return [...desired, ...extras];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function uniq<T>(arr: T[]): T[] {
|
|
185
|
+
const seen = new Set<T>();
|
|
186
|
+
const out: T[] = [];
|
|
187
|
+
for (const x of arr) {
|
|
188
|
+
if (seen.has(x)) continue;
|
|
189
|
+
seen.add(x);
|
|
190
|
+
out.push(x);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadJson(p: string): JsonObject {
|
|
196
|
+
if (!fs.existsSync(p)) return {};
|
|
197
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
198
|
+
if (!String(raw).trim()) return {};
|
|
199
|
+
const v: unknown = JSON.parse(raw);
|
|
200
|
+
return isPlainObject(v) ? v : {};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function saveJson(p: string, obj: unknown): void {
|
|
204
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
205
|
+
fs.writeFileSync(p, `${JSON.stringify(obj, null, 2)}\n`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function desiredPre(root: string, writeMatcher = "Write|Edit"): HookGroup[] {
|
|
209
|
+
return hasScript(root, "check-behind.mjs") ? [{ matcher: writeMatcher, hooks: [hookCmd(BEHIND)] }] : [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function mapHooks(src: string): HookName[] {
|
|
213
|
+
return ships(src, "session-map") ? ["session-map"] : [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** `map: false` for Antigravity, where the generator writes the map entry itself. */
|
|
217
|
+
function desiredSession(
|
|
218
|
+
{ src, consumer: root }: Roots,
|
|
219
|
+
{ map = true }: { map?: boolean } = {},
|
|
220
|
+
): { hooks: HookEntry[] }[] | null {
|
|
221
|
+
const hooks: HookEntry[] = [];
|
|
222
|
+
if (map && ships(src, "session-map")) hooks.push(hookCmd(MAP, 5));
|
|
223
|
+
if (hasScript(root, "daily-pull-check.mjs")) hooks.push(hookCmd(PULL));
|
|
224
|
+
if (hasScript(root, "rtk-check.mjs")) hooks.push(hookCmd(RTK));
|
|
225
|
+
if (hasScript(root, "daily-fleet-check.mjs")) hooks.push(hookCmd(FLEET));
|
|
226
|
+
return hooks.length ? [{ hooks }] : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function desiredPost(root: string, matcher = POST_WRITE_MATCHER): HookGroup[] | null {
|
|
230
|
+
const hooks: HookEntry[] = [];
|
|
231
|
+
if (hasScript(root, "validate.mjs")) hooks.push(hookCmd(VALIDATE));
|
|
232
|
+
if (hasScript(root, "sim.mjs")) hooks.push(hookCmd(SIM));
|
|
233
|
+
return hooks.length ? [{ matcher, hooks }] : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function consumerNamed(root: string): Record<string, NamedHook> {
|
|
237
|
+
const named: Record<string, NamedHook> = {};
|
|
238
|
+
if (hasScript(root, "daily-pull-check.mjs")) {
|
|
239
|
+
named["daily-pull-all"] = { event: "SessionStart", command: PULL };
|
|
240
|
+
}
|
|
241
|
+
if (hasScript(root, "rtk-check.mjs")) {
|
|
242
|
+
named["rtk-check"] = { event: "SessionStart", command: RTK };
|
|
243
|
+
}
|
|
244
|
+
if (hasScript(root, "daily-fleet-check.mjs")) {
|
|
245
|
+
named["daily-fleet-digest"] = { event: "SessionStart", command: FLEET };
|
|
246
|
+
}
|
|
247
|
+
if (hasScript(root, "check-behind.mjs")) {
|
|
248
|
+
named["check-behind"] = { event: "PreToolUse", matcher: AGY_WRITE_MATCHER, command: BEHIND };
|
|
249
|
+
}
|
|
250
|
+
if (hasScript(root, "validate.mjs")) {
|
|
251
|
+
named["validate-artifact"] = { event: "PostToolUse", matcher: AGY_WRITE_MATCHER, command: VALIDATE };
|
|
252
|
+
}
|
|
253
|
+
if (hasScript(root, "sim.mjs")) {
|
|
254
|
+
named["simulate-artifact"] = { event: "PostToolUse", matcher: AGY_WRITE_MATCHER, command: SIM };
|
|
255
|
+
}
|
|
256
|
+
return named;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function applyClaude(roots: Roots): void {
|
|
260
|
+
const root = roots.consumer;
|
|
261
|
+
const abs = path.join(roots.ws, ".claude/settings.json");
|
|
262
|
+
const live = loadJson(abs);
|
|
263
|
+
const hooks = isPlainObject(live.hooks) ? { ...live.hooks } : {};
|
|
264
|
+
hooks.PreToolUse = upsertGroups(hooks.PreToolUse, desiredPre(root));
|
|
265
|
+
const session = desiredSession(roots);
|
|
266
|
+
if (session) hooks.SessionStart = session;
|
|
267
|
+
const post = desiredPost(root);
|
|
268
|
+
if (post) hooks.PostToolUse = upsertGroups(hooks.PostToolUse, post);
|
|
269
|
+
const permissions = isPlainObject(live.permissions) ? { ...live.permissions } : {};
|
|
270
|
+
const liveDeny: unknown[] = Array.isArray(permissions.deny) ? permissions.deny : [];
|
|
271
|
+
permissions.deny = uniq([
|
|
272
|
+
...liveDeny.filter((x: unknown) => typeof x !== "string" || !x.startsWith("Write(")),
|
|
273
|
+
...loadConsumerDeny(roots),
|
|
274
|
+
]);
|
|
275
|
+
const next = { ...live, permissions, hooks };
|
|
276
|
+
saveJson(abs, next);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function applyCodex(roots: Roots): void {
|
|
280
|
+
const root = roots.consumer;
|
|
281
|
+
const abs = path.join(roots.ws, ".codex/hooks.json");
|
|
282
|
+
const live = loadJson(abs);
|
|
283
|
+
const liveHooks = isPlainObject(live.hooks) ? live.hooks : null;
|
|
284
|
+
const nested = liveHooks !== null;
|
|
285
|
+
const cur: JsonObject = liveHooks ? { ...liveHooks } : { ...live };
|
|
286
|
+
cur.PreToolUse = upsertGroups(cur.PreToolUse, desiredPre(root));
|
|
287
|
+
const session = desiredSession(roots);
|
|
288
|
+
if (session) cur.SessionStart = session;
|
|
289
|
+
const post = desiredPost(root);
|
|
290
|
+
if (post) {
|
|
291
|
+
const codexPost = post.map((g) => ({ hooks: g.hooks }));
|
|
292
|
+
cur.PostToolUse = upsertGroups(cur.PostToolUse, codexPost);
|
|
293
|
+
}
|
|
294
|
+
if (nested) saveJson(abs, { ...live, hooks: cur });
|
|
295
|
+
else saveJson(abs, { ...live, ...cur });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Antigravity rejects the `{matcher, hooks}` group form for SessionStart: flat entries, `extra` after what is there. */
|
|
299
|
+
function flatSession(existing: unknown, extra: HookEntry[]): unknown[] {
|
|
300
|
+
const prev = (Array.isArray(existing) ? existing : []).flatMap((e: unknown): unknown[] =>
|
|
301
|
+
isPlainObject(e) && Array.isArray(e.hooks) ? e.hooks : [e],
|
|
302
|
+
);
|
|
303
|
+
const added = new Set(extra.map((h) => h.command));
|
|
304
|
+
return [
|
|
305
|
+
...prev.filter((e) => !(isPlainObject(e) && typeof e.command === "string" && added.has(e.command))),
|
|
306
|
+
...extra,
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** `{matcher, hooks}` groups as the flat `{...hook, matcher}` entries Cursor reads. */
|
|
311
|
+
function flatEntries(groups: HookGroup[]): (HookEntry & { matcher: string })[] {
|
|
312
|
+
return groups.flatMap((g) => g.hooks.map((h) => ({ ...h, matcher: g.matcher })));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function applyCursor(roots: Roots): void {
|
|
316
|
+
const root = roots.consumer;
|
|
317
|
+
const abs = path.join(roots.ws, ".cursor/hooks.json");
|
|
318
|
+
const live = loadJson(abs);
|
|
319
|
+
const hooks = isPlainObject(live.hooks) ? { ...live.hooks } : {};
|
|
320
|
+
hooks.preToolUse = upsertGroups(hooks.preToolUse, flatEntries(desiredPre(root, CURSOR_WRITE_MATCHER)));
|
|
321
|
+
const session = desiredSession(roots, { map: false });
|
|
322
|
+
if (session) hooks.sessionStart = flatSession(hooks.sessionStart, session[0]!.hooks); // desiredSession yields one group or null;
|
|
323
|
+
const post = desiredPost(root, CURSOR_WRITE_MATCHER);
|
|
324
|
+
if (post) hooks.postToolUse = upsertGroups(hooks.postToolUse, flatEntries(post));
|
|
325
|
+
saveJson(abs, { version: 1, ...live, hooks });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function applyAgents(roots: Roots): void {
|
|
329
|
+
const root = roots.consumer;
|
|
330
|
+
const abs = path.join(roots.ws, ".agents/hooks.json");
|
|
331
|
+
const live = loadJson(abs);
|
|
332
|
+
const rulesync = isPlainObject(live.rulesync) ? { ...live.rulesync } : {};
|
|
333
|
+
rulesync.PreToolUse = upsertGroups(rulesync.PreToolUse, desiredPre(root, AGY_WRITE_MATCHER));
|
|
334
|
+
const session = desiredSession(roots, { map: false });
|
|
335
|
+
if (session) rulesync.SessionStart = flatSession(rulesync.SessionStart, session[0]!.hooks); // desiredSession yields one group or null;
|
|
336
|
+
const post = desiredPost(root, AGY_WRITE_MATCHER);
|
|
337
|
+
if (post) rulesync.PostToolUse = post;
|
|
338
|
+
saveJson(abs, { ...live, ...consumerNamed(root), rulesync });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function applyUnion(opts: UnionOptions | string = {}): UnionResult {
|
|
342
|
+
const roots = resolveRoots(opts);
|
|
343
|
+
const check = typeof opts === "object" && opts && opts.check === true;
|
|
344
|
+
if (check) {
|
|
345
|
+
const failures = checkUnion({ workspaceRoot: roots.ws, sourceRoot: roots.src, consumerRoot: roots.consumer });
|
|
346
|
+
if (failures.length) {
|
|
347
|
+
return { ok: false, check: true, error: `drift: ${failures.join("; ")}`, failures };
|
|
348
|
+
}
|
|
349
|
+
return { ok: true, check: true, failures: [] };
|
|
350
|
+
}
|
|
351
|
+
applyClaude(roots);
|
|
352
|
+
applyCodex(roots);
|
|
353
|
+
applyAgents(roots);
|
|
354
|
+
applyCursor(roots);
|
|
355
|
+
return { ok: true, check: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Hook files whose repo commands must be `hookCommand` and whose autorun commands must be `rootCmd` strings. */
|
|
359
|
+
const COMMAND_FILES: [string, string][] = [
|
|
360
|
+
["claude", ".claude/settings.json"],
|
|
361
|
+
["codex", ".codex/hooks.json"],
|
|
362
|
+
["agents", ".agents/hooks.json"],
|
|
363
|
+
["cursor", ".cursor/hooks.json"],
|
|
364
|
+
["rulesync", "agents/hooks.jsonc"],
|
|
365
|
+
["grok", ".grok/hooks/orchestrate-parent.json"],
|
|
366
|
+
["grok", ".grok/hooks/rtk.json"],
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
function allCommands(value: unknown, out: string[] = []): string[] {
|
|
370
|
+
if (Array.isArray(value)) for (const v of value) allCommands(v, out);
|
|
371
|
+
else if (isPlainObject(value)) {
|
|
372
|
+
for (const [k, v] of Object.entries(value)) {
|
|
373
|
+
if (k === "command" && typeof v === "string") out.push(v);
|
|
374
|
+
else allCommands(v, out);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isRootCmd(command: string): boolean {
|
|
381
|
+
if (!command.startsWith(ROOT_PREFIX)) return false;
|
|
382
|
+
const rel = command.slice(ROOT_PREFIX.length).split('"')[0]!; // split always yields one element
|
|
383
|
+
const exact = rootCmd(rel);
|
|
384
|
+
return command === exact || command.startsWith(`${exact} `);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function staleCommands(root: string): string[] {
|
|
388
|
+
const failures: string[] = [];
|
|
389
|
+
for (const [id, rel] of COMMAND_FILES) {
|
|
390
|
+
const p = path.join(root, rel);
|
|
391
|
+
if (!fs.existsSync(p)) continue;
|
|
392
|
+
const data: unknown = JSON.parse(stripJsonc(fs.readFileSync(p, "utf8")));
|
|
393
|
+
for (const command of allCommands(data)) {
|
|
394
|
+
const hook = hookNameOf(command);
|
|
395
|
+
if (hook) {
|
|
396
|
+
// The stock source may hold the short form, which sync expands; every generated file holds the expanded text.
|
|
397
|
+
const short = id === "rulesync" && command === `repo hook ${hook}`;
|
|
398
|
+
if (command !== hookCommand(hook) && !short) {
|
|
399
|
+
failures.push(
|
|
400
|
+
`${id}: ${hook} hook command is not the one bun repo sync writes for ${hook}: run bun repo sync`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const script = AUTORUN_NEEDLES.find((n) => command.includes(n));
|
|
406
|
+
if (script && !isRootCmd(command)) {
|
|
407
|
+
failures.push(`${id}: ${script} hook command does not find its script from the repo root: run bun repo sync`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return failures;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function checkUnion(opts: UnionOptions | string = {}): string[] {
|
|
415
|
+
const roots = resolveRoots(opts);
|
|
416
|
+
const root = roots.ws;
|
|
417
|
+
const failures: string[] = [];
|
|
418
|
+
const autorun = presentAutorun(roots.consumer);
|
|
419
|
+
const worktree: HookName[] = ships(roots.src, "worktree") ? ["worktree"] : [];
|
|
420
|
+
const files = [
|
|
421
|
+
{
|
|
422
|
+
id: "claude",
|
|
423
|
+
path: ".claude/settings.json",
|
|
424
|
+
hooks: [...OWNED_HOOKS, ...mapHooks(roots.src), ...worktree],
|
|
425
|
+
needles: [...autorun, ...loadConsumerDeny(roots)],
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: "codex",
|
|
429
|
+
path: ".codex/hooks.json",
|
|
430
|
+
hooks: [...OWNED_HOOKS, ...mapHooks(roots.src)],
|
|
431
|
+
needles: autorun,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: "agents",
|
|
435
|
+
path: ".agents/hooks.json",
|
|
436
|
+
hooks: OWNED_HOOKS,
|
|
437
|
+
needles: autorun,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "cursor",
|
|
441
|
+
path: ".cursor/hooks.json",
|
|
442
|
+
hooks: OWNED_HOOKS,
|
|
443
|
+
needles: autorun,
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
failures.push(...staleCommands(root));
|
|
447
|
+
for (const spec of files) {
|
|
448
|
+
const p = path.join(root, spec.path);
|
|
449
|
+
if (!fs.existsSync(p)) {
|
|
450
|
+
failures.push(`${spec.id}: missing ${spec.path}`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const text = fs.readFileSync(p, "utf8");
|
|
454
|
+
const present = new Set(allCommands(JSON.parse(text)).map(hookNameOf));
|
|
455
|
+
for (const h of spec.hooks) {
|
|
456
|
+
if (!present.has(h)) failures.push(`${spec.id}: missing ${h}`);
|
|
457
|
+
}
|
|
458
|
+
for (const n of spec.needles) {
|
|
459
|
+
if (!text.includes(n)) failures.push(`${spec.id}: missing ${n}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const gitDir = path.join(roots.consumer, "scripts/git-hooks");
|
|
463
|
+
if (fs.existsSync(gitDir)) {
|
|
464
|
+
for (const rel of GIT_HOOKS) {
|
|
465
|
+
if (!fs.existsSync(path.join(roots.consumer, rel))) failures.push(`git-hook missing: ${rel}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return failures;
|
|
469
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readEnv } from "../env.ts";
|
|
2
|
+
import type { Env } from "../env.ts";
|
|
3
|
+
/** TTY / CI / --yes probe. Copied into consumers; no clack. */
|
|
4
|
+
|
|
5
|
+
interface InteractiveOptions {
|
|
6
|
+
argv?: string[];
|
|
7
|
+
env?: Env;
|
|
8
|
+
stdin?: { isTTY?: boolean };
|
|
9
|
+
stdout?: { isTTY?: boolean };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True only when both streams are TTYs, CI is unset or falsy, and no `--yes`/`-y`/`--non-interactive` flag is present.
|
|
14
|
+
* @param [options]
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
export function isInteractive({
|
|
18
|
+
argv = process.argv.slice(2),
|
|
19
|
+
env = readEnv(),
|
|
20
|
+
stdin = process.stdin,
|
|
21
|
+
stdout = process.stdout,
|
|
22
|
+
}: InteractiveOptions = {}): boolean {
|
|
23
|
+
if (!stdin?.isTTY || !stdout?.isTTY) return false;
|
|
24
|
+
const ci = env.CI;
|
|
25
|
+
if (ci != null && ci !== "" && ci !== "0" && ci !== "false") return false;
|
|
26
|
+
for (const a of argv) {
|
|
27
|
+
if (a === "--yes" || a === "-y" || a === "--non-interactive") return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|