@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,168 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
4
|
+
import { lines, mergeSection } from "../scripts/managed-text.ts";
|
|
5
|
+
import { LESSONS_DIR, INDEX_NAME, lessonProblems, renderLessonsIndex } from "../scripts/lessons.ts";
|
|
6
|
+
import { nightly } from "./nightly.ts";
|
|
7
|
+
import { release } from "./release.ts";
|
|
8
|
+
import { releaseWorkflow } from "./release-workflow.ts";
|
|
9
|
+
import { mapSections } from "./maps.ts";
|
|
10
|
+
import { isSelfName } from "./stock.ts";
|
|
11
|
+
import { renderTrees } from "./trees.ts";
|
|
12
|
+
import type { PackageJson, Workspace, WorkspacePackage } from "./workspace.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One path repo sync owns, at a root-relative POSIX path. A `link` is a directory symlink (a junction on win32)
|
|
16
|
+
* to `target`, absolute or relative to the link's directory. `tracked` says whether the path is committed.
|
|
17
|
+
* `managed` says who owns the text: `whole` is sync's alone (marked generated, read-only in the editor); `block`
|
|
18
|
+
* is a file the user also writes, of which sync owns a marked section. An `executable` file is written mode 0755 on POSIX.
|
|
19
|
+
*/
|
|
20
|
+
export type Output =
|
|
21
|
+
| { kind: "file"; path: string; content: string; tracked: boolean; managed: Managed; executable?: boolean }
|
|
22
|
+
| { kind: "link"; path: string; target: string; tracked: boolean; managed: Managed };
|
|
23
|
+
export type Managed = "whole" | "block";
|
|
24
|
+
/** What a generator sees; `read` returns a root-relative file's text, or null when it is missing. */
|
|
25
|
+
export type SyncContext = {
|
|
26
|
+
root: string;
|
|
27
|
+
config: ResolvedConfig;
|
|
28
|
+
workspace: Workspace;
|
|
29
|
+
read(rel: string): string | null;
|
|
30
|
+
};
|
|
31
|
+
/** Computes outputs from the context alone; it never writes, so sync can validate everything first. */
|
|
32
|
+
export type Generator = (ctx: SyncContext) => { outputs: Output[]; errors: string[] };
|
|
33
|
+
|
|
34
|
+
const BUNFIG_PATH = "bunfig.toml";
|
|
35
|
+
const BUNFIG_HEADER = "# Generated by repo sync. Do not edit.";
|
|
36
|
+
// auto = "disable": with no node_modules above a script, bun would fetch a missing import from the registry.
|
|
37
|
+
const BUNFIG = `${BUNFIG_HEADER}\n[install]\nlinker = "hoisted"\nauto = "disable"\n`;
|
|
38
|
+
|
|
39
|
+
const INDEX_PATH = "references/INDEX.md";
|
|
40
|
+
const INDEX_NEW = "# References\n";
|
|
41
|
+
const STACK_SCOPE = "@workerstack/";
|
|
42
|
+
const DEPENDENCY_DIR = `${"node"}_modules/`;
|
|
43
|
+
const DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const;
|
|
44
|
+
|
|
45
|
+
/** bunfig.toml, refusing to replace one that lacks the generated header. */
|
|
46
|
+
export const bunfig: Generator = (ctx) => {
|
|
47
|
+
const existing = ctx.read(BUNFIG_PATH);
|
|
48
|
+
if (existing !== null && lines(existing)[0] !== BUNFIG_HEADER) {
|
|
49
|
+
return { outputs: [], errors: [`${BUNFIG_PATH} is hand-written; delete it so repo sync can own it`] };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
outputs: [{ kind: "file", path: BUNFIG_PATH, content: BUNFIG, tracked: true, managed: "whole" }],
|
|
53
|
+
errors: [],
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The repo:packages and repo:stack sections of references/INDEX.md, under the docs pack; in this repo also the
|
|
59
|
+
* repo:skills and repo:refs maps of its own skills and references.
|
|
60
|
+
*/
|
|
61
|
+
export const index: Generator = (ctx) => {
|
|
62
|
+
if (!ctx.config.packs.includes("docs")) return { outputs: [], errors: [] };
|
|
63
|
+
|
|
64
|
+
const errors: string[] = [];
|
|
65
|
+
const sections: [name: string, body: string][] = [];
|
|
66
|
+
const packageIndexes: Output[] = [];
|
|
67
|
+
const { packages } = ctx.workspace;
|
|
68
|
+
if (packages.length > 0) {
|
|
69
|
+
const entries: string[] = [];
|
|
70
|
+
for (const { dir, manifest } of packages) {
|
|
71
|
+
const text = ctx.read(`${dir}/${INDEX_PATH}`);
|
|
72
|
+
if (text === null) {
|
|
73
|
+
errors.push(`${dir} has no ${INDEX_PATH}; every workspace package needs one`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const title = lines(text)
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.find((line) => line !== "");
|
|
79
|
+
const description = title === undefined ? "" : `: ${title.replace(/^# /, "")}`;
|
|
80
|
+
entries.push(`- [${manifest.name || dir}](../${dir}/${INDEX_PATH})${description}`);
|
|
81
|
+
const deps = mergeSection(text, "deps", depsLines(dir, manifest, packages).join("\n"));
|
|
82
|
+
if (deps.ok)
|
|
83
|
+
packageIndexes.push({
|
|
84
|
+
kind: "file",
|
|
85
|
+
path: `${dir}/${INDEX_PATH}`,
|
|
86
|
+
content: deps.text,
|
|
87
|
+
tracked: true,
|
|
88
|
+
managed: "block",
|
|
89
|
+
});
|
|
90
|
+
else errors.push(`${dir}/${INDEX_PATH}: ${deps.error}`);
|
|
91
|
+
}
|
|
92
|
+
sections.push(["packages", entries.join("\n")]);
|
|
93
|
+
}
|
|
94
|
+
const stack = stackNames(ctx.workspace).map((name) => `- [${name}](../node_modules/${name}/${INDEX_PATH})`);
|
|
95
|
+
sections.push(["stack", stack.join("\n")]);
|
|
96
|
+
if (isSelfName(ctx.workspace.manifest.name)) {
|
|
97
|
+
const maps = mapSections(ctx.root);
|
|
98
|
+
sections.push(["skills", maps.skills], ["refs", maps.refs]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let text = ctx.read(INDEX_PATH) ?? INDEX_NEW;
|
|
102
|
+
for (const [name, body] of sections) {
|
|
103
|
+
const merged = mergeSection(text, name, body);
|
|
104
|
+
if (merged.ok) text = merged.text;
|
|
105
|
+
else errors.push(`${INDEX_PATH}: ${merged.error}`);
|
|
106
|
+
}
|
|
107
|
+
if (errors.length > 0) return { outputs: [], errors };
|
|
108
|
+
return {
|
|
109
|
+
outputs: [{ kind: "file", path: INDEX_PATH, content: text, tracked: true, managed: "block" }, ...packageIndexes],
|
|
110
|
+
errors: [],
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The repo:deps lines of the INDEX of package `dir`: one link per dependency (any field) that is a workspace package,
|
|
116
|
+
* then one per other `@workerstack/*` package, each group sorted by name. A workspace package wins over the installed
|
|
117
|
+
* one, so a workspace `@workerstack/x` is linked in place.
|
|
118
|
+
*/
|
|
119
|
+
function depsLines(dir: string, manifest: PackageJson, packages: readonly WorkspacePackage[]): string[] {
|
|
120
|
+
const names = new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(manifest[field] ?? {})));
|
|
121
|
+
const inWorkspace = new Map(
|
|
122
|
+
packages.flatMap((pkg) => (pkg.manifest.name === undefined ? [] : [[pkg.manifest.name, pkg.dir] as const])),
|
|
123
|
+
);
|
|
124
|
+
const link = (name: string, target: string) =>
|
|
125
|
+
`- [${name}](${path.posix.relative(`${dir}/references`, `${target}/${INDEX_PATH}`)})`;
|
|
126
|
+
const workspace = [...names].filter((name) => inWorkspace.has(name)).sort();
|
|
127
|
+
const stack = [...names].filter((name) => !inWorkspace.has(name) && name.startsWith(STACK_SCOPE)).sort();
|
|
128
|
+
return [
|
|
129
|
+
...workspace.map((name) => link(name, inWorkspace.get(name) as string)),
|
|
130
|
+
...stack.map((name) => link(name, `${DEPENDENCY_DIR}${name}`)),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Under the agents pack, `.repo/lessons/INDEX.md` for the lessons under `.repo/lessons`, and one error per lesson that breaks the format. */
|
|
135
|
+
export const lessons: Generator = (ctx) => {
|
|
136
|
+
if (!ctx.config.packs.includes("agents")) return { outputs: [], errors: [] };
|
|
137
|
+
const errors = lessonProblems(ctx.root);
|
|
138
|
+
if (!fs.existsSync(path.join(ctx.root, LESSONS_DIR))) return { outputs: [], errors };
|
|
139
|
+
const output: Output = {
|
|
140
|
+
kind: "file",
|
|
141
|
+
path: `${LESSONS_DIR}/${INDEX_NAME}`,
|
|
142
|
+
content: renderLessonsIndex(ctx.root),
|
|
143
|
+
tracked: true,
|
|
144
|
+
managed: "whole",
|
|
145
|
+
};
|
|
146
|
+
return { outputs: [output], errors };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** The agent trees of the enabled hosts, under the agents pack; a failed render is its errors and no outputs. */
|
|
150
|
+
export const trees: Generator = (ctx) => {
|
|
151
|
+
if (!ctx.config.packs.includes("agents")) return { outputs: [], errors: [] };
|
|
152
|
+
const result = renderTrees(ctx);
|
|
153
|
+
return result.ok ? { outputs: result.outputs, errors: [] } : { outputs: [], errors: result.errors };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Every generator, in the order sync runs and reports them. */
|
|
157
|
+
export const GENERATORS: readonly Generator[] = [bunfig, index, lessons, trees, release, nightly, releaseWorkflow];
|
|
158
|
+
|
|
159
|
+
/** Every `@workerstack/*` dependency name of the root and its packages, deduped and sorted. */
|
|
160
|
+
function stackNames(workspace: Workspace): string[] {
|
|
161
|
+
const names = new Set<string>();
|
|
162
|
+
for (const manifest of [workspace.manifest, ...workspace.packages.map((pkg) => pkg.manifest)]) {
|
|
163
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
164
|
+
for (const name of Object.keys(manifest[field] ?? {})) if (name.startsWith(STACK_SCOPE)) names.add(name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...names].sort();
|
|
168
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LintSkip } from "../config.ts";
|
|
3
|
+
import { oxlintConfig } from "./oxlint-config.ts";
|
|
4
|
+
import type { PackageJson, Workspace } from "./workspace.ts";
|
|
5
|
+
|
|
6
|
+
const workspace = (manifest: PackageJson, packages: Workspace["packages"] = []): Workspace => ({
|
|
7
|
+
root: "/repo",
|
|
8
|
+
manifest,
|
|
9
|
+
packages,
|
|
10
|
+
});
|
|
11
|
+
const configOf = (ws: Workspace, skips: readonly LintSkip[] = []): unknown => {
|
|
12
|
+
const built = oxlintConfig(ws, skips);
|
|
13
|
+
if (!built.ok) throw new Error(built.errors.join("\n"));
|
|
14
|
+
return JSON.parse(built.value);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SIZE = { skipBlankLines: true, skipComments: true };
|
|
18
|
+
const OFF = Object.fromEntries(
|
|
19
|
+
["correctness", "suspicious", "pedantic", "perf", "style", "restriction", "nursery"].map((name) => [name, "off"]),
|
|
20
|
+
);
|
|
21
|
+
const REPO_RULES = {
|
|
22
|
+
"repo/as-needs-reason": "error",
|
|
23
|
+
"repo/boundaries": "error",
|
|
24
|
+
"repo/class-file": "error",
|
|
25
|
+
"repo/config-file": "error",
|
|
26
|
+
"repo/entry-file": "error",
|
|
27
|
+
"repo/env-in-config": "error",
|
|
28
|
+
"repo/filename-matches-export": "error",
|
|
29
|
+
"repo/index-file": "error",
|
|
30
|
+
"repo/no-commented-out-code": "error",
|
|
31
|
+
"repo/no-mock-of-subject": "error",
|
|
32
|
+
"repo/no-module-side-effects": "error",
|
|
33
|
+
"repo/no-module-state": "error",
|
|
34
|
+
"repo/no-side-effect-import": "error",
|
|
35
|
+
"repo/no-v8-ignore": "error",
|
|
36
|
+
"repo/skip-on-platform": "error",
|
|
37
|
+
"repo/todo-needs-id": "error",
|
|
38
|
+
"repo/types-file": "error",
|
|
39
|
+
"unicorn/prefer-node-protocol": "error",
|
|
40
|
+
};
|
|
41
|
+
const BASE_RULES = {
|
|
42
|
+
"eslint/max-lines": ["error", { max: 200, ...SIZE }],
|
|
43
|
+
"eslint/max-lines-per-function": ["error", { max: 30, ...SIZE }],
|
|
44
|
+
"eslint/max-params": ["error", { max: 3 }],
|
|
45
|
+
"eslint/complexity": ["error", { max: 10 }],
|
|
46
|
+
"eslint/max-depth": ["error", { max: 4 }],
|
|
47
|
+
"eslint/no-shadow": "error",
|
|
48
|
+
"eslint/no-else-return": "error",
|
|
49
|
+
"eslint/no-nested-ternary": "error",
|
|
50
|
+
"eslint/no-param-reassign": "error",
|
|
51
|
+
"eslint/no-await-in-loop": "error",
|
|
52
|
+
"eslint/require-await": "error",
|
|
53
|
+
"eslint/no-console": "error",
|
|
54
|
+
"import/no-cycle": "error",
|
|
55
|
+
"import/no-default-export": "error",
|
|
56
|
+
"typescript/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "allow" }],
|
|
57
|
+
"typescript/no-explicit-any": "error",
|
|
58
|
+
"typescript/no-non-null-assertion": "error",
|
|
59
|
+
"typescript/no-floating-promises": "error",
|
|
60
|
+
"typescript/no-unnecessary-condition": "error",
|
|
61
|
+
"typescript/no-unsafe-argument": "error",
|
|
62
|
+
"typescript/no-unsafe-assignment": "error",
|
|
63
|
+
"typescript/switch-exhaustiveness-check": "error",
|
|
64
|
+
};
|
|
65
|
+
const TEST_OVERRIDE = {
|
|
66
|
+
files: ["**/*.{test,spec}.{ts,tsx}", "**/*.e2e.{ts,tsx}"],
|
|
67
|
+
rules: {
|
|
68
|
+
"repo/no-module-side-effects": "off",
|
|
69
|
+
"repo/no-module-state": "off",
|
|
70
|
+
"vitest/expect-expect": "error",
|
|
71
|
+
"vitest/no-conditional-tests": "error",
|
|
72
|
+
"vitest/no-focused-tests": "error",
|
|
73
|
+
"vitest/no-disabled-tests": "error",
|
|
74
|
+
"vitest/warn-todo": "error",
|
|
75
|
+
"eslint/no-console": "off",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const CONFIG_OVERRIDE = {
|
|
79
|
+
files: ["**/*.config.ts"],
|
|
80
|
+
rules: { "import/no-default-export": "off", "repo/no-module-side-effects": "off" },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
describe("oxlintConfig base", () => {
|
|
84
|
+
it("loads the repo plugin, turns every rule on at error, all categories off, type-aware, tests and configs relaxed, fixtures and reject files ignored", () => {
|
|
85
|
+
expect(configOf(workspace({ name: "app" }))).toEqual({
|
|
86
|
+
plugins: ["eslint", "typescript", "unicorn", "oxc", "import", "vitest"],
|
|
87
|
+
jsPlugins: [{ name: "repo", specifier: "@workerstack/repo/lint" }],
|
|
88
|
+
categories: OFF,
|
|
89
|
+
options: { typeAware: true, respectEslintDisableDirectives: false },
|
|
90
|
+
ignorePatterns: ["**/fixtures/**", "**/*.reject.{ts,tsx}"],
|
|
91
|
+
rules: { ...REPO_RULES, ...BASE_RULES },
|
|
92
|
+
overrides: [TEST_OVERRIDE, CONFIG_OVERRIDE],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("oxlintConfig per package", () => {
|
|
98
|
+
it("lets each bin of the root and every package run at import, and each ./lint export default-export and assemble its plugin", () => {
|
|
99
|
+
const ws = workspace({ name: "app", bin: "./bin/app.ts", exports: { "./lint": { default: "./lint/index.ts" } } }, [
|
|
100
|
+
{ dir: "packages/cli", manifest: { bin: { cli: "./bin/cli.ts", "cli-dev": "bin/dev.ts" } } },
|
|
101
|
+
{ dir: "packages/rules", manifest: { exports: { ".": "./index.ts", "./lint": "./cli/lint/index.ts" } } },
|
|
102
|
+
]);
|
|
103
|
+
expect(configOf(ws)).toMatchObject({
|
|
104
|
+
overrides: [
|
|
105
|
+
TEST_OVERRIDE,
|
|
106
|
+
CONFIG_OVERRIDE,
|
|
107
|
+
{
|
|
108
|
+
files: ["bin/app.ts", "packages/cli/bin/cli.ts", "packages/cli/bin/dev.ts"],
|
|
109
|
+
rules: { "repo/no-module-side-effects": "off" },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
files: ["lint/index.ts", "packages/rules/cli/lint/index.ts"],
|
|
113
|
+
rules: { "import/no-default-export": "off", "repo/index-file": "off" },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("reads no override from a bin or ./lint export of another shape", () => {
|
|
120
|
+
const ws = workspace({ bin: 7, exports: "./index.ts" }, [
|
|
121
|
+
{ dir: "a", manifest: { bin: { a: 1 }, exports: { "./lint": { import: "./lint.ts" } } } },
|
|
122
|
+
{ dir: "b", manifest: { bin: ["./bin/b.ts"], exports: { "./lint": 3 } } },
|
|
123
|
+
{ dir: "c", manifest: { bin: null, exports: null } },
|
|
124
|
+
]);
|
|
125
|
+
expect(configOf(ws)).toHaveProperty("overrides", [TEST_OVERRIDE, CONFIG_OVERRIDE]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const REASON = "the parsed config cache lives for the process";
|
|
130
|
+
const APP = workspace({ name: "app", bin: "./bin/app.ts" });
|
|
131
|
+
|
|
132
|
+
it("oxlintConfig writes one override per skip after every other, turning off only the named rules for the named files", () => {
|
|
133
|
+
const skips = [
|
|
134
|
+
{ files: ["src/cache.ts"], rules: ["repo/no-module-state"], reason: REASON },
|
|
135
|
+
{ files: ["src/**/*.ts", "lib/*.ts"], rules: ["eslint/max-lines", "vitest/no-disabled-tests"], reason: REASON },
|
|
136
|
+
];
|
|
137
|
+
expect(configOf(APP, skips)).toEqual(
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
rules: { ...REPO_RULES, ...BASE_RULES },
|
|
140
|
+
overrides: [
|
|
141
|
+
TEST_OVERRIDE,
|
|
142
|
+
CONFIG_OVERRIDE,
|
|
143
|
+
{ files: ["bin/app.ts"], rules: { "repo/no-module-side-effects": "off" } },
|
|
144
|
+
{ files: ["src/cache.ts"], rules: { "repo/no-module-state": "off" } },
|
|
145
|
+
{ files: ["src/**/*.ts", "lib/*.ts"], rules: { "eslint/max-lines": "off", "vitest/no-disabled-tests": "off" } },
|
|
146
|
+
],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("oxlintConfig refuses a rule the generated config does not run, naming the entry", () => {
|
|
152
|
+
const skips = [
|
|
153
|
+
{ files: ["src/a.ts"], rules: ["repo/no-module-state"], reason: REASON },
|
|
154
|
+
{ files: ["src/b.ts"], rules: ["eslint/no-console", "repo/no-modul-state"], reason: REASON },
|
|
155
|
+
{ files: ["src/c.ts"], rules: ["no-module-state"], reason: REASON },
|
|
156
|
+
];
|
|
157
|
+
expect(oxlintConfig(APP, skips)).toEqual({
|
|
158
|
+
ok: false,
|
|
159
|
+
errors: [
|
|
160
|
+
'workerstack.config.ts: lint.skip[1].rules: "repo/no-modul-state" is not a rule the generated config runs',
|
|
161
|
+
'workerstack.config.ts: lint.skip[2].rules: "no-module-state" is not a rule the generated config runs',
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("oxlintConfig accepts the script rules in a skip and leaves them out of its overrides, writing none for an entry of only them", () => {
|
|
167
|
+
const skips = [
|
|
168
|
+
{ files: ["src/retry.ts"], rules: ["repo/magic-number", "repo/magic-string"], reason: REASON },
|
|
169
|
+
{ files: ["src/user.ts"], rules: ["repo/guard-rejects"], reason: REASON },
|
|
170
|
+
{ files: ["src/cache.ts"], rules: ["repo/magic-number", "repo/no-module-state"], reason: REASON },
|
|
171
|
+
];
|
|
172
|
+
expect(configOf(APP, skips)).toHaveProperty("overrides", [
|
|
173
|
+
TEST_OVERRIDE,
|
|
174
|
+
CONFIG_OVERRIDE,
|
|
175
|
+
{ files: ["bin/app.ts"], rules: { "repo/no-module-side-effects": "off" } },
|
|
176
|
+
{ files: ["src/cache.ts"], rules: { "repo/no-module-state": "off" } },
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CONFIG_FILE, type LintSkip } from "../config.ts";
|
|
3
|
+
import { binsOf, isRecord } from "../lint/manifest.ts";
|
|
4
|
+
import { SCRIPT_RULES } from "../lint/script-rules.ts";
|
|
5
|
+
import type { PackageJson, Workspace } from "./workspace.ts";
|
|
6
|
+
|
|
7
|
+
/** The generated config sync writes at the root. */
|
|
8
|
+
export const OXLINT_CONFIG_PATH = ".oxlintrc.json";
|
|
9
|
+
/** The skip-free copies `repo check` writes beside it for one run, each named for its process. */
|
|
10
|
+
export const UNSKIPPED_CONFIG_GLOB = ".oxlintrc.unskipped-*.json";
|
|
11
|
+
export const unskippedConfigPath = (pid: number): string => UNSKIPPED_CONFIG_GLOB.replace("*", String(pid));
|
|
12
|
+
/** Fixture trees are copies of other repos' files: never linted or formatted. */
|
|
13
|
+
export const FIXTURES = "**/fixtures/**";
|
|
14
|
+
/** Reject files hold code that must not compile, checked by `repo check` and `repo turn` instead of linted. */
|
|
15
|
+
const REJECT_FILES = "**/*.reject.{ts,tsx}";
|
|
16
|
+
const TESTS = ["**/*.{test,spec}.{ts,tsx}", "**/*.e2e.{ts,tsx}"];
|
|
17
|
+
const CONFIGS = "**/*.config.ts";
|
|
18
|
+
const CATEGORIES = ["correctness", "suspicious", "pedantic", "perf", "style", "restriction", "nursery"] as const;
|
|
19
|
+
/** Size limits count code only: blank lines and comments are free. */
|
|
20
|
+
const CODE_LINES = { skipBlankLines: true, skipComments: true };
|
|
21
|
+
const OFF_SIDE_EFFECTS = { "repo/no-module-side-effects": "off" };
|
|
22
|
+
const OFF_DEFAULT_EXPORT = { "import/no-default-export": "off" };
|
|
23
|
+
|
|
24
|
+
/** Every rule the base turns on; each is an error, none a warning. The `repo/` rules are `@workerstack/repo/lint`'s. */
|
|
25
|
+
const RULES: Record<string, string | [string, Record<string, unknown>]> = {
|
|
26
|
+
"repo/as-needs-reason": "error",
|
|
27
|
+
"repo/boundaries": "error",
|
|
28
|
+
"repo/class-file": "error",
|
|
29
|
+
"repo/config-file": "error",
|
|
30
|
+
"repo/entry-file": "error",
|
|
31
|
+
"repo/env-in-config": "error",
|
|
32
|
+
"repo/filename-matches-export": "error",
|
|
33
|
+
"repo/index-file": "error",
|
|
34
|
+
"repo/no-commented-out-code": "error",
|
|
35
|
+
"repo/no-mock-of-subject": "error",
|
|
36
|
+
"repo/no-module-side-effects": "error",
|
|
37
|
+
"repo/no-module-state": "error",
|
|
38
|
+
"repo/no-side-effect-import": "error",
|
|
39
|
+
"repo/no-v8-ignore": "error",
|
|
40
|
+
"repo/skip-on-platform": "error",
|
|
41
|
+
"repo/todo-needs-id": "error",
|
|
42
|
+
"repo/types-file": "error",
|
|
43
|
+
"unicorn/prefer-node-protocol": "error",
|
|
44
|
+
"eslint/max-lines": ["error", { max: 200, ...CODE_LINES }],
|
|
45
|
+
"eslint/max-lines-per-function": ["error", { max: 30, ...CODE_LINES }],
|
|
46
|
+
"eslint/max-params": ["error", { max: 3 }],
|
|
47
|
+
"eslint/complexity": ["error", { max: 10 }],
|
|
48
|
+
"eslint/max-depth": ["error", { max: 4 }],
|
|
49
|
+
"eslint/no-shadow": "error",
|
|
50
|
+
"eslint/no-else-return": "error",
|
|
51
|
+
"eslint/no-nested-ternary": "error",
|
|
52
|
+
"eslint/no-param-reassign": "error",
|
|
53
|
+
"eslint/no-await-in-loop": "error",
|
|
54
|
+
"eslint/require-await": "error",
|
|
55
|
+
"eslint/no-console": "error",
|
|
56
|
+
"import/no-cycle": "error",
|
|
57
|
+
"import/no-default-export": "error",
|
|
58
|
+
// `repo/as-needs-reason` checks `x as T`; this refuses `<T>x`, which takes no reason comment. Object literals stay
|
|
59
|
+
// free so `{} as T` reaches the reason check too.
|
|
60
|
+
"typescript/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "allow" }],
|
|
61
|
+
"typescript/no-explicit-any": "error",
|
|
62
|
+
"typescript/no-non-null-assertion": "error",
|
|
63
|
+
"typescript/no-floating-promises": "error",
|
|
64
|
+
"typescript/no-unnecessary-condition": "error",
|
|
65
|
+
"typescript/no-unsafe-argument": "error",
|
|
66
|
+
"typescript/no-unsafe-assignment": "error",
|
|
67
|
+
"typescript/switch-exhaustiveness-check": "error",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Test rules, only in test files, where module-scope setup and printing are allowed. */
|
|
71
|
+
const TEST_RULES: Record<string, string> = {
|
|
72
|
+
"repo/no-module-state": "off",
|
|
73
|
+
"vitest/expect-expect": "error",
|
|
74
|
+
"vitest/no-conditional-tests": "error",
|
|
75
|
+
"vitest/no-focused-tests": "error",
|
|
76
|
+
"vitest/no-disabled-tests": "error",
|
|
77
|
+
"vitest/warn-todo": "error",
|
|
78
|
+
"eslint/no-console": "off",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The file a manifest's `./lint` export names, as a path or as its `default` condition. */
|
|
82
|
+
function lintExportOf(manifest: PackageJson): unknown[] {
|
|
83
|
+
const target = isRecord(manifest.exports) ? manifest.exports["./lint"] : undefined;
|
|
84
|
+
return [isRecord(target) ? target.default : target];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Every string `pick` finds in the root's and each package's manifest, as a root-relative POSIX path, sorted. */
|
|
88
|
+
function filesOf(workspace: Workspace, pick: (manifest: PackageJson) => unknown[]): string[] {
|
|
89
|
+
const all = [{ dir: "", manifest: workspace.manifest }, ...workspace.packages];
|
|
90
|
+
const files = all.flatMap(({ dir, manifest }) =>
|
|
91
|
+
pick(manifest).flatMap((file) => (typeof file === "string" ? [path.posix.join(dir, file)] : [])),
|
|
92
|
+
);
|
|
93
|
+
return [...new Set(files)].sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A bin runs its main at import, and a plugin entry must default-export the plugin it assembles from its rules: each
|
|
98
|
+
* gets an override when there is one.
|
|
99
|
+
*/
|
|
100
|
+
function packageOverrides(workspace: Workspace): { files: string[]; rules: Record<string, string> }[] {
|
|
101
|
+
const overrides = [
|
|
102
|
+
{ files: filesOf(workspace, (manifest) => binsOf(manifest.bin)), rules: OFF_SIDE_EFFECTS },
|
|
103
|
+
{ files: filesOf(workspace, lintExportOf), rules: { ...OFF_DEFAULT_EXPORT, "repo/index-file": "off" } },
|
|
104
|
+
];
|
|
105
|
+
return overrides.filter((override) => override.files.length > 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The generated config's text, or why a skip could not be turned into an override. */
|
|
109
|
+
export type Built = { ok: true; value: string } | { ok: false; errors: string[] };
|
|
110
|
+
|
|
111
|
+
const isOn = (setting: string | [string, unknown]) => (Array.isArray(setting) ? setting[0] : setting) === "error";
|
|
112
|
+
|
|
113
|
+
/** Every rule key the generated config sets to `error`, in the base or in the test override. */
|
|
114
|
+
const runRules = (): Set<string> =>
|
|
115
|
+
new Set(
|
|
116
|
+
[...Object.entries(RULES), ...Object.entries(TEST_RULES)].flatMap(([rule, setting]) =>
|
|
117
|
+
isOn(setting) ? [rule] : [],
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/** One line per skipped rule name neither the generated config nor a script check runs, naming its entry. */
|
|
122
|
+
function unknownRules(skips: readonly LintSkip[]): string[] {
|
|
123
|
+
const run = runRules();
|
|
124
|
+
return skips.flatMap((skip, index) =>
|
|
125
|
+
skip.rules
|
|
126
|
+
.filter((rule) => !run.has(rule) && !SCRIPT_RULES.includes(rule))
|
|
127
|
+
.map((rule) => `${CONFIG_FILE}: lint.skip[${index}].rules: "${rule}" is not a rule the generated config runs`),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Each `lint.skip` entry as an override turning off its oxlint rules for its files; the script rules are no oxlint
|
|
133
|
+
* rules (`repo check` and `repo turn` drop their hits), so an entry of only those writes none.
|
|
134
|
+
*/
|
|
135
|
+
function skipOverrides(skips: readonly LintSkip[]): { files: readonly string[]; rules: Record<string, string> }[] {
|
|
136
|
+
return skips.flatMap((skip) => {
|
|
137
|
+
const rules = skip.rules.filter((rule) => !SCRIPT_RULES.includes(rule));
|
|
138
|
+
return rules.length === 0
|
|
139
|
+
? []
|
|
140
|
+
: [{ files: skip.files, rules: Object.fromEntries(rules.map((rule) => [rule, "off"])) }];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The oxlint config: the repo plugin and the named rules, type-aware, with eslint-disable comments ignored. Each
|
|
146
|
+
* `lint.skip` entry is an override written last, so it turns off only its rules for only its files.
|
|
147
|
+
*/
|
|
148
|
+
export function oxlintConfig(workspace: Workspace, skips: readonly LintSkip[]): Built {
|
|
149
|
+
const errors = unknownRules(skips);
|
|
150
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
151
|
+
const config = {
|
|
152
|
+
plugins: ["eslint", "typescript", "unicorn", "oxc", "import", "vitest"],
|
|
153
|
+
// A specifier, not a path: oxlint resolves it from the config's node_modules, or from NODE_PATH (`repo lint`).
|
|
154
|
+
jsPlugins: [{ name: "repo", specifier: "@workerstack/repo/lint" }],
|
|
155
|
+
categories: Object.fromEntries(CATEGORIES.map((category) => [category, "off"])),
|
|
156
|
+
options: { typeAware: true, respectEslintDisableDirectives: false },
|
|
157
|
+
ignorePatterns: [FIXTURES, REJECT_FILES],
|
|
158
|
+
rules: RULES,
|
|
159
|
+
overrides: [
|
|
160
|
+
{ files: TESTS, rules: { ...OFF_SIDE_EFFECTS, ...TEST_RULES } },
|
|
161
|
+
// vitest and workerstack configs must default-export, and call their define helper at import.
|
|
162
|
+
{ files: [CONFIGS], rules: { ...OFF_DEFAULT_EXPORT, ...OFF_SIDE_EFFECTS } },
|
|
163
|
+
...packageOverrides(workspace),
|
|
164
|
+
...skipOverrides(skips),
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
return { ok: true, value: `${JSON.stringify(config, null, 2)}\n` };
|
|
168
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HEADER, context } from "../fixtures/release-context.ts";
|
|
3
|
+
import { GENERATORS } from "./outputs.ts";
|
|
4
|
+
import { release } from "./release.ts";
|
|
5
|
+
|
|
6
|
+
const CI_PATH = ".github/workflows/ci.yml";
|
|
7
|
+
|
|
8
|
+
describe("release gates", () => {
|
|
9
|
+
it("is a generator", () => {
|
|
10
|
+
expect(GENERATORS).toContain(release);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("writes nothing with the pack off", () => {
|
|
14
|
+
expect(release(context(["agents", "standards", "docs"]))).toEqual({ outputs: [], errors: [] });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("rewrites a ci.yml that has the generated header on its first line", () => {
|
|
18
|
+
const result = release(context(["release"], {}, { [CI_PATH]: `${HEADER}\nname: old\n` }));
|
|
19
|
+
expect(result.errors).toEqual([]);
|
|
20
|
+
expect(result.outputs).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("refuses a hand-written ci.yml", () => {
|
|
24
|
+
expect(release(context(["release"], {}, { [CI_PATH]: "name: ci\n" }))).toEqual({
|
|
25
|
+
outputs: [],
|
|
26
|
+
errors: [".github/workflows/ci.yml is hand-written; delete it so repo sync can own it"],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BASE_CONFIG } from "../fixtures/base-config.ts";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { REPORT_FILE } from "../scripts/ci-skips.constants.ts";
|
|
4
|
+
import { release } from "./release.ts";
|
|
5
|
+
|
|
6
|
+
const output = release({
|
|
7
|
+
root: "/repo",
|
|
8
|
+
config: { packs: ["release"], agents: { hosts: [], skills: [] }, ...BASE_CONFIG },
|
|
9
|
+
workspace: { root: "/repo", manifest: {}, packages: [] },
|
|
10
|
+
read: () => null,
|
|
11
|
+
}).outputs[0];
|
|
12
|
+
const yaml = output?.kind === "file" ? output.content : "";
|
|
13
|
+
const value = (key: string) => new RegExp(`^\\s+(?:- )?${key}: (.+)$`, "m").exec(yaml)?.[1] ?? "";
|
|
14
|
+
const glob = (pattern: string) => new RegExp(`^${pattern.replaceAll("*", ".*")}$`);
|
|
15
|
+
|
|
16
|
+
it("each OS's report file name is one the skips check reads", () => {
|
|
17
|
+
const file = value("REPORT");
|
|
18
|
+
for (const os of ["Linux", "Windows", "macOS"]) {
|
|
19
|
+
expect(file.replace("${{ runner.os }}", os)).toMatch(REPORT_FILE);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("the uploaded path is the report the gates wrote, and each matrix artifact matches the download pattern", () => {
|
|
24
|
+
expect(yaml).toContain(`path: ${value("REPORT")}`);
|
|
25
|
+
const pattern = value("pattern");
|
|
26
|
+
for (const os of ["ubuntu-latest", "windows-latest", "macos-latest"]) {
|
|
27
|
+
expect(`ci-report-${os}`).toMatch(glob(pattern));
|
|
28
|
+
}
|
|
29
|
+
expect(yaml).toContain("name: ci-report-${{ matrix.os }}");
|
|
30
|
+
expect(yaml).toContain("merge-multiple: true");
|
|
31
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CHECKOUT_STEP, CI_HEADER, INSTALL_STEP, SETUP_BUN_STEP } from "./release.constants.ts";
|
|
2
|
+
|
|
3
|
+
/** The generated header, which must be the first line of a release.yml sync owns. */
|
|
4
|
+
export const RELEASE_HEADER: string = CI_HEADER;
|
|
5
|
+
export const RELEASE_PATH: string = ".github/workflows/release.yml";
|
|
6
|
+
|
|
7
|
+
/** Action pin, a full commit SHA with its release as the comment. */
|
|
8
|
+
const SETUP_NODE: string = "actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0";
|
|
9
|
+
/** Trusted publishing needs npm 11.5.1 and Node 22.14; dist-tags under it need npm 11.21. */
|
|
10
|
+
const NPM_VERSION: string = "12.2.2";
|
|
11
|
+
const NODE_VERSION: string = "24";
|
|
12
|
+
export const GATE_TIMEOUT_MINUTES: number = 60;
|
|
13
|
+
export const PUBLISH_TIMEOUT_MINUTES: number = 25;
|
|
14
|
+
export const GITHUB_TIMEOUT_MINUTES: number = 10;
|
|
15
|
+
|
|
16
|
+
/** The tag every job works on: the pushed tag, or the one given by hand. */
|
|
17
|
+
const TAG_ENV: string = " TAG: ${{ inputs.tag || github.ref_name }}";
|
|
18
|
+
const TOKEN_ENV: string = " GH_TOKEN: ${{ github.token }}";
|
|
19
|
+
const CHECKOUT_REF: string = " ref: ${{ inputs.tag || github.ref }}";
|
|
20
|
+
|
|
21
|
+
/** Every job checks out the tag, not the default branch a dispatch runs on. */
|
|
22
|
+
export const CHECKOUT_TAG_STEP: string = `${CHECKOUT_STEP}
|
|
23
|
+
with:
|
|
24
|
+
${CHECKOUT_REF}`;
|
|
25
|
+
export const CHECKOUT_TAG_FULL_STEP: string = `${CHECKOUT_TAG_STEP}
|
|
26
|
+
fetch-depth: 0`;
|
|
27
|
+
export const BUN_STEPS: readonly string[] = [SETUP_BUN_STEP, INSTALL_STEP];
|
|
28
|
+
export const NODE_STEPS: readonly string[] = [
|
|
29
|
+
` - uses: ${SETUP_NODE}
|
|
30
|
+
with:
|
|
31
|
+
node-version: "${NODE_VERSION}"
|
|
32
|
+
package-manager-cache: false`,
|
|
33
|
+
` - run: npm install --global npm@${NPM_VERSION}`,
|
|
34
|
+
];
|
|
35
|
+
export const GATE_ENV: string = ` env:\n${TAG_ENV}\n${TOKEN_ENV}`;
|
|
36
|
+
export const PUBLISH_ENV: string = ` env:\n${TAG_ENV}`;
|
|
37
|
+
export const GATE_PERMISSIONS: string = " permissions:\n actions: read\n contents: read";
|
|
38
|
+
export const PUBLISH_PERMISSIONS: string = " permissions:\n contents: read\n id-token: write";
|
|
39
|
+
export const GITHUB_PERMISSIONS: string = " permissions:\n contents: write";
|
|
40
|
+
|
|
41
|
+
/** The workflow text before the jobs; `%NAME%` is the package name, which the tags are prefixed with. */
|
|
42
|
+
export const PREAMBLE: string = `name: release
|
|
43
|
+
|
|
44
|
+
on:
|
|
45
|
+
push:
|
|
46
|
+
tags:
|
|
47
|
+
- "%NAME%@*"
|
|
48
|
+
workflow_dispatch:
|
|
49
|
+
inputs:
|
|
50
|
+
tag:
|
|
51
|
+
description: "Existing tag to release, e.g. %NAME%@1.0.0"
|
|
52
|
+
required: true
|
|
53
|
+
|
|
54
|
+
concurrency:
|
|
55
|
+
group: release-\${{ inputs.tag || github.ref_name }}
|
|
56
|
+
cancel-in-progress: false
|
|
57
|
+
|
|
58
|
+
permissions:
|
|
59
|
+
contents: read
|
|
60
|
+
|
|
61
|
+
jobs:`;
|