@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,22 @@
|
|
|
1
|
+
# Index
|
|
2
|
+
|
|
3
|
+
Essay-style INDEX must not appear as a catalog row.
|
|
4
|
+
|
|
5
|
+
<!-- repo:skills -->
|
|
6
|
+
| name | origin | description | task | commandPath | refs |
|
|
7
|
+
| --- | --- | --- | --- | --- | --- |
|
|
8
|
+
| alpha | local | Alpha skill one-liner. | alpha-work | scripts/alpha.mjs | cascade.md, alpha.md |
|
|
9
|
+
| beta | repo | Beta folded description that should collapse to one line. | | | |
|
|
10
|
+
<!-- /repo:skills -->
|
|
11
|
+
|
|
12
|
+
Hand text between sections stays.
|
|
13
|
+
|
|
14
|
+
<!-- repo:refs -->
|
|
15
|
+
| path | title | tags | summary |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| node_modules/@workerstack/repo/references/alpha.md | Alpha | alpha | First sentence of alpha ref. |
|
|
18
|
+
| node_modules/@workerstack/repo/references/nested/beta.md | Beta nested | beta | Nested beta summary line. |
|
|
19
|
+
<!-- /repo:refs -->
|
|
20
|
+
|
|
21
|
+
<!-- repo:stack -->
|
|
22
|
+
<!-- /repo:stack -->
|
|
Binary file
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { BASE_CONFIG } from "../fixtures/base-config.ts";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parse } from "jsonc-parser";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { readGoldens } from "../../dev/capture-tree-goldens.ts";
|
|
9
|
+
import { HOSTS, type Host } from "../config.ts";
|
|
10
|
+
import { gitFiles, listedFiles, untrackedEntries } from "./git-files.ts";
|
|
11
|
+
import { HOST_TREES } from "./hosts.ts";
|
|
12
|
+
import type { Managed, Output, SyncContext } from "./outputs.ts";
|
|
13
|
+
|
|
14
|
+
const IGNORE_HEAD = "# >>> @workerstack/repo (generated by repo sync)\n.tmp/\n.worktrees/\n.repo/tmp/\ncoverage/\n";
|
|
15
|
+
const IGNORE_END = "# <<< @workerstack/repo\n";
|
|
16
|
+
const ATTR_HEAD = "# >>> @workerstack/repo (generated by repo sync)\n* text=auto eol=lf\n";
|
|
17
|
+
|
|
18
|
+
type Fixture = { hosts?: Host[]; name?: string; files?: Record<string, string> };
|
|
19
|
+
|
|
20
|
+
function context({ hosts = [], name = "app", files = {} }: Fixture = {}): SyncContext {
|
|
21
|
+
return {
|
|
22
|
+
root: "/repo",
|
|
23
|
+
config: {
|
|
24
|
+
packs: ["agents"],
|
|
25
|
+
agents: { hosts, skills: [] },
|
|
26
|
+
...BASE_CONFIG,
|
|
27
|
+
},
|
|
28
|
+
workspace: { root: "/repo", manifest: { name }, packages: [] },
|
|
29
|
+
read: (rel) => files[rel] ?? null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const file = (path: string, tracked: boolean, managed: Managed = "whole"): Output => ({
|
|
34
|
+
kind: "file",
|
|
35
|
+
path,
|
|
36
|
+
content: `${path}\n`,
|
|
37
|
+
tracked,
|
|
38
|
+
managed,
|
|
39
|
+
});
|
|
40
|
+
const link = (path: string, tracked: boolean): Output => ({
|
|
41
|
+
kind: "link",
|
|
42
|
+
path,
|
|
43
|
+
target: "elsewhere",
|
|
44
|
+
tracked,
|
|
45
|
+
managed: "whole",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** What the trees generator plans for `hosts`: AGENTS.md, .mcp.json and a file in each dir the hosts own, none committed. */
|
|
49
|
+
function treeOutputs(hosts: readonly Host[], tracked: boolean): Output[] {
|
|
50
|
+
const dirs = hosts.flatMap((host) => HOST_TREES[host].owns);
|
|
51
|
+
return [
|
|
52
|
+
...(hosts.length > 0 ? [file("AGENTS.md", tracked)] : []),
|
|
53
|
+
...(hosts.includes("claude") ? [file(".mcp.json", tracked)] : []),
|
|
54
|
+
...dirs.map((dir) => file(`${dir}config.toml`, tracked)),
|
|
55
|
+
...dirs.map((dir) => link(`${dir}skills-link`, tracked)),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const outputOf = (outputs: Output[], rel: string): string => {
|
|
60
|
+
const found = outputs.find((output) => output.path === rel);
|
|
61
|
+
if (found?.kind !== "file") throw new Error(`no file output ${rel}`);
|
|
62
|
+
return found.content;
|
|
63
|
+
};
|
|
64
|
+
const run = (fixture: Fixture, outputs: Output[]) => gitFiles(context(fixture), outputs);
|
|
65
|
+
const gitignoreOf = (fixture: Fixture, outputs: Output[]) => outputOf(run(fixture, outputs).outputs, ".gitignore");
|
|
66
|
+
const gitattributesOf = (fixture: Fixture, outputs: Output[]) =>
|
|
67
|
+
outputOf(run(fixture, outputs).outputs, ".gitattributes");
|
|
68
|
+
const settingsOf = (fixture: Fixture, outputs: Output[]) =>
|
|
69
|
+
outputOf(run(fixture, outputs).outputs, ".vscode/settings.json");
|
|
70
|
+
const readonlyOf = (text: string): string[] =>
|
|
71
|
+
Object.keys((parse(text) as Record<string, Record<string, boolean>>)["files.readonlyInclude"] ?? {});
|
|
72
|
+
const block = (...entries: string[]) => `${IGNORE_HEAD}${["node_modules/", ...entries].join("\n")}\n${IGNORE_END}`;
|
|
73
|
+
|
|
74
|
+
describe("the gitignore block", () => {
|
|
75
|
+
it("lists AGENTS.md, .mcp.json and every host dir for the untracked trees of all hosts", () => {
|
|
76
|
+
expect(gitignoreOf({ hosts: [...HOSTS] }, treeOutputs(HOSTS, false))).toBe(
|
|
77
|
+
block("AGENTS.md", ".mcp.json", ".agents/", ".claude/", ".codex/", ".cursor/", ".grok/"),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("lists only what the enabled hosts own, a parent dir covering its children", () => {
|
|
82
|
+
expect(gitignoreOf({ hosts: ["claude"] }, treeOutputs(["claude"], false))).toBe(
|
|
83
|
+
block("AGENTS.md", ".mcp.json", ".claude/"),
|
|
84
|
+
);
|
|
85
|
+
expect(gitignoreOf({ hosts: ["codex"] }, treeOutputs(["codex"], false))).toBe(
|
|
86
|
+
block("AGENTS.md", ".agents/skills/", ".codex/"),
|
|
87
|
+
);
|
|
88
|
+
const both: Host[] = ["codex", "antigravity"];
|
|
89
|
+
expect(gitignoreOf({ hosts: both }, treeOutputs(both, false))).toBe(block("AGENTS.md", ".agents/", ".codex/"));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("lists an untracked output outside every host dir by its exact path, and an untracked link by its dir", () => {
|
|
93
|
+
const outputs = [file("generated/out.txt", false), link(".claude/skills/ask", false)];
|
|
94
|
+
expect(gitignoreOf({ hosts: ["claude"] }, outputs)).toBe(block("generated/out.txt", ".claude/"));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores the vitest preset's report directory, so lint does not scan generated coverage reports", () => {
|
|
98
|
+
const hosts: Host[] = ["claude"];
|
|
99
|
+
expect(gitignoreOf({ name: "@workerstack/repo", hosts }, treeOutputs(hosts, true)).split("\n")).toContain(
|
|
100
|
+
"coverage/",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("lists nothing untracked for this repo, whose trees are committed, and keeps its own dependency line outside", () => {
|
|
105
|
+
const self = { name: "@workerstack/repo", hosts: ["claude", "codex"] as Host[] };
|
|
106
|
+
expect(gitignoreOf(self, treeOutputs(self.hosts, true))).toBe(`${IGNORE_HEAD}${IGNORE_END}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("lists no tracked output and no block output, tracked or not", () => {
|
|
110
|
+
const outputs = [
|
|
111
|
+
file("bunfig.toml", true),
|
|
112
|
+
file("references/INDEX.md", false, "block"),
|
|
113
|
+
file(".gitattributes", false, "block"),
|
|
114
|
+
];
|
|
115
|
+
expect(gitignoreOf({ hosts: ["claude"] }, outputs)).toBe(block());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("writes the block alone when .gitignore is missing, empty or blank", () => {
|
|
119
|
+
for (const files of [{}, { ".gitignore": "" }, { ".gitignore": "\n\n" }] as Record<string, string>[])
|
|
120
|
+
expect(gitignoreOf({ files }, [])).toBe(block());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("appends the block after one blank line, adding a missing trailing newline first", () => {
|
|
124
|
+
expect(gitignoreOf({ files: { ".gitignore": "dist" } }, [])).toBe(`dist\n\n${block()}`);
|
|
125
|
+
expect(gitignoreOf({ files: { ".gitignore": "node_modules/\n.tmp/\n\n\n" } }, [])).toBe(
|
|
126
|
+
`node_modules/\n.tmp/\n\n${block()}`,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("replaces the block in place, matching the begin marker by prefix and keeping the text around it", () => {
|
|
131
|
+
const text = "dist/\n# >>> @workerstack/repo (old label)\n.cache/\n# <<< @workerstack/repo\n\n.env\n";
|
|
132
|
+
expect(gitignoreOf({ files: { ".gitignore": text } }, [])).toBe(`dist/\n${block()}\n.env\n`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("leaves a current .gitignore byte-identical and does not take a longer marker for the begin marker", () => {
|
|
136
|
+
const current = `dist/\n\n${block()}`;
|
|
137
|
+
expect(gitignoreOf({ files: { ".gitignore": current } }, [])).toBe(current);
|
|
138
|
+
const longer = "# >>> @workerstack/repository\n";
|
|
139
|
+
expect(gitignoreOf({ files: { ".gitignore": longer } }, [])).toBe(`${longer}\n${block()}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("fails on a begin marker with no closing marker, for .gitignore and .gitattributes alike", () => {
|
|
143
|
+
const open = "# >>> @workerstack/repo (generated by repo sync)\n.tmp/\n";
|
|
144
|
+
const result = run({ files: { ".gitignore": open, ".gitattributes": open } }, []);
|
|
145
|
+
expect(result.errors).toEqual([
|
|
146
|
+
'.gitignore: "# >>> @workerstack/repo" has no closing "# <<< @workerstack/repo"',
|
|
147
|
+
'.gitattributes: "# >>> @workerstack/repo" has no closing "# <<< @workerstack/repo"',
|
|
148
|
+
]);
|
|
149
|
+
expect(result.outputs.map((output) => output.path)).toEqual([".vscode/settings.json"]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("untrackedEntries", () => {
|
|
154
|
+
it("is what the gitignore block lists for the outputs, without the base lines", () => {
|
|
155
|
+
expect(untrackedEntries(["claude"], treeOutputs(["claude"], false))).toEqual([
|
|
156
|
+
"AGENTS.md",
|
|
157
|
+
".mcp.json",
|
|
158
|
+
".claude/",
|
|
159
|
+
]);
|
|
160
|
+
expect(untrackedEntries(["claude"], treeOutputs(["claude"], true))).toEqual([]);
|
|
161
|
+
expect(untrackedEntries([], [file("AGENTS.md", false)])).toEqual(["AGENTS.md"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("adds the pattern of knip's ignore-free copies beside knip.json, sorted among the files", () => {
|
|
165
|
+
const outputs = [file("AGENTS.md", false), file("knip.json", false), file(".oxlintrc.json", false)];
|
|
166
|
+
expect(untrackedEntries([], outputs)).toEqual([
|
|
167
|
+
"AGENTS.md",
|
|
168
|
+
"knip.json",
|
|
169
|
+
"knip.unignored-*.json",
|
|
170
|
+
".oxlintrc.json",
|
|
171
|
+
".oxlintrc.unskipped-*.json",
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("the gitattributes block", () => {
|
|
177
|
+
it("sets LF and marks every tracked whole file output, sorted, with no link, untracked or block output", () => {
|
|
178
|
+
const outputs = [
|
|
179
|
+
file("bunfig.toml", true),
|
|
180
|
+
file("AGENTS.md", true),
|
|
181
|
+
file(".agents/agents/implementer-deep.md", true),
|
|
182
|
+
link(".claude/skills/ask", true),
|
|
183
|
+
file(".claude/settings.json", false),
|
|
184
|
+
file("references/INDEX.md", true, "block"),
|
|
185
|
+
file(".gitignore", true, "block"),
|
|
186
|
+
];
|
|
187
|
+
expect(gitattributesOf({}, outputs)).toBe(
|
|
188
|
+
`${ATTR_HEAD}.agents/agents/implementer-deep.md linguist-generated\nAGENTS.md linguist-generated\nbunfig.toml linguist-generated\n${IGNORE_END}`,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("sets LF alone when nothing is tracked, as in a consumer whose only tracked output is a block", () => {
|
|
193
|
+
expect(gitattributesOf({ hosts: ["claude"] }, treeOutputs(["claude"], false))).toBe(`${ATTR_HEAD}${IGNORE_END}`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("keeps the user's lines outside the block and replaces the block in place", () => {
|
|
197
|
+
const text = `*.png binary\n\n${ATTR_HEAD}stale linguist-generated\n${IGNORE_END}\n*.lock -diff\n`;
|
|
198
|
+
expect(gitattributesOf({ files: { ".gitattributes": text } }, [file("bunfig.toml", true)])).toBe(
|
|
199
|
+
`*.png binary\n\n${ATTR_HEAD}bunfig.toml linguist-generated\n${IGNORE_END}\n*.lock -diff\n`,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("files.readonlyInclude in .vscode/settings.json", () => {
|
|
205
|
+
const OUTPUTS = [file("bunfig.toml", true), file(".claude/settings.json", false), link(".claude/skills/ask", false)];
|
|
206
|
+
|
|
207
|
+
it("creates the file with the exact path of every whole file output, sorted, and no link", () => {
|
|
208
|
+
expect(settingsOf({}, OUTPUTS)).toBe(
|
|
209
|
+
'{\n "files.readonlyInclude": {\n ".claude/settings.json": true,\n "bunfig.toml": true\n }\n}\n',
|
|
210
|
+
);
|
|
211
|
+
expect(settingsOf({ files: { ".vscode/settings.json": " \n" } }, OUTPUTS)).toBe(settingsOf({}, OUTPUTS));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("leaves out block outputs and does not lock a user file inside a generated dir", () => {
|
|
215
|
+
const outputs = [
|
|
216
|
+
...OUTPUTS,
|
|
217
|
+
file(".gitignore", true, "block"),
|
|
218
|
+
file(".vscode/settings.json", true, "block"),
|
|
219
|
+
file("references/INDEX.md", true, "block"),
|
|
220
|
+
];
|
|
221
|
+
expect(readonlyOf(settingsOf({}, outputs))).toEqual([".claude/settings.json", "bunfig.toml"]);
|
|
222
|
+
expect(readonlyOf(settingsOf({}, outputs))).not.toContain(".claude");
|
|
223
|
+
expect(readonlyOf(settingsOf({}, outputs))).not.toContain(".claude/mine.md");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("adds the map to a commented file and keeps every other key and comment byte for byte", () => {
|
|
227
|
+
const before = '// my editor\n{\n "editor.tabSize": 4, // wide\n\t/* keep */ "files.autoSave": "off",\n}\n';
|
|
228
|
+
const after = settingsOf({ files: { ".vscode/settings.json": before } }, OUTPUTS);
|
|
229
|
+
expect(after.startsWith("// my editor\n{\n")).toBe(true);
|
|
230
|
+
expect(after.endsWith('\n "editor.tabSize": 4, // wide\n\t/* keep */ "files.autoSave": "off",\n}\n')).toBe(true);
|
|
231
|
+
expect(readonlyOf(after)).toEqual([".claude/settings.json", "bunfig.toml"]);
|
|
232
|
+
expect(parse(after)).toMatchObject({ "editor.tabSize": 4, "files.autoSave": "off" });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("replaces a map of the user's own, and keeps the keys around it as written", () => {
|
|
236
|
+
const before =
|
|
237
|
+
'{\n // lock\n "files.readonlyInclude": { "mine.txt": true, "bunfig.toml": false },\n "editor.tabSize": 2 /* last */\n}\n';
|
|
238
|
+
const after = settingsOf({ files: { ".vscode/settings.json": before } }, OUTPUTS);
|
|
239
|
+
expect(readonlyOf(after)).toEqual([".claude/settings.json", "bunfig.toml"]);
|
|
240
|
+
expect(parse(after)["files.readonlyInclude"]["bunfig.toml"]).toBe(true);
|
|
241
|
+
expect(after.startsWith("{\n // lock\n ")).toBe(true);
|
|
242
|
+
expect(after.endsWith('"editor.tabSize": 2 /* last */\n}\n')).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("is stable: a second pass over its own output changes nothing", () => {
|
|
246
|
+
const first = settingsOf({ files: { ".vscode/settings.json": '{\n // c\n "a": 1\n}\n' } }, OUTPUTS);
|
|
247
|
+
expect(settingsOf({ files: { ".vscode/settings.json": first } }, OUTPUTS)).toBe(first);
|
|
248
|
+
const replaced = settingsOf(
|
|
249
|
+
{ files: { ".vscode/settings.json": '{"files.readonlyInclude": {"x": true}}' } },
|
|
250
|
+
OUTPUTS,
|
|
251
|
+
);
|
|
252
|
+
expect(settingsOf({ files: { ".vscode/settings.json": replaced } }, OUTPUTS)).toBe(replaced);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("keeps CRLF line endings of a file that uses them", () => {
|
|
256
|
+
const after = settingsOf({ files: { ".vscode/settings.json": '{\r\n "a": 1\r\n}\r\n' } }, OUTPUTS);
|
|
257
|
+
expect(after.replaceAll("\r\n", "")).not.toContain("\n");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("writes an empty map when there is no whole output", () => {
|
|
261
|
+
expect(settingsOf({}, [])).toBe('{\n "files.readonlyInclude": {}\n}\n');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("refuses a file that is not valid JSONC, naming the error and offset, and one that is no object", () => {
|
|
265
|
+
expect(run({ files: { ".vscode/settings.json": '{ "a": }' } }, []).errors).toEqual([
|
|
266
|
+
".vscode/settings.json: invalid JSONC: ValueExpected at offset 7",
|
|
267
|
+
]);
|
|
268
|
+
for (const text of ["[]", "1", "null"]) {
|
|
269
|
+
expect(run({ files: { ".vscode/settings.json": text } }, []).errors).toEqual([
|
|
270
|
+
".vscode/settings.json: must hold a JSON object",
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("the golden fixture's outputs", () => {
|
|
277
|
+
const goldens = [...readGoldens().keys()];
|
|
278
|
+
const outputs = goldens.map((rel) => file(rel, false));
|
|
279
|
+
|
|
280
|
+
it("are ignored by host dir, read-only by exact path, and marked generated by none (a consumer commits none)", () => {
|
|
281
|
+
const fixture = { hosts: [...HOSTS] };
|
|
282
|
+
expect(gitignoreOf(fixture, outputs)).toBe(
|
|
283
|
+
block("AGENTS.md", ".mcp.json", ".agents/", ".claude/", ".codex/", ".cursor/", ".grok/"),
|
|
284
|
+
);
|
|
285
|
+
expect(readonlyOf(settingsOf(fixture, outputs))).toEqual([...goldens].sort());
|
|
286
|
+
expect(gitattributesOf(fixture, outputs)).toBe(`${ATTR_HEAD}${IGNORE_END}`);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("are marked generated one line each when the repo commits them", () => {
|
|
290
|
+
const tracked = goldens.map((rel) => file(rel, true));
|
|
291
|
+
const lines = gitattributesOf({ hosts: [...HOSTS] }, tracked).split("\n");
|
|
292
|
+
expect(lines.filter((line) => line.endsWith(" linguist-generated"))).toEqual(
|
|
293
|
+
[...goldens].sort().map((rel) => `${rel} linguist-generated`),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("listedFiles", () => {
|
|
299
|
+
it("lists tracked and untracked files git does not ignore, with extra ignore lines applied, sorted", () => {
|
|
300
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-listed-"));
|
|
301
|
+
try {
|
|
302
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
303
|
+
for (const rel of ["b.ts", "a/x.ts", "skip/y.ts", "ignored.log"]) {
|
|
304
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
305
|
+
fs.writeFileSync(path.join(root, rel), "x");
|
|
306
|
+
}
|
|
307
|
+
fs.writeFileSync(path.join(root, ".gitignore"), "*.log\n");
|
|
308
|
+
expect(listedFiles(root, ["skip/"])).toEqual([".gitignore", "a/x.ts", "b.ts"]);
|
|
309
|
+
expect(listedFiles(path.join(root, "missing"), [])).toBeNull();
|
|
310
|
+
} finally {
|
|
311
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("package outputs", () => {
|
|
317
|
+
const packageOutputs = (tracked: boolean): Output[] => [
|
|
318
|
+
file("packages/web/AGENTS.md", tracked),
|
|
319
|
+
file("packages/web/CLAUDE.md", tracked),
|
|
320
|
+
link("packages/web/.claude/skills/deploy", tracked),
|
|
321
|
+
file(".claude/rules/packages/web/x.md", tracked),
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
it("lists each untracked package output by its exact path, beside the collapsed host dir", () => {
|
|
325
|
+
expect(gitignoreOf({ hosts: ["claude"] }, packageOutputs(false))).toBe(
|
|
326
|
+
`${IGNORE_HEAD}node_modules/\npackages/web/.claude/skills/deploy\npackages/web/AGENTS.md\npackages/web/CLAUDE.md\n.claude/\n${IGNORE_END}`,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("marks tracked package files generated and read-only by exact path, and ignores nothing", () => {
|
|
331
|
+
const outputs = packageOutputs(true);
|
|
332
|
+
expect(gitignoreOf({ name: "@workerstack/repo", hosts: ["claude"] }, outputs)).toBe(`${IGNORE_HEAD}${IGNORE_END}`);
|
|
333
|
+
expect(gitattributesOf({}, outputs)).toContain(
|
|
334
|
+
"packages/web/AGENTS.md linguist-generated\npackages/web/CLAUDE.md linguist-generated\n",
|
|
335
|
+
);
|
|
336
|
+
const readonly = parse(settingsOf({}, outputs))["files.readonlyInclude"] as Record<string, boolean>;
|
|
337
|
+
expect(readonly["packages/web/AGENTS.md"]).toBe(true);
|
|
338
|
+
expect(readonly["packages/web/CLAUDE.md"]).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { applyEdits, modify, parse, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
5
|
+
import type { Host } from "../config.ts";
|
|
6
|
+
import { mergeBlock } from "../scripts/managed-text.ts";
|
|
7
|
+
import { HOST_TREES } from "./hosts.ts";
|
|
8
|
+
import type { Output, SyncContext } from "./outputs.ts";
|
|
9
|
+
import { KNIP_CONFIG_PATH, KNIP_UNIGNORED_GLOB } from "./knip-config.ts";
|
|
10
|
+
import { OXLINT_CONFIG_PATH, UNSKIPPED_CONFIG_GLOB } from "./oxlint-config.ts";
|
|
11
|
+
import { isSelfName } from "./stock.ts";
|
|
12
|
+
import type { Workspace } from "./workspace.ts";
|
|
13
|
+
|
|
14
|
+
/** What a pass over the generators' outputs returns: more outputs, or the errors that stopped it. */
|
|
15
|
+
export type PostPass = (ctx: SyncContext, outputs: readonly Output[]) => { outputs: Output[]; errors: string[] };
|
|
16
|
+
|
|
17
|
+
type FileOutput = Extract<Output, { kind: "file" }>;
|
|
18
|
+
type Result = { outputs: Output[]; errors: string[] };
|
|
19
|
+
|
|
20
|
+
const GITIGNORE_PATH = ".gitignore";
|
|
21
|
+
const GITATTRIBUTES_PATH = ".gitattributes";
|
|
22
|
+
const SETTINGS_PATH = ".vscode/settings.json";
|
|
23
|
+
/** Every path the git files pass plans, for a pass that runs before it and must name them. */
|
|
24
|
+
export const GIT_FILE_PATHS: readonly string[] = [GITIGNORE_PATH, GITATTRIBUTES_PATH, SETTINGS_PATH];
|
|
25
|
+
const BLOCK_BEGIN = "# >>> @workerstack/repo";
|
|
26
|
+
const BLOCK_END = "# <<< @workerstack/repo";
|
|
27
|
+
const IGNORE_BASE = [".tmp/", ".worktrees/", ".repo/tmp/", "coverage/"];
|
|
28
|
+
const DEPENDENCY_DIR = `${"node"}_modules/`;
|
|
29
|
+
const ATTRIBUTES_BASE = "* text=auto eol=lf";
|
|
30
|
+
const READONLY_KEY = "files.readonlyInclude";
|
|
31
|
+
|
|
32
|
+
const byPath = (a: string, b: string) => Number(a > b) - Number(a < b);
|
|
33
|
+
/** Sorts a leading dot away, so `AGENTS.md` stays before `.mcp.json` as the gitignore block always listed them. */
|
|
34
|
+
const byName = (a: string, b: string) => byPath(a.replace(/^\./, ""), b.replace(/^\./, ""));
|
|
35
|
+
|
|
36
|
+
/** The file outputs sync owns whole, which the user never writes into. */
|
|
37
|
+
function wholeFiles(outputs: readonly Output[]): FileOutput[] {
|
|
38
|
+
return outputs.flatMap((output) => (output.kind === "file" && output.managed === "whole" ? [output] : []));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What git must ignore for the untracked whole outputs: an output under a dir an enabled host owns collapses to
|
|
43
|
+
* the outermost such dir, any other is listed as its exact path, and the oxlint and knip configs bring the
|
|
44
|
+
* pattern of their one-run copies. Files first, then dirs, each sorted.
|
|
45
|
+
*/
|
|
46
|
+
export function untrackedEntries(hosts: readonly Host[], outputs: readonly Output[]): string[] {
|
|
47
|
+
const owned = hosts.flatMap((host) => HOST_TREES[host].owns).sort((a, b) => a.length - b.length);
|
|
48
|
+
const entries = new Set<string>();
|
|
49
|
+
for (const output of outputs) {
|
|
50
|
+
if (output.tracked || output.managed !== "whole") continue;
|
|
51
|
+
entries.add(owned.find((dir) => output.path.startsWith(dir)) ?? output.path);
|
|
52
|
+
}
|
|
53
|
+
// `repo check` writes a skip-free copy beside the oxlint config for one run.
|
|
54
|
+
if (entries.has(OXLINT_CONFIG_PATH)) entries.add(UNSKIPPED_CONFIG_GLOB);
|
|
55
|
+
// `repo check` writes an ignore-free copy beside the knip config for one run.
|
|
56
|
+
if (entries.has(KNIP_CONFIG_PATH)) entries.add(KNIP_UNIGNORED_GLOB);
|
|
57
|
+
const all = [...entries];
|
|
58
|
+
return [
|
|
59
|
+
...all.filter((entry) => !entry.endsWith("/")).sort(byName),
|
|
60
|
+
...all.filter((entry) => entry.endsWith("/")).sort(byPath),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The post-pass over the generators' outputs: the managed blocks of `.gitignore` and `.gitattributes`, and
|
|
66
|
+
* `files.readonlyInclude` in `.vscode/settings.json`. Block outputs are left out of all three.
|
|
67
|
+
*/
|
|
68
|
+
export const gitFiles: PostPass = (ctx, outputs) => {
|
|
69
|
+
const results = [gitignore(ctx, outputs), gitattributes(ctx, outputs), settings(ctx, outputs)];
|
|
70
|
+
return { outputs: results.flatMap((result) => result.outputs), errors: results.flatMap((result) => result.errors) };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** A block output in `rel`: sync's `body` between the markers, the rest of the file as the user wrote it. */
|
|
74
|
+
function blockFile(ctx: SyncContext, rel: string, body: string[]): Result {
|
|
75
|
+
const merged = mergeBlock(ctx.read(rel) ?? "", BLOCK_BEGIN, BLOCK_END, body.join("\n"));
|
|
76
|
+
if (!merged.ok) return { outputs: [], errors: [`${rel}: ${merged.error}`] };
|
|
77
|
+
return { outputs: [{ kind: "file", path: rel, content: merged.text, tracked: true, managed: "block" }], errors: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The lines of the managed `.gitignore` block: the base entries, the dependency dir and what the untracked outputs
|
|
82
|
+
* need. This repo keeps its own dependency line outside the block and commits its trees, so its untracked list is empty.
|
|
83
|
+
*/
|
|
84
|
+
export function ignoreBlockLines(
|
|
85
|
+
name: string | undefined,
|
|
86
|
+
hosts: readonly Host[],
|
|
87
|
+
outputs: readonly Output[],
|
|
88
|
+
): string[] {
|
|
89
|
+
const dependencies = isSelfName(name) ? [] : [DEPENDENCY_DIR];
|
|
90
|
+
return [...IGNORE_BASE, ...dependencies, ...untrackedEntries(hosts, outputs)];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gitignore(ctx: SyncContext, outputs: readonly Output[]): Result {
|
|
94
|
+
return blockFile(
|
|
95
|
+
ctx,
|
|
96
|
+
GITIGNORE_PATH,
|
|
97
|
+
ignoreBlockLines(ctx.workspace.manifest.name, ctx.config.agents.hosts, outputs),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function gitattributes(ctx: SyncContext, outputs: readonly Output[]): Result {
|
|
102
|
+
const generated = wholeFiles(outputs)
|
|
103
|
+
.filter((output) => output.tracked)
|
|
104
|
+
.map((output) => output.path)
|
|
105
|
+
.sort(byPath);
|
|
106
|
+
return blockFile(ctx, GITATTRIBUTES_PATH, [ATTRIBUTES_BASE, ...generated.map((rel) => `${rel} linguist-generated`)]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `files.readonlyInclude` set to every whole file output; the settings file's other keys and comments stay as written. */
|
|
110
|
+
function settings(ctx: SyncContext, outputs: readonly Output[]): Result {
|
|
111
|
+
const existing = ctx.read(SETTINGS_PATH);
|
|
112
|
+
const fresh = (existing ?? "").trim() === "";
|
|
113
|
+
const text = fresh ? "{}" : (existing as string);
|
|
114
|
+
const problems: ParseError[] = [];
|
|
115
|
+
const value: unknown = parse(text, problems, { allowTrailingComma: true });
|
|
116
|
+
if (problems.length > 0) {
|
|
117
|
+
const { error, offset } = problems[0] as ParseError;
|
|
118
|
+
return {
|
|
119
|
+
outputs: [],
|
|
120
|
+
errors: [`${SETTINGS_PATH}: invalid JSONC: ${printParseErrorCode(error)} at offset ${offset}`],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
124
|
+
return { outputs: [], errors: [`${SETTINGS_PATH}: must hold a JSON object`] };
|
|
125
|
+
}
|
|
126
|
+
const readonly = Object.fromEntries(
|
|
127
|
+
wholeFiles(outputs)
|
|
128
|
+
.map((output) => output.path)
|
|
129
|
+
.sort(byPath)
|
|
130
|
+
.map((rel) => [rel, true]),
|
|
131
|
+
);
|
|
132
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
133
|
+
// Inserted first, the new key leaves every neighbour's whitespace as written; appended, its formatting would rewrite the last one's.
|
|
134
|
+
const options = { formattingOptions: { insertSpaces: true, tabSize: 2, eol }, getInsertionIndex: () => 0 };
|
|
135
|
+
const edited = applyEdits(text, modify(text, [READONLY_KEY], readonly, options));
|
|
136
|
+
return {
|
|
137
|
+
outputs: [
|
|
138
|
+
{
|
|
139
|
+
kind: "file",
|
|
140
|
+
path: SETTINGS_PATH,
|
|
141
|
+
content: fresh ? `${edited}${eol}` : edited,
|
|
142
|
+
tracked: true,
|
|
143
|
+
managed: "block",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
errors: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Every file git sees under `root` (tracked, or untracked and not ignored) as sorted POSIX paths, with the
|
|
152
|
+
* `extraIgnores` lines applied on top of the ignore files already there. A deleted tracked file stays in the index,
|
|
153
|
+
* so it is listed too. Null outside a git repo or when git fails.
|
|
154
|
+
*/
|
|
155
|
+
export function listedFiles(root: string, extraIgnores: readonly string[]): string[] | null {
|
|
156
|
+
try {
|
|
157
|
+
const listed = execFileSync(
|
|
158
|
+
"git",
|
|
159
|
+
["ls-files", "--cached", "--others", "--exclude-standard", ...extraIgnores.flatMap((line) => ["-x", line]), "-z"],
|
|
160
|
+
{
|
|
161
|
+
cwd: root,
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
// An unmerged path is listed once per stage.
|
|
167
|
+
return [...new Set(listed.split("\0").filter((rel) => rel !== ""))].sort(byPath);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const NESTED_IGNORE = "ignore rules live in the root .gitignore (or a public package's own); move them there";
|
|
174
|
+
const NPM_IGNORE = 'npm reads package.json "files"; delete it';
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One `path: reason` line per ignore file git sees (tracked, or untracked and not ignored) that sync refuses: a
|
|
178
|
+
* `.gitignore` below the root outside a public package's root (its package.json lacks `"private": true`), and any
|
|
179
|
+
* `.npmignore`. `ignored` are the lines of the `.gitignore` block sync is about to write, which git applies on top of
|
|
180
|
+
* the ignore files already there, so a new repo is not judged by what its first sync would ignore. None outside a
|
|
181
|
+
* git repo or when git fails.
|
|
182
|
+
*/
|
|
183
|
+
export function strayIgnoreFiles(root: string, workspace: Workspace, ignored: readonly string[]): string[] {
|
|
184
|
+
const listed = listedFiles(root, ignored);
|
|
185
|
+
if (listed === null) return [];
|
|
186
|
+
const publicRoots = new Set(workspace.packages.filter((pkg) => pkg.manifest.private !== true).map((pkg) => pkg.dir));
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
for (const rel of listed) {
|
|
189
|
+
const name = path.posix.basename(rel);
|
|
190
|
+
const nested =
|
|
191
|
+
name === ".gitignore" && path.posix.dirname(rel) !== "." && !publicRoots.has(path.posix.dirname(rel));
|
|
192
|
+
if ((name === ".npmignore" || nested) && fs.existsSync(path.join(root, rel))) {
|
|
193
|
+
lines.push(`${rel}: ${name === ".npmignore" ? NPM_IGNORE : NESTED_IGNORE}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BASE_CONFIG } from "../fixtures/base-config.ts";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { afterEach, expect, it } from "vitest";
|
|
7
|
+
import type { Output, SyncContext } from "./outputs.ts";
|
|
8
|
+
import { runGit, standards } from "./standards.ts";
|
|
9
|
+
import { readEnv } from "../env.ts";
|
|
10
|
+
|
|
11
|
+
/** The git hooks the standards pass plans, and the git runner behind `core.hooksPath`. */
|
|
12
|
+
const temps: string[] = [];
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function tempDir(): string {
|
|
18
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-githook-")));
|
|
19
|
+
temps.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function context(packs: SyncContext["config"]["packs"], read: SyncContext["read"] = () => null): SyncContext {
|
|
24
|
+
return {
|
|
25
|
+
root: "/repo",
|
|
26
|
+
config: {
|
|
27
|
+
packs,
|
|
28
|
+
agents: { hosts: [], skills: [] },
|
|
29
|
+
...BASE_CONFIG,
|
|
30
|
+
},
|
|
31
|
+
workspace: { root: "/repo", manifest: { name: "app" }, packages: [] },
|
|
32
|
+
read,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hooks = (ctx: SyncContext): Output[] =>
|
|
37
|
+
standards(ctx, []).outputs.filter((output) => output.path.startsWith(".githooks/"));
|
|
38
|
+
|
|
39
|
+
/** Runs a planned hook with `sh` at a top level that holds a fake bin, the way git runs it, with an agent env set. */
|
|
40
|
+
function runHook(name: string, ...args: string[]) {
|
|
41
|
+
const root = tempDir();
|
|
42
|
+
fs.mkdirSync(path.join(root, "node_modules", ".bin"), { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(root, "node_modules", ".bin", "repo"), '#!/bin/sh\necho "repo $@"\n', { mode: 0o755 });
|
|
44
|
+
const hook = hooks(context(["standards"])).find((output) => output.path === `.githooks/${name}`);
|
|
45
|
+
if (hook?.kind !== "file") throw new Error(`no hook ${name}`);
|
|
46
|
+
fs.writeFileSync(path.join(root, "hook"), hook.content);
|
|
47
|
+
const env = { PATH: readEnv().PATH, CLAUDE_PROJECT_DIR: "/nowhere" };
|
|
48
|
+
return spawnSync("sh", ["hook", ...args], { cwd: root, env, encoding: "utf8" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it("plans pre-commit and commit-msg as tracked, executable files sync owns, and none without the pack", () => {
|
|
52
|
+
const shapes = hooks(context(["standards"])).map((output) => ({
|
|
53
|
+
path: output.path,
|
|
54
|
+
tracked: output.tracked,
|
|
55
|
+
managed: output.managed,
|
|
56
|
+
executable: output.kind === "file" && output.executable === true,
|
|
57
|
+
}));
|
|
58
|
+
expect(shapes).toEqual([
|
|
59
|
+
{ path: ".githooks/commit-msg", tracked: true, managed: "whole", executable: true },
|
|
60
|
+
{ path: ".githooks/pre-commit", tracked: true, managed: "whole", executable: true },
|
|
61
|
+
]);
|
|
62
|
+
expect(hooks(context(["agents", "docs"]))).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("pre-commit runs repo turn --staged from the bin of the checkout git runs it in", () => {
|
|
66
|
+
const r = runHook("pre-commit");
|
|
67
|
+
expect(r.stderr).toBe("");
|
|
68
|
+
expect(r.stdout).toBe("repo turn --staged\n");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("commit-msg runs repo commit-msg on the message file git passes", () => {
|
|
72
|
+
expect(runHook("commit-msg", ".git/COMMIT_EDITMSG").stdout).toBe("repo commit-msg .git/COMMIT_EDITMSG\n");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("refuses a hand-written hook at either path, and replaces one it generated", () => {
|
|
76
|
+
const at = (text: string) => (rel: string) => (rel === ".githooks/pre-commit" ? text : null);
|
|
77
|
+
expect(standards(context(["standards"], at("#!/bin/sh\nnpx lint-staged\n")), [])).toEqual({
|
|
78
|
+
outputs: [],
|
|
79
|
+
errors: [".githooks/pre-commit is hand-written; delete it so repo sync can own it"],
|
|
80
|
+
});
|
|
81
|
+
const generated = at("#!/bin/sh\n# Generated by repo sync. Do not edit.\nold\n");
|
|
82
|
+
expect(standards(context(["standards"], generated), []).errors).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("runGit returns git's output, or a null status with the reason when git cannot start", () => {
|
|
86
|
+
const root = tempDir();
|
|
87
|
+
const version = runGit(root, ["--version"]);
|
|
88
|
+
expect(version.status).toBe(0);
|
|
89
|
+
expect(version.stdout).toMatch(/^git version /);
|
|
90
|
+
expect(runGit(root, ["hash-object", "--stdin"], "x").stdout).toBe("c1b0730e0133447badcfd47fd144e254807b06e1\n");
|
|
91
|
+
const missing = runGit(path.join(root, "missing"), ["--version"]);
|
|
92
|
+
expect(missing).toMatchObject({ status: null, stdout: "" });
|
|
93
|
+
expect(missing.stderr).toMatch(/ENOENT/);
|
|
94
|
+
});
|