@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,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readGoldens, renderFixtureTrees } from "../../dev/capture-tree-goldens.ts";
|
|
3
|
+
|
|
4
|
+
// One render spawns rulesync three times, about 2.5s each on a CI runner.
|
|
5
|
+
const RENDER_TIMEOUT = 60_000;
|
|
6
|
+
|
|
7
|
+
// Regenerate only with `bun dev/capture-tree-goldens.ts`; a diff here is a byte a host would read differently.
|
|
8
|
+
describe("agent trees of the consumer fixture over the frozen stock", { timeout: RENDER_TIMEOUT }, () => {
|
|
9
|
+
it("render exactly the golden files, byte for byte", async () => {
|
|
10
|
+
const rendered = await renderFixtureTrees();
|
|
11
|
+
const goldens = readGoldens();
|
|
12
|
+
expect([...rendered.keys()]).toEqual([...goldens.keys()]);
|
|
13
|
+
for (const [rel, content] of goldens) expect(rendered.get(rel), rel).toBe(content);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, expect, it } from "vitest";
|
|
6
|
+
import { markHooksExecutable } from "./standards.ts";
|
|
7
|
+
|
|
8
|
+
const dirs: string[] = [];
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const git = (cwd: string, ...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" });
|
|
14
|
+
/** What the user's own `git add -A` does to the temp repo. */
|
|
15
|
+
const stageAll = (root: string) => git(root, "add", "-A");
|
|
16
|
+
|
|
17
|
+
/** A repo like a Windows checkout: git cannot see file modes, so a new file is added as 100644. */
|
|
18
|
+
function windowsLikeRepo(): string {
|
|
19
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "hooks-mode-")));
|
|
20
|
+
dirs.push(root);
|
|
21
|
+
git(root, "init", "-q", "-b", "main");
|
|
22
|
+
git(root, "config", "core.fileMode", "false");
|
|
23
|
+
fs.mkdirSync(path.join(root, ".githooks"));
|
|
24
|
+
for (const name of ["commit-msg", "pre-commit"]) fs.writeFileSync(path.join(root, ".githooks", name), "#!/bin/sh\n");
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const modes = (root: string) => git(root, "ls-files", "-s", ".githooks").replaceAll(/ \w+ 0/g, "");
|
|
29
|
+
|
|
30
|
+
it("on win32 marks the hooks 100755 in the index, and a later git add -A keeps it", () => {
|
|
31
|
+
const root = windowsLikeRepo();
|
|
32
|
+
const result = markHooksExecutable(root, "win32");
|
|
33
|
+
expect(result.warnings).toEqual([]);
|
|
34
|
+
expect(result.done).toHaveLength(1);
|
|
35
|
+
stageAll(root);
|
|
36
|
+
expect(modes(root)).toBe("100755\t.githooks/commit-msg\n100755\t.githooks/pre-commit\n");
|
|
37
|
+
expect(markHooksExecutable(root, "win32")).toEqual({ done: [], warnings: [] });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("on win32 fixes a hook already tracked as 100644 without restaging its content", () => {
|
|
41
|
+
const root = windowsLikeRepo();
|
|
42
|
+
stageAll(root);
|
|
43
|
+
expect(modes(root)).toBe("100644\t.githooks/commit-msg\n100644\t.githooks/pre-commit\n");
|
|
44
|
+
fs.writeFileSync(path.join(root, ".githooks", "pre-commit"), "#!/bin/sh\nedited\n");
|
|
45
|
+
markHooksExecutable(root, "win32");
|
|
46
|
+
expect(modes(root)).toBe("100755\t.githooks/commit-msg\n100755\t.githooks/pre-commit\n");
|
|
47
|
+
expect(git(root, "diff", "--name-only")).toBe(".githooks/pre-commit\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does nothing off win32", () => {
|
|
51
|
+
const root = windowsLikeRepo();
|
|
52
|
+
expect(markHooksExecutable(root, "linux")).toEqual({ done: [], warnings: [] });
|
|
53
|
+
expect(git(root, "ls-files", ".githooks")).toBe("");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
type Reply = { status: number | null; stdout: string; stderr: string };
|
|
57
|
+
const reply = (status: number | null, stdout = "", stderr = ""): Reply => ({ status, stdout, stderr });
|
|
58
|
+
|
|
59
|
+
/** A git runner answering `answers` by the first argument, recording each call's arguments. */
|
|
60
|
+
function fakeGit(root: string, answers: Record<string, Reply>) {
|
|
61
|
+
const calls: string[][] = [];
|
|
62
|
+
const run = (_cwd: string, args: readonly string[]): Reply => {
|
|
63
|
+
calls.push([...args]);
|
|
64
|
+
return answers[args[0] ?? ""] ?? reply(0, `${root}\n`);
|
|
65
|
+
};
|
|
66
|
+
return { run, calls };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("does nothing when git cannot name the top level, names another one, or cannot list the hooks", () => {
|
|
70
|
+
const root = windowsLikeRepo();
|
|
71
|
+
const broken = fakeGit(root, { "rev-parse": reply(128, "", "not a repo") });
|
|
72
|
+
expect(markHooksExecutable(root, "win32", broken.run)).toEqual({ done: [], warnings: [] });
|
|
73
|
+
const elsewhere = fakeGit(root, { "rev-parse": reply(0, `${os.tmpdir()}\n`) });
|
|
74
|
+
expect(markHooksExecutable(root, "win32", elsewhere.run)).toEqual({ done: [], warnings: [] });
|
|
75
|
+
const unlisted = fakeGit(root, { "ls-files": reply(1) });
|
|
76
|
+
expect(markHooksExecutable(root, "win32", unlisted.run)).toEqual({ done: [], warnings: [] });
|
|
77
|
+
expect(unlisted.calls.some((args) => args[0] === "update-index")).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("stages a hook the index lacks with --add, one it holds by its staged blob, and skips a missing file", () => {
|
|
81
|
+
const root = windowsLikeRepo();
|
|
82
|
+
fs.rmSync(path.join(root, ".githooks", "pre-commit"));
|
|
83
|
+
const fake = fakeGit(root, { "ls-files": reply(0, "garbage\n100644 abc123 0\t.githooks/other\n") });
|
|
84
|
+
const first = markHooksExecutable(root, "win32", fake.run);
|
|
85
|
+
expect(first.done).toEqual(["marked .githooks/commit-msg executable in the index"]);
|
|
86
|
+
expect(fake.calls.at(-1)).toEqual(["update-index", "--add", "--chmod=+x", "--", ".githooks/commit-msg"]);
|
|
87
|
+
const tracked = fakeGit(root, { "ls-files": reply(0, "100644 abc123 0\t.githooks/commit-msg\n") });
|
|
88
|
+
markHooksExecutable(root, "win32", tracked.run);
|
|
89
|
+
expect(tracked.calls.at(-1)).toEqual(["update-index", "--cacheinfo", "100755,abc123,.githooks/commit-msg"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("warns with git's reason when it cannot mark a hook", () => {
|
|
93
|
+
const root = windowsLikeRepo();
|
|
94
|
+
const fake = fakeGit(root, { "update-index": reply(128, "", "index.lock exists\n") });
|
|
95
|
+
expect(markHooksExecutable(root, "win32", fake.run)).toEqual({
|
|
96
|
+
done: [],
|
|
97
|
+
warnings: ["cannot mark the git hooks executable: index.lock exists"],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Host } from "../config.ts";
|
|
2
|
+
|
|
3
|
+
/** What one host owns: the root-relative path prefixes its tree is written under. */
|
|
4
|
+
export type HostTree = { owns: readonly string[] };
|
|
5
|
+
|
|
6
|
+
/** The one table of which host owns which dirs; trees, the git files and prune all read it. */
|
|
7
|
+
export const HOST_TREES: Record<Host, HostTree> = {
|
|
8
|
+
claude: { owns: [".claude/"] },
|
|
9
|
+
codex: { owns: [".codex/", ".agents/skills/"] },
|
|
10
|
+
antigravity: { owns: [".agents/"] },
|
|
11
|
+
grok: { owns: [".grok/"] },
|
|
12
|
+
cursor: { owns: [".cursor/"] },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** The agent instructions file; every host reads it. */
|
|
16
|
+
const AGENTS_FILE = "AGENTS.md";
|
|
17
|
+
/** The file a package keeps for claude, which holds only the pointer line. */
|
|
18
|
+
const CLAUDE_FILE = "CLAUDE.md";
|
|
19
|
+
/** The MCP server list Claude reads. */
|
|
20
|
+
const MCP_FILE = ".mcp.json";
|
|
21
|
+
|
|
22
|
+
/** Whether a generated root-relative path (a package's `AGENTS.md` and `CLAUDE.md` included) belongs to one of `hosts`; any other path is not a host output. */
|
|
23
|
+
export function isHostOutput(hosts: readonly Host[], rel: string): boolean {
|
|
24
|
+
if (rel === AGENTS_FILE || rel.endsWith(`/${AGENTS_FILE}`)) return hosts.length > 0;
|
|
25
|
+
if (rel.endsWith(`/${CLAUDE_FILE}`)) return hosts.includes("claude");
|
|
26
|
+
if (rel === MCP_FILE) return hosts.includes("claude");
|
|
27
|
+
return hosts.some((host) => HOST_TREES[host].owns.some((prefix) => rel.startsWith(prefix)));
|
|
28
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { KnipIgnore } from "../knip-schema.ts";
|
|
7
|
+
import { KNIP_CONFIG_PATH, KNIP_UNIGNORED_GLOB, knipConfig } from "./knip-config.ts";
|
|
8
|
+
import type { PackageJson, Workspace } from "./workspace.ts";
|
|
9
|
+
|
|
10
|
+
const temps: string[] = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** A temp root holding the empty `files`, and a workspace of its manifest and packages. */
|
|
16
|
+
function workspace(manifest: PackageJson, packages: Workspace["packages"], files: string[] = []): Workspace {
|
|
17
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-knip-config-"));
|
|
18
|
+
temps.push(root);
|
|
19
|
+
for (const rel of files) {
|
|
20
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(root, rel), "");
|
|
22
|
+
}
|
|
23
|
+
return { root, manifest, packages };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const generated = z.looseObject({ workspaces: z.record(z.string(), z.unknown()) });
|
|
27
|
+
/** The generated config, parsed. */
|
|
28
|
+
const parsed = (ws: Workspace, ignores: readonly KnipIgnore[] = []) =>
|
|
29
|
+
generated.parse(JSON.parse(knipConfig(ws, ignores)));
|
|
30
|
+
|
|
31
|
+
const SCHEMA = "https://unpkg.com/knip@6/schema.json";
|
|
32
|
+
const FIXTURES = ["**/fixtures/**"];
|
|
33
|
+
const DEFAULT_ENTRIES = [
|
|
34
|
+
"{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
|
|
35
|
+
"src/{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
|
|
36
|
+
];
|
|
37
|
+
const RULES = [
|
|
38
|
+
...["files", "dependencies", "devDependencies", "optionalPeerDependencies", "unlisted", "binaries"],
|
|
39
|
+
...["unresolved", "exports", "nsExports", "types", "nsTypes", "enumMembers", "namespaceMembers"],
|
|
40
|
+
...["duplicates", "catalog", "catalogReferences", "cycles"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("the generated knip config: shape", () => {
|
|
44
|
+
it("names its path and the unignored copies it makes", () => {
|
|
45
|
+
expect([KNIP_CONFIG_PATH, KNIP_UNIGNORED_GLOB]).toEqual(["knip.json", "knip.unignored-*.json"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("points at the schema and turns every rule to error", () => {
|
|
49
|
+
expect(parsed(workspace({}, []))).toMatchObject({
|
|
50
|
+
$schema: SCHEMA,
|
|
51
|
+
rules: Object.fromEntries(RULES.map((rule) => [rule, "error"])),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is one root workspace for a single package, with workerstack.config.ts as an entry and fixtures ignored", () => {
|
|
56
|
+
expect(parsed(workspace({}, []))).toMatchObject({
|
|
57
|
+
workspaces: {
|
|
58
|
+
".": { entry: [...DEFAULT_ENTRIES, "workerstack.config.ts"], ignore: FIXTURES, ignoreBinaries: [] },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("adds a workspace per package dir, without workerstack.config.ts, and ends with a newline", () => {
|
|
64
|
+
const ws = workspace({}, [
|
|
65
|
+
{ dir: "packages/api", manifest: {} },
|
|
66
|
+
{ dir: "packages/web", manifest: {} },
|
|
67
|
+
]);
|
|
68
|
+
const { workspaces } = parsed(ws);
|
|
69
|
+
expect(Object.keys(workspaces)).toEqual([".", "packages/api", "packages/web"]);
|
|
70
|
+
expect(workspaces).toMatchObject({
|
|
71
|
+
"packages/api": { entry: DEFAULT_ENTRIES, ignore: FIXTURES, ignoreBinaries: [] },
|
|
72
|
+
});
|
|
73
|
+
expect(knipConfig(ws, []).endsWith("}\n")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("the generated knip config: vitest", () => {
|
|
78
|
+
it("makes every vitest config present an entry and a vitest config of its workspace", () => {
|
|
79
|
+
const ws = workspace(
|
|
80
|
+
{},
|
|
81
|
+
[{ dir: "packages/api", manifest: {} }],
|
|
82
|
+
["vitest.config.ts", "vitest.e2e.config.ts", "vite.config.ts", "packages/api/vitest.unit.config.mts"],
|
|
83
|
+
);
|
|
84
|
+
expect(parsed(ws)).toMatchObject({
|
|
85
|
+
workspaces: {
|
|
86
|
+
".": {
|
|
87
|
+
entry: [...DEFAULT_ENTRIES, "workerstack.config.ts", "vitest.config.ts", "vitest.e2e.config.ts"],
|
|
88
|
+
vitest: { config: ["vitest.config.ts", "vitest.e2e.config.ts"] },
|
|
89
|
+
},
|
|
90
|
+
"packages/api": {
|
|
91
|
+
entry: [...DEFAULT_ENTRIES, "vitest.unit.config.mts"],
|
|
92
|
+
vitest: { config: ["vitest.unit.config.mts"] },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("leaves the vitest plugin alone where no vitest config is", () => {
|
|
99
|
+
expect(parsed(workspace({}, []))).not.toHaveProperty(["workspaces", ".", "vitest"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("the generated knip config: bins", () => {
|
|
104
|
+
it("ignores the binaries a workspace's own manifest declares, as a map or a path, and no other's", () => {
|
|
105
|
+
const ws = workspace({ bin: { repo: "./bin/repo.ts", other: "./bin/o.ts" } }, [
|
|
106
|
+
{ dir: "packages/a", manifest: { name: "@scope/a", bin: "./bin.ts" } },
|
|
107
|
+
{ dir: "packages/b", manifest: { bin: "./bin.ts" } },
|
|
108
|
+
{ dir: "packages/c", manifest: { name: "c" } },
|
|
109
|
+
]);
|
|
110
|
+
expect(parsed(ws)).toMatchObject({
|
|
111
|
+
workspaces: {
|
|
112
|
+
".": { ignoreBinaries: ["repo", "other"] },
|
|
113
|
+
"packages/a": { ignoreBinaries: ["a"] },
|
|
114
|
+
"packages/b": { ignoreBinaries: [] },
|
|
115
|
+
"packages/c": { ignoreBinaries: [] },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("the generated knip config: ignores", () => {
|
|
122
|
+
it("maps ignores to ignoreFiles, ignoreDependencies and ignoreBinaries on the root alone", () => {
|
|
123
|
+
const ws = workspace({ bin: { repo: "./bin/repo.ts" } }, [{ dir: "packages/a", manifest: {} }]);
|
|
124
|
+
const config = parsed(ws, [
|
|
125
|
+
{ file: "src/old.ts", reason: "loaded by name" },
|
|
126
|
+
{ dependency: "oxlint", reason: "spawned by path" },
|
|
127
|
+
{ binary: "tsc", reason: "run by a hook" },
|
|
128
|
+
]);
|
|
129
|
+
expect(config).toMatchObject({
|
|
130
|
+
workspaces: {
|
|
131
|
+
".": {
|
|
132
|
+
ignoreFiles: ["src/old.ts"],
|
|
133
|
+
ignoreDependencies: ["@workerstack/repo", "oxlint"],
|
|
134
|
+
ignoreBinaries: ["repo", "tsc"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
expect(config).not.toHaveProperty(["workspaces", "packages/a", "ignoreFiles"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("writes no ignoreFiles for no ignores, and always ignores this package's own name as a dependency", () => {
|
|
142
|
+
const config = parsed(workspace({}, [{ dir: "packages/a", manifest: {} }]));
|
|
143
|
+
expect(config).not.toHaveProperty(["workspaces", ".", "ignoreFiles"]);
|
|
144
|
+
expect(config).toMatchObject({ workspaces: { ".": { ignoreDependencies: ["@workerstack/repo"] } } });
|
|
145
|
+
expect(config).not.toHaveProperty(["workspaces", "packages/a", "ignoreDependencies"]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_FILE } from "../config.ts";
|
|
4
|
+
import type { KnipIgnore } from "../knip-schema.ts";
|
|
5
|
+
import { isRecord } from "../lint/manifest.ts";
|
|
6
|
+
import { KNIP_JSON_INDENT, KNIP_SCHEMA_URL } from "./knip-config.constants.ts";
|
|
7
|
+
import { FIXTURES } from "./oxlint-config.ts";
|
|
8
|
+
import { PACKAGE_NAME } from "./stock.ts";
|
|
9
|
+
import type { PackageJson, Workspace, WorkspacePackage } from "./workspace.ts";
|
|
10
|
+
|
|
11
|
+
/** The generated config sync writes at the root. */
|
|
12
|
+
export const KNIP_CONFIG_PATH = "knip.json";
|
|
13
|
+
/** The ignore-free copies `repo check` writes beside it for one run, each named for its process. */
|
|
14
|
+
export const KNIP_UNIGNORED_GLOB = "knip.unignored-*.json";
|
|
15
|
+
const ROOT = ".";
|
|
16
|
+
const VITEST_CONFIGS = "vitest*.config.*";
|
|
17
|
+
|
|
18
|
+
/** Every issue type knip 6.43.0 reports: each is an error, so none passes unseen. */
|
|
19
|
+
const RULES = [
|
|
20
|
+
"files",
|
|
21
|
+
"dependencies",
|
|
22
|
+
"devDependencies",
|
|
23
|
+
"optionalPeerDependencies",
|
|
24
|
+
"unlisted",
|
|
25
|
+
"binaries",
|
|
26
|
+
"unresolved",
|
|
27
|
+
"exports",
|
|
28
|
+
"nsExports",
|
|
29
|
+
"types",
|
|
30
|
+
"nsTypes",
|
|
31
|
+
"enumMembers",
|
|
32
|
+
"namespaceMembers",
|
|
33
|
+
"duplicates",
|
|
34
|
+
"catalog",
|
|
35
|
+
"catalogReferences",
|
|
36
|
+
"cycles",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/** The command names a manifest's `bin` makes: its keys, or the unscoped package name when `bin` is one path. */
|
|
40
|
+
function binNames(manifest: PackageJson): string[] {
|
|
41
|
+
if (isRecord(manifest.bin)) return Object.keys(manifest.bin);
|
|
42
|
+
if (typeof manifest.bin !== "string" || manifest.name === undefined) return [];
|
|
43
|
+
return [path.posix.basename(manifest.name)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The `vitest*.config.*` files directly in `dir`, sorted. */
|
|
47
|
+
function vitestConfigs(root: string, dir: string): string[] {
|
|
48
|
+
return fs.globSync(VITEST_CONFIGS, { cwd: path.join(root, dir) }).sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Knip's own default entries: an explicit `entry` replaces them, so every workspace lists them again. */
|
|
52
|
+
const DEFAULT_ENTRIES = [
|
|
53
|
+
"{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
|
|
54
|
+
"src/{index,cli,main}.{js,mjs,cjs,jsx,ts,tsx,mts,cts}!",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
type WorkspaceConfig = {
|
|
58
|
+
entry: string[];
|
|
59
|
+
vitest?: { config: string[] };
|
|
60
|
+
ignore: string[];
|
|
61
|
+
ignoreBinaries: string[];
|
|
62
|
+
ignoreFiles?: string[];
|
|
63
|
+
ignoreDependencies?: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The entry, vitest and bin settings of one workspace; `extraEntry` is what only its kind adds. */
|
|
67
|
+
function workspaceConfig(root: string, pkg: WorkspacePackage, extraEntry: string[]): WorkspaceConfig {
|
|
68
|
+
const { dir, manifest } = pkg;
|
|
69
|
+
const configs = vitestConfigs(root, dir);
|
|
70
|
+
return {
|
|
71
|
+
entry: [...DEFAULT_ENTRIES, ...extraEntry, ...configs],
|
|
72
|
+
...(configs.length > 0 ? { vitest: { config: configs } } : {}),
|
|
73
|
+
ignore: [FIXTURES],
|
|
74
|
+
ignoreBinaries: binNames(manifest),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The root workspace: its own settings and the `ignores`, each as the list knip takes for its kind. */
|
|
79
|
+
function rootConfig(workspace: Workspace, ignores: readonly KnipIgnore[]): WorkspaceConfig {
|
|
80
|
+
const base = workspaceConfig(workspace.root, { dir: ROOT, manifest: workspace.manifest }, [CONFIG_FILE]);
|
|
81
|
+
const files = ignores.flatMap((ignore) => ("file" in ignore ? [ignore.file] : []));
|
|
82
|
+
const dependencies = ignores.flatMap((ignore) => ("dependency" in ignore ? [ignore.dependency] : []));
|
|
83
|
+
const binaries = ignores.flatMap((ignore) => ("binary" in ignore ? [ignore.binary] : []));
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
ignoreBinaries: [...base.ignoreBinaries, ...binaries],
|
|
87
|
+
...(files.length > 0 ? { ignoreFiles: files } : {}),
|
|
88
|
+
// The generated git hooks run this package's bin, which knip cannot see.
|
|
89
|
+
ignoreDependencies: [PACKAGE_NAME, ...dependencies],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The knip config of a workspace: every rule an error, each package dir a workspace of knip's default entries and its own (the root also
|
|
95
|
+
* `workerstack.config.ts`), the vitest configs present read by knip's vitest plugin, fixtures ignored, a workspace's
|
|
96
|
+
* own bins not reported, and the root carrying `ignores`. `repo check` passes none for its ignore-free run.
|
|
97
|
+
*/
|
|
98
|
+
export function knipConfig(workspace: Workspace, ignores: readonly KnipIgnore[]): string {
|
|
99
|
+
const config = {
|
|
100
|
+
$schema: KNIP_SCHEMA_URL,
|
|
101
|
+
rules: Object.fromEntries(RULES.map((rule) => [rule, "error"])),
|
|
102
|
+
workspaces: {
|
|
103
|
+
[ROOT]: rootConfig(workspace, ignores),
|
|
104
|
+
...Object.fromEntries(workspace.packages.map((pkg) => [pkg.dir, workspaceConfig(workspace.root, pkg, [])])),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
return `${JSON.stringify(config, null, KNIP_JSON_INDENT)}\n`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { MANIFEST_REL, normalizeRel, prunePaths, readManifest, renderManifest } from "./manifest.ts";
|
|
6
|
+
|
|
7
|
+
const temps: string[] = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function rootWith(manifest: string | null): string {
|
|
13
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-manifest-"));
|
|
14
|
+
temps.push(root);
|
|
15
|
+
if (manifest !== null) {
|
|
16
|
+
fs.mkdirSync(path.join(root, ".repo/tmp"), { recursive: true });
|
|
17
|
+
fs.writeFileSync(path.join(root, MANIFEST_REL), manifest);
|
|
18
|
+
}
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("readManifest", () => {
|
|
23
|
+
it("reads a valid manifest", () => {
|
|
24
|
+
expect(readManifest(rootWith('{"version":1,"paths":["a","b/c"]}'))).toEqual({ version: 1, paths: ["a", "b/c"] });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each([
|
|
28
|
+
null,
|
|
29
|
+
"{",
|
|
30
|
+
"null",
|
|
31
|
+
"[]",
|
|
32
|
+
'{"version":2,"paths":[]}',
|
|
33
|
+
'{"version":1,"paths":"a"}',
|
|
34
|
+
'{"version":1,"paths":[1]}',
|
|
35
|
+
])("reads %j as empty", (text) => {
|
|
36
|
+
expect(readManifest(rootWith(text))).toEqual({ version: 1, paths: [] });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("renderManifest", () => {
|
|
41
|
+
it("sorts and dedupes so equal sets give equal bytes", () => {
|
|
42
|
+
expect(renderManifest(["b", "a", "b"])).toBe(renderManifest(["a", "b"]));
|
|
43
|
+
expect(JSON.parse(renderManifest(["b", "a"]))).toEqual({ version: 1, paths: ["a", "b"] });
|
|
44
|
+
expect(renderManifest([]).endsWith("\n")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("normalizeRel", () => {
|
|
49
|
+
it.each([
|
|
50
|
+
["a/b", "/r", "a/b"],
|
|
51
|
+
["a\\b\\c", "/r", "a/b/c"],
|
|
52
|
+
[".\\a//b/./c", "/r", "a/b/c"],
|
|
53
|
+
["/r/a/b", "/r", "a/b"],
|
|
54
|
+
["\\r\\a\\b", "/r", "a/b"],
|
|
55
|
+
["C:\\Users\\x\\repo\\.claude\\a", "C:\\Users\\x\\repo", ".claude/a"],
|
|
56
|
+
["c:/users/X/REPO/a", "C:\\Users\\x\\repo\\", "a"],
|
|
57
|
+
["/R/a", "/r", null],
|
|
58
|
+
["/other/a", "/r", null],
|
|
59
|
+
["C:\\other\\a", "C:\\Users\\x\\repo", null],
|
|
60
|
+
["C:\\Users\\x\\repo", "C:\\Users\\x\\repo", null],
|
|
61
|
+
["../a", "/r", null],
|
|
62
|
+
["a\\..\\..\\b", "/r", null],
|
|
63
|
+
["", "/r", null],
|
|
64
|
+
["./", "/r", null],
|
|
65
|
+
])("%j under %j is %j", (raw, root, expected) => {
|
|
66
|
+
expect(normalizeRel(raw, root)).toBe(expected);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("prunePaths", () => {
|
|
71
|
+
it("returns the sorted, normalized paths no output names", () => {
|
|
72
|
+
const prev = {
|
|
73
|
+
version: 1 as const,
|
|
74
|
+
paths: ["b", ".\\a\\x", "C:\\repo\\a\\y", "C:\\repo\\keep", "keep", "../out", "C:\\else\\z"],
|
|
75
|
+
};
|
|
76
|
+
expect(prunePaths(prev, ["keep"], "C:\\repo")).toEqual(["a/x", "a/y", "b"]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/** The root-relative paths sync wrote last run, tracked or not; only these are ever pruned. */
|
|
5
|
+
export const MANIFEST_REL = ".repo/tmp/sync.json";
|
|
6
|
+
|
|
7
|
+
export type Manifest = { version: 1; paths: string[] };
|
|
8
|
+
|
|
9
|
+
/** The manifest on disk; a missing, unreadable or invalid one reads as empty, so nothing is pruned. */
|
|
10
|
+
export function readManifest(root: string): Manifest {
|
|
11
|
+
try {
|
|
12
|
+
const data: unknown = JSON.parse(fs.readFileSync(path.join(root, MANIFEST_REL), "utf8"));
|
|
13
|
+
if (typeof data === "object" && data !== null) {
|
|
14
|
+
const { version, paths } = data as { version?: unknown; paths?: unknown };
|
|
15
|
+
if (version === 1 && Array.isArray(paths) && paths.every((entry) => typeof entry === "string")) {
|
|
16
|
+
return { version: 1, paths: paths as string[] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// falls through to the empty manifest
|
|
21
|
+
}
|
|
22
|
+
return { version: 1, paths: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The manifest file text for `paths`: deduped and sorted, so equal sets give equal bytes. */
|
|
26
|
+
export function renderManifest(paths: readonly string[]): string {
|
|
27
|
+
const manifest: Manifest = { version: 1, paths: [...new Set(paths)].sort() };
|
|
28
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One root-relative POSIX spelling of `raw`: backslashes become `/`, `./` and doubled separators go, and an
|
|
33
|
+
* absolute path under `root` (either spelling, drive letter case-insensitive) is made relative. Null for a path
|
|
34
|
+
* that is empty, outside `root` or climbs with `..`, so such a path is never matched, written or pruned.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeRel(raw: string, root: string): string | null {
|
|
37
|
+
const slash = (text: string) => text.replaceAll("\\", "/").replace(/\/+/g, "/");
|
|
38
|
+
let rel = slash(raw);
|
|
39
|
+
if (/^[A-Za-z]:\//.test(rel) || rel.startsWith("/")) {
|
|
40
|
+
const base = `${slash(root).replace(/\/$/, "")}/`;
|
|
41
|
+
const drive = /^[A-Za-z]:/.test(base);
|
|
42
|
+
const head = rel.slice(0, base.length);
|
|
43
|
+
if (drive ? head.toLowerCase() !== base.toLowerCase() : head !== base) return null;
|
|
44
|
+
rel = rel.slice(base.length);
|
|
45
|
+
}
|
|
46
|
+
const parts = rel.split("/").filter((part) => part !== "" && part !== ".");
|
|
47
|
+
if (parts.length === 0 || parts.includes("..")) return null;
|
|
48
|
+
return parts.join("/");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The normalized paths of `prev` that no output names; `outputPaths` must already be normalized. */
|
|
52
|
+
export function prunePaths(prev: Manifest, outputPaths: Iterable<string>, root: string): string[] {
|
|
53
|
+
const keep = new Set(outputPaths);
|
|
54
|
+
const stale = new Set<string>();
|
|
55
|
+
for (const raw of prev.paths) {
|
|
56
|
+
const rel = normalizeRel(raw, root);
|
|
57
|
+
if (rel !== null && !keep.has(rel)) stale.add(rel);
|
|
58
|
+
}
|
|
59
|
+
return [...stale].sort();
|
|
60
|
+
}
|