@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,280 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { SKILL_PACKS } from "../config.ts";
|
|
5
|
+
import { emitAgentShellTools } from "../scripts/emit-agent-shell-tools.ts";
|
|
6
|
+
import { emitMcpPolicy } from "../scripts/emit-mcp-policy.ts";
|
|
7
|
+
import { expandHookCommand } from "../scripts/hook-command.ts";
|
|
8
|
+
import { applyUnion } from "../scripts/hooks-union.ts";
|
|
9
|
+
import { WORKSPACE_SKILLS_REL, STOCK_SKILLS_REL, listSkillDirNames } from "../scripts/skill-conflicts.ts";
|
|
10
|
+
import { renderTasks } from "../scripts/vscode-tasks.ts";
|
|
11
|
+
import { renderHooks } from "./agents/hooks.ts";
|
|
12
|
+
import { renderRules, type RuleFile } from "./agents/rules.ts";
|
|
13
|
+
import { loadSources, type Sources } from "./agents/sources.ts";
|
|
14
|
+
import { renderSubagents } from "./agents/subagents.ts";
|
|
15
|
+
import { listedFiles } from "./git-files.ts";
|
|
16
|
+
import { isHostOutput } from "./hosts.ts";
|
|
17
|
+
import type { Output, SyncContext } from "./outputs.ts";
|
|
18
|
+
import { repoDirProblems } from "./repo-dir.ts";
|
|
19
|
+
import { isSelfName, stockRoot } from "./stock.ts";
|
|
20
|
+
|
|
21
|
+
/** The outputs, or every problem found, one per entry. */
|
|
22
|
+
export type TreesResult = { ok: true; outputs: Output[] } | { ok: false; errors: string[] };
|
|
23
|
+
|
|
24
|
+
/** Several source problems at once, so a render reports each instead of the first. */
|
|
25
|
+
class Problems extends Error {
|
|
26
|
+
readonly problems: string[];
|
|
27
|
+
|
|
28
|
+
constructor(problems: string[]) {
|
|
29
|
+
super(problems.join("\n"));
|
|
30
|
+
this.problems = problems;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STAGE_DIR_REL = ".repo/tmp";
|
|
35
|
+
const STAGE_PREFIX = "stage-";
|
|
36
|
+
const CLAUDE_SETTINGS_REL = ".claude/settings.json";
|
|
37
|
+
const CLAUDE_OVERLAY_REL = ".repo/settings/claude.json";
|
|
38
|
+
const MCP_REL = ".mcp.json";
|
|
39
|
+
const MCP_OVERLAY_REL = ".repo/mcp.json";
|
|
40
|
+
const CLAUDE_BASE_REL = "agents/settings/claude.json";
|
|
41
|
+
const TASKS_REL = ".vscode/tasks.json";
|
|
42
|
+
const OVERLAY_SKILLS_REL = ".repo/skills";
|
|
43
|
+
/** Each stock dir of static grok files, and the tree dir it is copied to. */
|
|
44
|
+
const STATIC_ROOTS = [
|
|
45
|
+
["agents/grok/roles", ".grok/roles"],
|
|
46
|
+
["agents/grok/hooks", ".grok/hooks"],
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/** `over` onto `base`: objects merge key by key, lists append what they lack, anything else is replaced. */
|
|
50
|
+
function mergeSettings(base: unknown, over: unknown): unknown {
|
|
51
|
+
if (Array.isArray(base) && Array.isArray(over)) {
|
|
52
|
+
return [...base, ...over.filter((item) => !base.some((have) => isDeepStrictEqual(have, item)))];
|
|
53
|
+
}
|
|
54
|
+
if (isObject(base) && isObject(over)) {
|
|
55
|
+
const merged: Record<string, unknown> = { ...base };
|
|
56
|
+
for (const [key, value] of Object.entries(over))
|
|
57
|
+
merged[key] = key in base ? mergeSettings(base[key], value) : value;
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
return over;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
64
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Throws the step's error, so one handler reports a failed emit step like a failed generator. */
|
|
68
|
+
function must(step: string, result: { ok: boolean; error?: string }): void {
|
|
69
|
+
if (!result.ok) throw new Error(`${step}: ${result.error}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every file under `dir`, as POSIX paths relative to it. */
|
|
73
|
+
function filesUnder(dir: string): string[] {
|
|
74
|
+
return fs
|
|
75
|
+
.readdirSync(dir, { recursive: true, encoding: "utf8" })
|
|
76
|
+
.map((rel) => rel.replaceAll("\\", "/"))
|
|
77
|
+
.filter((rel) => fs.statSync(path.join(dir, rel)).isFile());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The stock files no generator writes: grok's roles and hook configs. */
|
|
81
|
+
function staticFiles(stock: string): Map<string, string> {
|
|
82
|
+
const files = new Map<string, string>();
|
|
83
|
+
for (const [from, to] of STATIC_ROOTS) {
|
|
84
|
+
for (const rel of filesUnder(path.join(stock, from))) {
|
|
85
|
+
const text = fs.readFileSync(path.join(stock, from, rel), "utf8");
|
|
86
|
+
files.set(
|
|
87
|
+
`${to}/${rel}`,
|
|
88
|
+
text.replace(
|
|
89
|
+
/("command": )("repo hook [\w-]+")/g,
|
|
90
|
+
(_, key, value) => key + JSON.stringify(expandHookCommand(String(JSON.parse(value)))),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function posix(rel: string): string {
|
|
99
|
+
return rel.split(path.sep).join("/");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The skill links for `hosts`: `.agents/skills` for codex and antigravity, `.claude/skills` for claude. */
|
|
103
|
+
function skillLinks(ctx: SyncContext, stock: string, self: boolean): Output[] {
|
|
104
|
+
const { hosts, skills } = ctx.config.agents;
|
|
105
|
+
const own = self ? [] : listSkillDirNames(path.join(ctx.root, OVERLAY_SKILLS_REL));
|
|
106
|
+
const optional = new Set<string>(SKILL_PACKS.filter((pack) => !skills.includes(pack)));
|
|
107
|
+
const stockNames = listSkillDirNames(path.join(stock, STOCK_SKILLS_REL)).filter((name) => !optional.has(name));
|
|
108
|
+
const names = [...new Set([...stockNames, ...own])].sort();
|
|
109
|
+
const sourceOf = (name: string) =>
|
|
110
|
+
own.includes(name) ? path.join(ctx.root, OVERLAY_SKILLS_REL, name) : path.join(stock, STOCK_SKILLS_REL, name);
|
|
111
|
+
const agentsDir = hosts.includes("codex") || hosts.includes("antigravity");
|
|
112
|
+
const links: Output[] = [];
|
|
113
|
+
const add = (dir: string, name: string, target: string) =>
|
|
114
|
+
links.push({ kind: "link", path: `${dir}/${name}`, target, tracked: self, managed: "whole" });
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
if (agentsDir)
|
|
117
|
+
add(WORKSPACE_SKILLS_REL, name, posix(path.relative(path.join(ctx.root, WORKSPACE_SKILLS_REL), sourceOf(name))));
|
|
118
|
+
if (hosts.includes("claude")) {
|
|
119
|
+
const target =
|
|
120
|
+
agentsDir && !self
|
|
121
|
+
? `../../${WORKSPACE_SKILLS_REL}/${name}`
|
|
122
|
+
: posix(path.relative(path.join(ctx.root, ".claude/skills"), sourceOf(name)));
|
|
123
|
+
add(".claude/skills", name, target);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...links, ...packageSkillLinks(ctx, self)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Each workspace package's own skills, linked straight to `<pkg>/.repo/skills/<name>` from the dirs its hosts read:
|
|
131
|
+
* `<pkg>/.claude/skills` for claude (which names them `/<pkg>:<name>`), `<pkg>/.agents/skills` for codex and antigravity.
|
|
132
|
+
*/
|
|
133
|
+
function packageSkillLinks(ctx: SyncContext, self: boolean): Output[] {
|
|
134
|
+
const { hosts } = ctx.config.agents;
|
|
135
|
+
const agentsDir = hosts.includes("codex") || hosts.includes("antigravity");
|
|
136
|
+
const dirs = [...(hosts.includes("claude") ? [".claude/skills"] : []), ...(agentsDir ? [".agents/skills"] : [])];
|
|
137
|
+
return ctx.workspace.packages.flatMap(({ dir }) =>
|
|
138
|
+
listSkillDirNames(path.join(ctx.root, dir, OVERLAY_SKILLS_REL)).flatMap((name): Output[] =>
|
|
139
|
+
dirs.map((host) => ({
|
|
140
|
+
kind: "link",
|
|
141
|
+
path: `${dir}/${host}/${name}`,
|
|
142
|
+
target: `../../${OVERLAY_SKILLS_REL}/${name}`,
|
|
143
|
+
tracked: self,
|
|
144
|
+
managed: "whole",
|
|
145
|
+
})),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The rule files of the root and of every workspace package, each scope rendered on its own so no package rule folds
|
|
152
|
+
* into the root `AGENTS.md`, and one problem line for each path two scopes plan.
|
|
153
|
+
*/
|
|
154
|
+
function renderScopes(ctx: SyncContext, sources: Sources): { files: Map<string, string>; clashes: string[] } {
|
|
155
|
+
const { hosts } = ctx.config.agents;
|
|
156
|
+
const planned = new Map<string, RuleFile>();
|
|
157
|
+
const clashes: string[] = [];
|
|
158
|
+
for (const scope of ["", ...ctx.workspace.packages.map((pkg) => pkg.dir)]) {
|
|
159
|
+
for (const [rel, file] of renderRules(sources.rules, hosts, scope)) {
|
|
160
|
+
const first = planned.get(rel);
|
|
161
|
+
if (first === undefined) planned.set(rel, file);
|
|
162
|
+
else clashes.push(`${rel}: planned by ${first.source} and ${file.source}; rename one`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { files: new Map([...planned].map(([rel, file]) => [rel, file.text])), clashes };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Deep-merges the overlay file `overlayRel` (when it exists) over the staged JSON file `rel`. */
|
|
169
|
+
function mergeOverlay(ctx: SyncContext, stage: string, rel: string, overlayRel: string): void {
|
|
170
|
+
const overlay = ctx.read(overlayRel);
|
|
171
|
+
if (overlay === null) return;
|
|
172
|
+
let own: unknown;
|
|
173
|
+
try {
|
|
174
|
+
own = JSON.parse(overlay);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(`${overlayRel}: ${(error as Error).message}`);
|
|
177
|
+
}
|
|
178
|
+
if (!isObject(own)) throw new Error(`${overlayRel}: must be a JSON object`);
|
|
179
|
+
const file = path.join(stage, rel);
|
|
180
|
+
const base: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
181
|
+
fs.writeFileSync(file, `${JSON.stringify(mergeSettings(base, own), null, 2)}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Fills the stage: rules, subagents and hooks, the emit steps, the claude overlay and the static files; returns its files. */
|
|
185
|
+
function fillStage(
|
|
186
|
+
ctx: SyncContext,
|
|
187
|
+
stage: string,
|
|
188
|
+
stock: string,
|
|
189
|
+
sources: Sources,
|
|
190
|
+
rules: Map<string, string>,
|
|
191
|
+
): Map<string, string> {
|
|
192
|
+
const { hosts } = ctx.config.agents;
|
|
193
|
+
const claudeBase = fs.readFileSync(path.join(stock, CLAUDE_BASE_REL), "utf8");
|
|
194
|
+
const generated = [rules, renderSubagents(sources, hosts), renderHooks(sources, hosts, claudeBase)];
|
|
195
|
+
for (const [rel, text] of generated.flatMap((files) => [...files])) {
|
|
196
|
+
fs.mkdirSync(path.dirname(path.join(stage, rel)), { recursive: true });
|
|
197
|
+
fs.writeFileSync(path.join(stage, rel), text);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
must("mcp policy", emitMcpPolicy({ workspaceRoot: stage, sourceRoot: stock, consumerRoot: ctx.root }));
|
|
201
|
+
if (hosts.includes("claude")) {
|
|
202
|
+
must("scout tools", emitAgentShellTools({ workspaceRoot: stage, sourceRoot: stock }));
|
|
203
|
+
}
|
|
204
|
+
must("hooks", applyUnion({ workspaceRoot: stage, sourceRoot: stock, consumerRoot: ctx.root }));
|
|
205
|
+
|
|
206
|
+
if (hosts.includes("claude")) {
|
|
207
|
+
mergeOverlay(ctx, stage, CLAUDE_SETTINGS_REL, CLAUDE_OVERLAY_REL);
|
|
208
|
+
mergeOverlay(ctx, stage, MCP_REL, MCP_OVERLAY_REL);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const files = new Map<string, string>();
|
|
212
|
+
for (const rel of filesUnder(stage)) files.set(rel, fs.readFileSync(path.join(stage, rel), "utf8"));
|
|
213
|
+
if (hosts.includes("grok")) for (const [rel, text] of staticFiles(stock)) files.set(rel, text);
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Every generated agent tree for the enabled hosts as outputs, built in a fresh `.repo/tmp/stage-*` dir (removed again) from
|
|
219
|
+
* the stock package plus the `.repo/` overlay. Nothing outside the stage is written. In a consumer the trees are
|
|
220
|
+
* ignored (`tracked: false`); in this repo they are the committed trees; `.vscode/tasks.json` is committed in both.
|
|
221
|
+
*/
|
|
222
|
+
export function renderTrees(ctx: SyncContext): TreesResult {
|
|
223
|
+
const { hosts } = ctx.config.agents;
|
|
224
|
+
if (hosts.length === 0) return { ok: true, outputs: [] };
|
|
225
|
+
const self = isSelfName(ctx.workspace.manifest.name);
|
|
226
|
+
const stock = self ? ctx.root : stockRoot(ctx.root);
|
|
227
|
+
if (!fs.existsSync(path.join(stock, "agents"))) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
errors: [`${path.relative(ctx.root, stock).split(path.sep).join("/")} is missing; run \`bun install\``],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// One stage per call: processes rendering the same root at once (parallel test files, two syncs) must not clear each other's.
|
|
235
|
+
fs.mkdirSync(path.join(ctx.root, STAGE_DIR_REL), { recursive: true });
|
|
236
|
+
const stage = fs.mkdtempSync(path.join(ctx.root, STAGE_DIR_REL, STAGE_PREFIX));
|
|
237
|
+
let result: TreesResult;
|
|
238
|
+
try {
|
|
239
|
+
result = { ok: true, outputs: assemble(ctx, stage, stock, self) };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
result = { ok: false, errors: error instanceof Problems ? error.problems : [(error as Error).message] };
|
|
242
|
+
}
|
|
243
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The stage's host files, the skill links and the tasks file, sorted by path. */
|
|
248
|
+
function assemble(ctx: SyncContext, stage: string, stock: string, self: boolean): Output[] {
|
|
249
|
+
const { hosts } = ctx.config.agents;
|
|
250
|
+
const outputs: Output[] = [];
|
|
251
|
+
const packages = ctx.workspace.packages.map((pkg) => pkg.dir);
|
|
252
|
+
const { sources, problems: loadProblems } = loadSources({
|
|
253
|
+
stockRoot: stock,
|
|
254
|
+
consumerRoot: self ? null : ctx.root,
|
|
255
|
+
packages,
|
|
256
|
+
});
|
|
257
|
+
// The checks run on the docs that loaded, so one bad file does not hide the problems of the others.
|
|
258
|
+
const rules = renderScopes(ctx, sources);
|
|
259
|
+
const problems = [
|
|
260
|
+
...repoDirProblems(ctx.root, {
|
|
261
|
+
sources,
|
|
262
|
+
stock,
|
|
263
|
+
listed: listedFiles(ctx.root, []),
|
|
264
|
+
skills: ctx.config.agents.skills,
|
|
265
|
+
packages,
|
|
266
|
+
}),
|
|
267
|
+
...loadProblems,
|
|
268
|
+
...rules.clashes,
|
|
269
|
+
];
|
|
270
|
+
if (problems.length > 0) throw new Problems(problems);
|
|
271
|
+
const files = fillStage(ctx, stage, stock, sources, rules.files);
|
|
272
|
+
for (const [rel, content] of files) {
|
|
273
|
+
if (isHostOutput(hosts, rel)) outputs.push({ kind: "file", path: rel, content, tracked: self, managed: "whole" });
|
|
274
|
+
}
|
|
275
|
+
outputs.push(...skillLinks(ctx, stock, self));
|
|
276
|
+
const tasks = renderTasks(ctx.root, ctx.read(TASKS_REL));
|
|
277
|
+
if (!tasks.ok) throw new Error(tasks.error);
|
|
278
|
+
outputs.push({ kind: "file", path: TASKS_REL, content: tasks.text, tracked: true, managed: "whole" });
|
|
279
|
+
return outputs.sort((a, b) => Number(a.path > b.path) - Number(a.path < b.path));
|
|
280
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { readGoldens, renderFixtureTrees } from "../../dev/capture-tree-goldens.ts";
|
|
7
|
+
import { loadConfig } from "../config.ts";
|
|
8
|
+
import { computeOutputs } from "../sync.ts";
|
|
9
|
+
import type { Output } from "./outputs.ts";
|
|
10
|
+
import { OXFMT_CONFIG, OXLINT_CONFIG, validateOutputs } from "./validate.ts";
|
|
11
|
+
import { readWorkspace } from "./workspace.ts";
|
|
12
|
+
|
|
13
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
14
|
+
const SLOW = 60_000;
|
|
15
|
+
|
|
16
|
+
const tmpDirs: string[] = [];
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function tmpRoot(): string {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-validate-"));
|
|
23
|
+
tmpDirs.push(dir);
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const file = (p: string, content: string): Output => ({
|
|
28
|
+
kind: "file",
|
|
29
|
+
path: p,
|
|
30
|
+
content,
|
|
31
|
+
tracked: true,
|
|
32
|
+
managed: "whole",
|
|
33
|
+
});
|
|
34
|
+
const link = (p: string, target: string): Output => ({
|
|
35
|
+
kind: "link",
|
|
36
|
+
path: p,
|
|
37
|
+
target,
|
|
38
|
+
tracked: true,
|
|
39
|
+
managed: "whole",
|
|
40
|
+
});
|
|
41
|
+
const AGENT = "---\nname: scout\ndescription: Facts only.\n---\nBody\n";
|
|
42
|
+
|
|
43
|
+
describe("file formats", () => {
|
|
44
|
+
it("accepts well-formed json, toml, markdown and text", () => {
|
|
45
|
+
const root = tmpRoot();
|
|
46
|
+
const outputs = [
|
|
47
|
+
file(".vscode/tasks.json", "{}\n"),
|
|
48
|
+
file("knip.json", "{}\n"),
|
|
49
|
+
file("bunfig.toml", '[install]\nlinker = "hoisted"\n'),
|
|
50
|
+
file("AGENTS.md", "# Plain markdown\n"),
|
|
51
|
+
file("references/INDEX.md", "---\n---\nEmpty frontmatter\n"),
|
|
52
|
+
file(".gitignore", "anything\n"),
|
|
53
|
+
file(".claude/agents/scout.md", AGENT),
|
|
54
|
+
file(".claude/agents/crlf.md", "---\r\nname: a\r\ndescription: b\r\n---\r\nBody\r\n"),
|
|
55
|
+
];
|
|
56
|
+
expect(validateOutputs(root, outputs)).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("reports bad JSON with the parser's message", () => {
|
|
60
|
+
const lines = validateOutputs(tmpRoot(), [file(".vscode/tasks.json", "{ nope")]);
|
|
61
|
+
expect(lines).toHaveLength(1);
|
|
62
|
+
expect(lines[0]).toMatch(/^\.vscode\/tasks\.json: invalid JSON: /);
|
|
63
|
+
expect(validateOutputs(tmpRoot(), [file("knip.json", "{ nope")])[0]).toMatch(/^knip\.json: invalid JSON: /);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("accepts a commented JSONC settings file with a trailing comma, and a .gitattributes and ci.yml as text", () => {
|
|
67
|
+
const text = '// editor\n{\n /* mine */ "editor.tabSize": 2,\n "files.readonlyInclude": { "a": true, },\n}\n';
|
|
68
|
+
expect(
|
|
69
|
+
validateOutputs(tmpRoot(), [
|
|
70
|
+
file(".vscode/settings.json", text),
|
|
71
|
+
file(".gitattributes", "* text=auto\n"),
|
|
72
|
+
file(".github/workflows/ci.yml", "name: ci\n"),
|
|
73
|
+
file(".github/workflows/release.yml", "name: release\n"),
|
|
74
|
+
]),
|
|
75
|
+
).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reports bad JSONC with the parser's error code and offset", () => {
|
|
79
|
+
expect(validateOutputs(tmpRoot(), [file(".vscode/settings.json", '{ "a": }')])).toEqual([
|
|
80
|
+
".vscode/settings.json: invalid JSONC: ValueExpected at offset 7",
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("reports bad TOML with the parser's message", () => {
|
|
85
|
+
const lines = validateOutputs(tmpRoot(), [file("bunfig.toml", "[install\nx = ")]);
|
|
86
|
+
expect(lines).toHaveLength(1);
|
|
87
|
+
expect(lines[0]).toMatch(/^bunfig\.toml: invalid TOML: /);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("reports bad YAML frontmatter", () => {
|
|
91
|
+
const lines = validateOutputs(tmpRoot(), [file("AGENTS.md", "---\nname: [unclosed\n---\nBody\n")]);
|
|
92
|
+
expect(lines).toHaveLength(1);
|
|
93
|
+
expect(lines[0]).toMatch(/^AGENTS\.md: invalid frontmatter: /);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reports frontmatter that is not closed", () => {
|
|
97
|
+
expect(validateOutputs(tmpRoot(), [file("AGENTS.md", "---\nname: a\nBody\n")])).toEqual([
|
|
98
|
+
"AGENTS.md: invalid frontmatter: no closing ---",
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reports frontmatter that is not a mapping", () => {
|
|
103
|
+
expect(validateOutputs(tmpRoot(), [file("AGENTS.md", "---\n- a\n- b\n---\nBody\n")])).toEqual([
|
|
104
|
+
"AGENTS.md: invalid frontmatter: must be a mapping",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("checks agent frontmatter against its schema", () => {
|
|
109
|
+
const lines = validateOutputs(tmpRoot(), [
|
|
110
|
+
file(".cursor/agents/a.md", "---\nname: a\n---\nBody\n"),
|
|
111
|
+
file(".grok/agents/b.md", "No frontmatter at all\n"),
|
|
112
|
+
]);
|
|
113
|
+
expect(lines).toEqual([
|
|
114
|
+
".cursor/agents/a.md: description: Invalid input: expected string, received undefined",
|
|
115
|
+
".grok/agents/b.md: name: Invalid input: expected string, received undefined",
|
|
116
|
+
".grok/agents/b.md: description: Invalid input: expected string, received undefined",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("reports a schema miss with its dotted path and zod's message", () => {
|
|
121
|
+
const settings = JSON.stringify({ hooks: { PreToolUse: "x" } });
|
|
122
|
+
expect(validateOutputs(tmpRoot(), [file(".claude/settings.json", settings)])).toEqual([
|
|
123
|
+
".claude/settings.json: hooks.PreToolUse: Invalid input: expected array, received string",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("reports a schema miss at the top level without a dotted path", () => {
|
|
128
|
+
expect(validateOutputs(tmpRoot(), [file(".mcp.json", "[]")])).toEqual([
|
|
129
|
+
".mcp.json: Invalid input: expected object, received array",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("reports indexes in a dotted path", () => {
|
|
134
|
+
const settings = JSON.stringify({ hooks: { Stop: [{ hooks: [{ command: 1 }] }] } });
|
|
135
|
+
const lines = validateOutputs(tmpRoot(), [file(".claude/settings.json", settings)]);
|
|
136
|
+
expect(lines).toContain(
|
|
137
|
+
".claude/settings.json: hooks.Stop.0.hooks.0.type: Invalid input: expected string, received undefined",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("accepts keys the host may add", () => {
|
|
142
|
+
const hooks = {
|
|
143
|
+
hooks: { Stop: [{ future: 1, hooks: [{ type: "prompt", prompt: "x", timeout: 5, extra: true }] }] },
|
|
144
|
+
other: 1,
|
|
145
|
+
};
|
|
146
|
+
const outputs = [
|
|
147
|
+
file(".claude/settings.json", JSON.stringify({ ...hooks, permissions: { deny: ["x"], allow: [] } })),
|
|
148
|
+
file(".codex/hooks.json", JSON.stringify(hooks)),
|
|
149
|
+
file(
|
|
150
|
+
".cursor/hooks.json",
|
|
151
|
+
JSON.stringify({ version: 1, hooks: { stop: [{ type: "command", command: "x", matcher: "y", extra: 1 }] } }),
|
|
152
|
+
),
|
|
153
|
+
file(
|
|
154
|
+
".agents/hooks.json",
|
|
155
|
+
JSON.stringify({ rulesync: { Stop: [{ command: "x" }, { matcher: "m", hooks: [{ type: "command" }] }] } }),
|
|
156
|
+
),
|
|
157
|
+
file(".agents/mcp_config.json", JSON.stringify({ mcpServers: { a: { command: "x" } } })),
|
|
158
|
+
file(".grok/hooks/rtk.json", JSON.stringify(hooks)),
|
|
159
|
+
];
|
|
160
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("accepts the empty hook files the generators emit", () => {
|
|
164
|
+
const outputs = [
|
|
165
|
+
file(".agents/hooks.json", "{}"),
|
|
166
|
+
file(".grok/hooks/rulesync.json", '{"hooks":{}}'),
|
|
167
|
+
file(".mcp.json", '{"mcpServers":{}}'),
|
|
168
|
+
];
|
|
169
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("refuses a file path with no registered format", () => {
|
|
173
|
+
expect(validateOutputs(tmpRoot(), [file("docs/x.yaml", "a: 1\n")])).toEqual([
|
|
174
|
+
"docs/x.yaml: no output format registered; add it to cli/sync/validate.ts",
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("reports every problem, in output order", () => {
|
|
179
|
+
const lines = validateOutputs(tmpRoot(), [file("b.yaml", ""), file(".vscode/tasks.json", "{"), file("a.yaml", "")]);
|
|
180
|
+
expect(lines.map((line) => line.split(":")[0])).toEqual(["b.yaml", ".vscode/tasks.json", "a.yaml"]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("the standards configs", () => {
|
|
185
|
+
/** The top-level keys the pinned tool's shipped JSON schema allows. */
|
|
186
|
+
function shippedKeys(tool: "oxlint" | "oxfmt"): string[] {
|
|
187
|
+
const dir = path.dirname(Bun.resolveSync(`${tool}/package.json`, import.meta.dirname));
|
|
188
|
+
const schema = JSON.parse(fs.readFileSync(path.join(dir, "configuration_schema.json"), "utf8")) as {
|
|
189
|
+
properties: Record<string, unknown>;
|
|
190
|
+
};
|
|
191
|
+
return Object.keys(schema.properties).sort();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
it("checks each config against every top-level key its tool allows, and only those", () => {
|
|
195
|
+
expect(Object.keys(OXLINT_CONFIG.shape).sort()).toEqual(shippedKeys("oxlint"));
|
|
196
|
+
expect(Object.keys(OXFMT_CONFIG.shape).sort()).toEqual(shippedKeys("oxfmt"));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("accepts a commented oxfmt config and a plain oxlint config", () => {
|
|
200
|
+
const outputs = [
|
|
201
|
+
file(".oxlintrc.json", JSON.stringify({ plugins: ["eslint"], rules: { "eslint/no-console": ["error", {}] } })),
|
|
202
|
+
file(".oxfmtrc.jsonc", '// generated\n{ "printWidth": 120, "ignorePatterns": ["/a"], }\n'),
|
|
203
|
+
];
|
|
204
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("refuses a key neither tool knows", () => {
|
|
208
|
+
const lines = validateOutputs(tmpRoot(), [
|
|
209
|
+
file(".oxlintrc.json", '{ "rulez": {} }'),
|
|
210
|
+
file(".oxfmtrc.jsonc", '{ "width": 120 }'),
|
|
211
|
+
]);
|
|
212
|
+
expect(lines).toHaveLength(2);
|
|
213
|
+
expect(lines[0]).toMatch(/^\.oxlintrc\.json: .*"rulez"/);
|
|
214
|
+
expect(lines[1]).toMatch(/^\.oxfmtrc\.jsonc: .*"width"/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("links", () => {
|
|
219
|
+
it("accepts a link to a path on disk, relative or absolute", () => {
|
|
220
|
+
const root = tmpRoot();
|
|
221
|
+
fs.mkdirSync(path.join(root, "agents/skills/ask"), { recursive: true });
|
|
222
|
+
const outputs = [
|
|
223
|
+
link(".agents/skills/ask", "../../agents/skills/ask"),
|
|
224
|
+
link(".agents/skills/abs", path.join(root, "agents/skills/ask")),
|
|
225
|
+
];
|
|
226
|
+
expect(validateOutputs(root, outputs)).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("reports a dangling link", () => {
|
|
230
|
+
expect(validateOutputs(tmpRoot(), [link(".agents/skills/ask", "../../agents/skills/ask")])).toEqual([
|
|
231
|
+
".agents/skills/ask: link target ../../agents/skills/ask does not exist",
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("reports a link that leaves the repo for a missing path", () => {
|
|
236
|
+
const lines = validateOutputs(tmpRoot(), [link("a/b", "../../../no-such-dir-workerstack")]);
|
|
237
|
+
expect(lines).toEqual(["a/b: link target ../../../no-such-dir-workerstack does not exist"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("follows a link whose target is another planned link into an existing dir", () => {
|
|
241
|
+
const root = tmpRoot();
|
|
242
|
+
fs.mkdirSync(path.join(root, ".repo/skills/x"), { recursive: true });
|
|
243
|
+
const outputs = [
|
|
244
|
+
link(".claude/skills/x", "../../.agents/skills/x"),
|
|
245
|
+
link(".agents/skills/x", "../../.repo/skills/x"),
|
|
246
|
+
];
|
|
247
|
+
expect(validateOutputs(root, outputs)).toEqual([]);
|
|
248
|
+
expect(validateOutputs(root, [...outputs].reverse())).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("reports a chain of planned links that ends in nothing", () => {
|
|
252
|
+
const outputs = [
|
|
253
|
+
link(".claude/skills/x", "../../.agents/skills/x"),
|
|
254
|
+
link(".agents/skills/x", "../../.repo/skills/x"),
|
|
255
|
+
];
|
|
256
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([
|
|
257
|
+
".claude/skills/x: link target ../../.agents/skills/x does not exist",
|
|
258
|
+
".agents/skills/x: link target ../../.repo/skills/x does not exist",
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("reports planned links that point at each other", () => {
|
|
263
|
+
const outputs = [link("a", "b"), link("b", "a")];
|
|
264
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([
|
|
265
|
+
"a: link target b does not exist",
|
|
266
|
+
"b: link target a does not exist",
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("accepts a link into a planned dir that is not on disk yet", () => {
|
|
271
|
+
const outputs = [link(".claude/skills/x", "../../.codex/agents"), file(".codex/agents/a.toml", 'name = "a"\n')];
|
|
272
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("checks files against the formats it is given", () => {
|
|
276
|
+
const formats = [{ match: () => true, format: "text" as const }];
|
|
277
|
+
expect(validateOutputs(tmpRoot(), [file("docs/x.yaml", "a: 1\n")], formats)).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("registered formats", () => {
|
|
282
|
+
it("accepts every output of the golden consumer fixture", { timeout: SLOW }, async () => {
|
|
283
|
+
const rendered = await renderFixtureTrees();
|
|
284
|
+
expect([...rendered.keys()]).toEqual([...readGoldens().keys()]);
|
|
285
|
+
const outputs = [...rendered].map(([rel, content]) => file(rel, content));
|
|
286
|
+
expect(outputs.length).toBeGreaterThan(30);
|
|
287
|
+
expect(validateOutputs(tmpRoot(), outputs)).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("accepts every output this repo plans", { timeout: SLOW }, async () => {
|
|
291
|
+
const config = await loadConfig(REPO_ROOT);
|
|
292
|
+
const workspace = readWorkspace(REPO_ROOT);
|
|
293
|
+
if (!config.ok || !workspace.ok) throw new Error("this repo's config or workspace did not load");
|
|
294
|
+
const planned = computeOutputs(REPO_ROOT, config.config, workspace.workspace);
|
|
295
|
+
if (!planned.ok) throw new Error(planned.errors.join("\n"));
|
|
296
|
+
expect(planned.outputs.length).toBeGreaterThan(100);
|
|
297
|
+
expect(validateOutputs(REPO_ROOT, planned.outputs)).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|