@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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `repo check`'s workflow lines: actionlint, pinned to one release, run over `.github/workflows` under the release
|
|
3
|
+
* pack. The binary is fetched once into the state home, checked against its pinned sha256 and cached; a download that
|
|
4
|
+
* fails or does not match is a problem line, never a cached file.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import {
|
|
12
|
+
ACTIONLINT_FLAGS,
|
|
13
|
+
ACTIONLINT_VERSION,
|
|
14
|
+
ARCH_NAMES,
|
|
15
|
+
BINARY_MODE,
|
|
16
|
+
CACHE_DIR,
|
|
17
|
+
DEFAULT_SYSTEM_ROOT,
|
|
18
|
+
EXIT_PROBLEMS,
|
|
19
|
+
FETCH_TIMEOUT_MS,
|
|
20
|
+
HASHES,
|
|
21
|
+
MAX_OUTPUT_BYTES,
|
|
22
|
+
OS_NAMES,
|
|
23
|
+
RELEASE_URL,
|
|
24
|
+
SHOWN_HASH,
|
|
25
|
+
WINDOWS_PLATFORM,
|
|
26
|
+
WINDOWS_TAR,
|
|
27
|
+
WORKFLOW_EXTENSIONS,
|
|
28
|
+
WORKFLOWS_DIR,
|
|
29
|
+
} from "./actionlint.constants.ts";
|
|
30
|
+
import { findRoot, loadConfig } from "./config.ts";
|
|
31
|
+
import { childEnv } from "./env.ts";
|
|
32
|
+
import { toolEnv, type Captured } from "./scripts/lint.ts";
|
|
33
|
+
import { stateHome } from "./scripts/setup.ts";
|
|
34
|
+
|
|
35
|
+
/** What `repo check` prints of actionlint: problem lines that fail it. */
|
|
36
|
+
export type ActionlintCheck = { problems: string[] };
|
|
37
|
+
/** Runs `command` with `args`; the default spawns it and captures its output. */
|
|
38
|
+
export type Spawn = (command: string, args: string[], options: { cwd: string; env: NodeJS.ProcessEnv }) => Captured;
|
|
39
|
+
/** Everything `checkActionlintAt` reaches outside the repo through, each replaceable by a test. */
|
|
40
|
+
export type ActionlintDeps = {
|
|
41
|
+
fetch?: (url: string) => Promise<Uint8Array>;
|
|
42
|
+
spawn?: Spawn;
|
|
43
|
+
cacheDir?: string;
|
|
44
|
+
platform?: string;
|
|
45
|
+
arch?: string;
|
|
46
|
+
hashes?: Record<string, string>;
|
|
47
|
+
};
|
|
48
|
+
/** The deps with every default filled in. */
|
|
49
|
+
type Resolved = ActionlintDeps & Required<Pick<ActionlintDeps, "fetch" | "spawn" | "hashes">>;
|
|
50
|
+
type Binary = { ok: true; path: string } | { ok: false; problem: string };
|
|
51
|
+
type Bytes = { ok: true; bytes: Uint8Array } | { ok: false; problem: string };
|
|
52
|
+
/** What an install needs: the archive's name and key, where the binary goes and the injected edges. */
|
|
53
|
+
type Plan = { name: string; key: string; target: string; platform: string; deps: Resolved };
|
|
54
|
+
|
|
55
|
+
const NONE: ActionlintCheck = { problems: [] };
|
|
56
|
+
const itemsSchema = () =>
|
|
57
|
+
z.array(
|
|
58
|
+
z.object({ message: z.string(), filepath: z.string(), line: z.number(), column: z.number(), kind: z.string() }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** The archive at `url`, or a thrown error for a failed request. */
|
|
62
|
+
export async function fetchArchive(url: string): Promise<Uint8Array> {
|
|
63
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
64
|
+
if (!response.ok) throw new Error(`${url} answered ${response.status}`);
|
|
65
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const spawnCaptured: Spawn = (command, args, { cwd, env }) => {
|
|
69
|
+
const result = spawnSync(command, args, { cwd, env, encoding: "utf8", maxBuffer: MAX_OUTPUT_BYTES });
|
|
70
|
+
// A command that cannot start leaves stdout and stderr unset at runtime, whatever the types say.
|
|
71
|
+
if (result.error !== undefined) return { code: 1, stdout: "", stderr: `${result.error.message}\n` };
|
|
72
|
+
return { code: result.status ?? 1, stdout: result.stdout, stderr: result.stderr };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The workflow files under `root`, relative and sorted; none when the directory is missing. */
|
|
76
|
+
function workflowsOf(root: string): string[] {
|
|
77
|
+
const dir = path.join(root, WORKFLOWS_DIR);
|
|
78
|
+
if (!fs.existsSync(dir)) return [];
|
|
79
|
+
const names = fs.readdirSync(dir).filter((name) => WORKFLOW_EXTENSIONS.includes(path.extname(name)));
|
|
80
|
+
return names.toSorted().map((name) => `${WORKFLOWS_DIR}/${name}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The archive's bytes when they hash to the pinned value; otherwise the problem line. */
|
|
84
|
+
async function download(plan: Plan): Promise<Bytes> {
|
|
85
|
+
const { name, key, deps } = plan;
|
|
86
|
+
let bytes: Uint8Array;
|
|
87
|
+
try {
|
|
88
|
+
bytes = await deps.fetch(`${RELEASE_URL}/${name}`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return { ok: false, problem: `could not fetch ${ACTIONLINT_VERSION} (${why}); connect once so repo can cache it` };
|
|
92
|
+
}
|
|
93
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
94
|
+
const expected = deps.hashes[key] ?? "";
|
|
95
|
+
if (actual === expected) return { ok: true, bytes };
|
|
96
|
+
const [a, e] = [actual, expected].map((hash) => `${hash.slice(0, SHOWN_HASH)}…`);
|
|
97
|
+
return { ok: false, problem: `${name} has sha256 ${a}, expected ${e}; nothing was cached` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Unpacks `archive` into `into` with tar; on windows the system tar by full path, which reads a zip. */
|
|
101
|
+
function unpack(plan: Plan, archive: string, into: string): Captured {
|
|
102
|
+
const system = childEnv().SystemRoot ?? DEFAULT_SYSTEM_ROOT;
|
|
103
|
+
const tar = plan.platform === WINDOWS_PLATFORM ? `${system}${WINDOWS_TAR}` : "tar";
|
|
104
|
+
const spawn = plan.deps.spawn;
|
|
105
|
+
return spawn(tar, ["-xf", archive, "-C", into], { cwd: into, env: toolEnv(childEnv()) });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Fetches, verifies, unpacks and renames the binary into place; a problem line when any step fails. */
|
|
109
|
+
async function install(plan: Plan): Promise<Binary> {
|
|
110
|
+
const dir = path.dirname(plan.target);
|
|
111
|
+
const archive = path.join(dir, `download-${process.pid}-${plan.name}`);
|
|
112
|
+
const extracted = path.join(dir, `unpack-${process.pid}`);
|
|
113
|
+
try {
|
|
114
|
+
const got = await download(plan);
|
|
115
|
+
if (!got.ok) return got;
|
|
116
|
+
fs.writeFileSync(archive, got.bytes);
|
|
117
|
+
fs.mkdirSync(extracted, { recursive: true });
|
|
118
|
+
const tarred = unpack(plan, archive, extracted);
|
|
119
|
+
if (tarred.code !== 0) return { ok: false, problem: `could not unpack ${plan.name}: ${tarred.stderr.trim()}` };
|
|
120
|
+
const unpacked = path.join(extracted, path.basename(plan.target));
|
|
121
|
+
fs.chmodSync(unpacked, BINARY_MODE);
|
|
122
|
+
fs.renameSync(unpacked, plan.target);
|
|
123
|
+
return { ok: true, path: plan.target };
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(archive, { force: true });
|
|
126
|
+
fs.rmSync(extracted, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The cached binary, installing it first when it is missing. */
|
|
131
|
+
async function binaryFor(deps: Resolved): Promise<Binary> {
|
|
132
|
+
const platform = deps.platform ?? process.platform;
|
|
133
|
+
const arch = deps.arch ?? process.arch;
|
|
134
|
+
const [osName, archName] = [OS_NAMES[platform], ARCH_NAMES[arch]];
|
|
135
|
+
if (osName === undefined || archName === undefined) {
|
|
136
|
+
const build = `${ACTIONLINT_VERSION} build for ${platform} ${arch}`;
|
|
137
|
+
return { ok: false, problem: `no ${build}; run repo check on linux, macOS or Windows` };
|
|
138
|
+
}
|
|
139
|
+
const cacheDir = deps.cacheDir ?? path.join(stateHome(), CACHE_DIR, ACTIONLINT_VERSION);
|
|
140
|
+
const target = path.join(cacheDir, platform === WINDOWS_PLATFORM ? "actionlint.exe" : "actionlint");
|
|
141
|
+
if (fs.existsSync(target)) return { ok: true, path: target };
|
|
142
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
143
|
+
const key = `${osName}_${archName}`;
|
|
144
|
+
const name = `actionlint_${ACTIONLINT_VERSION}_${key}${platform === WINDOWS_PLATFORM ? ".zip" : ".tar.gz"}`;
|
|
145
|
+
return await install({ name, key, target, platform, deps });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One line per item of actionlint's json, or the failure line when the run or its output is not as expected. */
|
|
149
|
+
function linesOf(ran: Captured): string[] {
|
|
150
|
+
if (ran.code === 0) return [];
|
|
151
|
+
const failure = `actionlint: exited ${ran.code}: ${ran.stderr.trim()}`;
|
|
152
|
+
if (ran.code !== EXIT_PROBLEMS) return [failure];
|
|
153
|
+
try {
|
|
154
|
+
const items = itemsSchema().parse(JSON.parse(ran.stdout));
|
|
155
|
+
return items.map((i) => `actionlint: ${i.filepath}:${i.line}:${i.column}: ${i.message} [${i.kind}]`);
|
|
156
|
+
} catch {
|
|
157
|
+
return [failure];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `repo check`'s actionlint lines for the repo at `cwd`: none without a config, without the release pack or without a
|
|
163
|
+
* workflow file, and then no download either.
|
|
164
|
+
*/
|
|
165
|
+
export async function checkActionlintAt(cwd: string, deps: ActionlintDeps = {}): Promise<ActionlintCheck> {
|
|
166
|
+
const root = findRoot(cwd);
|
|
167
|
+
if (root === null) return NONE;
|
|
168
|
+
const loaded = await loadConfig(root);
|
|
169
|
+
if (!loaded.ok || !loaded.config.packs.includes("release")) return NONE;
|
|
170
|
+
const files = workflowsOf(root);
|
|
171
|
+
if (files.length === 0) return NONE;
|
|
172
|
+
const io = { fetch: fetchArchive, spawn: spawnCaptured, hashes: HASHES, ...deps };
|
|
173
|
+
const binary = await binaryFor(io);
|
|
174
|
+
if (!binary.ok) return { problems: [`actionlint: ${binary.problem}`] };
|
|
175
|
+
const args = [...ACTIONLINT_FLAGS, ...files];
|
|
176
|
+
const ran = io.spawn(binary.path, args, { cwd: root, env: toolEnv(childEnv()) });
|
|
177
|
+
return { problems: linesOf(ran) };
|
|
178
|
+
}
|
package/cli/bin.test.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BIN, type Bin, main } from "./bin.ts";
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/** A bin whose registry answers `code`, whose `cli/main.ts` runs `start`, and whose exit is recorded. */
|
|
9
|
+
function bin(code: number | null, start: () => Promise<void> = async () => {}) {
|
|
10
|
+
return {
|
|
11
|
+
dispatch: vi.fn<Bin["dispatch"]>(() => Promise.resolve(code)),
|
|
12
|
+
load: vi.fn(() => Promise.resolve({ start })),
|
|
13
|
+
exit: vi.fn<Bin["exit"]>(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("main", () => {
|
|
18
|
+
it("exits with the code of a registry command or hook, never loading cli/main.ts", async () => {
|
|
19
|
+
const deps = bin(3);
|
|
20
|
+
await main(["hook", "turn"], deps);
|
|
21
|
+
expect(deps.dispatch).toHaveBeenCalledWith(["hook", "turn"]);
|
|
22
|
+
expect([deps.load.mock.calls.length, deps.exit.mock.calls]).toEqual([0, [[3]]]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("runs cli/main.ts when the registry names nothing, then exits with the code it set", async () => {
|
|
26
|
+
const start = vi.fn(async () => {});
|
|
27
|
+
const deps = bin(null, start);
|
|
28
|
+
await main(["init"], deps);
|
|
29
|
+
expect([start.mock.calls.length, deps.exit.mock.calls]).toEqual([1, [[undefined]]]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each([
|
|
33
|
+
[new Error("no manifest"), "repo: no manifest\n"],
|
|
34
|
+
["a string", "repo: a string\n"],
|
|
35
|
+
])("prints a thrown %j as repo: <message> and exits 1", async (thrown, printed) => {
|
|
36
|
+
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
37
|
+
const deps = bin(null, () => Promise.reject(thrown));
|
|
38
|
+
await main(["init"], deps);
|
|
39
|
+
expect([stderr.mock.calls, deps.exit.mock.calls]).toEqual([[[printed]], [[1]]]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("BIN", () => {
|
|
44
|
+
it("is what main runs by default: the registry's code goes to the process's exit", async () => {
|
|
45
|
+
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
46
|
+
const exit = vi.spyOn(process, "exit").mockImplementation(() => undefined /* a spy, so the run goes on */ as never);
|
|
47
|
+
await main(["hook"]);
|
|
48
|
+
expect([stderr.mock.calls.length, exit.mock.calls]).toEqual([1, [[1]]]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("loads cli/main.ts lazily, its start among its exports", async () => {
|
|
52
|
+
expect(typeof (await BIN.load()).start).toBe("function");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/cli/bin.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { dispatch } from "./commands.ts";
|
|
2
|
+
|
|
3
|
+
/** What the bin runs on and how it ends; tests pass stubs. */
|
|
4
|
+
export type Bin = {
|
|
5
|
+
/** Runs a registry command or hook and returns its exit code, or null when `cli/main.ts` owns argv. */
|
|
6
|
+
dispatch: (argv: readonly string[]) => Promise<number | null>;
|
|
7
|
+
/** Loads `cli/main.ts`, only once the registry has named nothing. */
|
|
8
|
+
load: () => Promise<{ start: () => Promise<void> }>;
|
|
9
|
+
exit: (code?: number) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** The registry, a lazy `cli/main.ts`, and the process's own exit. */
|
|
13
|
+
export const BIN: Readonly<Bin> = {
|
|
14
|
+
dispatch,
|
|
15
|
+
load: () => import("./main.ts"),
|
|
16
|
+
exit: (code) => process.exit(code),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** The exit code of a registry command or hook, or undefined once `cli/main.ts` has run and set its own. */
|
|
20
|
+
async function run(argv: readonly string[], deps: Readonly<Bin>): Promise<number | undefined> {
|
|
21
|
+
const code = await deps.dispatch(argv);
|
|
22
|
+
if (code !== null) return code;
|
|
23
|
+
await (await deps.load()).start();
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `repo`: registry commands and hooks first, so they never load the init, sync and prompt code in `cli/main.ts`.
|
|
29
|
+
* Exits once the command settles, as each script did when run directly, so a hook never waits on a stray handle; a
|
|
30
|
+
* thrown error prints `repo: <message>` and exits 1.
|
|
31
|
+
*/
|
|
32
|
+
export async function main(argv: readonly string[], deps: Readonly<Bin> = BIN): Promise<void> {
|
|
33
|
+
await run(argv, deps).then(
|
|
34
|
+
(code) => deps.exit(code),
|
|
35
|
+
(err: unknown) => {
|
|
36
|
+
process.stderr.write(`repo: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
37
|
+
deps.exit(1);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { checkChangelog } from "./check-changelog.ts";
|
|
3
|
+
import { removePlanted, tempDir, write } from "./fixtures/planted-repo.ts";
|
|
4
|
+
|
|
5
|
+
afterEach(removePlanted);
|
|
6
|
+
|
|
7
|
+
const BAD = "# Changelog\n\n## [Unreleased]\n\n### Improved\n\n- x\n\n## [0.2.0]\n";
|
|
8
|
+
const GOOD = "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- x\n\n## [0.2.0] - 2026-11-01\n";
|
|
9
|
+
|
|
10
|
+
function root(files: Record<string, unknown>): string {
|
|
11
|
+
const dir = tempDir();
|
|
12
|
+
write(dir, files);
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("checkChangelog", () => {
|
|
17
|
+
it("holds a public root's CHANGELOG.md to Keep a Changelog, one line per problem", () => {
|
|
18
|
+
const lines = checkChangelog(root({ "package.json": { name: "app" }, "CHANGELOG.md": BAD }));
|
|
19
|
+
expect(lines).toEqual([
|
|
20
|
+
'CHANGELOG.md:5: "### Improved" is not a section; use Added, Changed, Deprecated, Removed, Fixed or Security',
|
|
21
|
+
'CHANGELOG.md:9: "## [0.2.0]" needs a date; write ## [0.2.0] - YYYY-MM-DD',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("passes a public root with a good changelog", () => {
|
|
26
|
+
expect(checkChangelog(root({ "package.json": { name: "app" }, "CHANGELOG.md": GOOD }))).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("names a missing CHANGELOG.md in a public root", () => {
|
|
30
|
+
expect(checkChangelog(root({ "package.json": { name: "app" } }))).toEqual([
|
|
31
|
+
"CHANGELOG.md is missing; add one in the Keep a Changelog format",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("checks nothing in a private root, or one without a readable manifest", () => {
|
|
36
|
+
expect(checkChangelog(root({ "package.json": { name: "app", private: true }, "CHANGELOG.md": BAD }))).toEqual([]);
|
|
37
|
+
expect(checkChangelog(root({ "CHANGELOG.md": BAD }))).toEqual([]);
|
|
38
|
+
expect(checkChangelog(root({ "package.json": "[", "CHANGELOG.md": BAD }))).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { changelogFormatProblems } from "./release/changelog.ts";
|
|
4
|
+
import { CHANGELOG_FILE } from "./release/release.constants.ts";
|
|
5
|
+
import { ownerLookup } from "./tdd-scope.ts";
|
|
6
|
+
|
|
7
|
+
/** The `repo check` lines for the root's CHANGELOG.md; none for a private root or one without a readable manifest. */
|
|
8
|
+
export function checkChangelog(root: string): string[] {
|
|
9
|
+
if (!ownerLookup(root)("package.json").public) return [];
|
|
10
|
+
const file = path.join(root, CHANGELOG_FILE);
|
|
11
|
+
return changelogFormatProblems(fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null);
|
|
12
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { checkComments, commentProblems, sourceFiles } from "./check-comments.ts";
|
|
7
|
+
import { captureBun } from "./scripts/lint.ts";
|
|
8
|
+
|
|
9
|
+
const temps: string[] = [];
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const LINT = (word: string) =>
|
|
15
|
+
`"${word}" switches a lint rule off; fix the code, or add a lint.skip entry with a reason`;
|
|
16
|
+
const IGNORE = '"@ts-ignore" hides a type error; fix the type, or write @ts-expect-error with a reason';
|
|
17
|
+
const NOCHECK = '"@ts-nocheck" turns type checking off for the file; fix the types instead';
|
|
18
|
+
const EXPECT = '"@ts-expect-error" needs a reason: // @ts-expect-error <why this line must not compile>';
|
|
19
|
+
|
|
20
|
+
/** A temp dir holding `files`. */
|
|
21
|
+
function plant(files: Record<string, string>): string {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-comments-"));
|
|
23
|
+
temps.push(root);
|
|
24
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
25
|
+
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(root, rel), text);
|
|
27
|
+
}
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const git = (root: string, ...args: string[]) => execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
32
|
+
|
|
33
|
+
/** The problems of one planted file. */
|
|
34
|
+
const problemsOf = (text: string, rel = "a.ts") => commentProblems(plant({ [rel]: text }), [rel]);
|
|
35
|
+
|
|
36
|
+
it("names each refused comment with its file and line, in the order of the files given", () => {
|
|
37
|
+
const root = plant({
|
|
38
|
+
"src/a.ts":
|
|
39
|
+
"export const a = 1;\n\n// oxlint-disable-next-line repo/no-module-state\nlet b = 2;\n\nb++;\n// @ts-ignore\n",
|
|
40
|
+
"src/b.ts": "// @ts-nocheck\n".concat("\n".repeat(7), "// @ts-expect-error\nexport const c: number = 'c';\n"),
|
|
41
|
+
});
|
|
42
|
+
expect(commentProblems(root, ["src/a.ts", "src/b.ts"])).toEqual([
|
|
43
|
+
`src/a.ts:3: ${LINT("oxlint-disable-next-line")}`,
|
|
44
|
+
`src/a.ts:7: ${IGNORE}`,
|
|
45
|
+
`src/b.ts:1: ${NOCHECK}`,
|
|
46
|
+
`src/b.ts:9: ${EXPECT}`,
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
["// eslint-disable\n", 1, "eslint-disable"],
|
|
52
|
+
["// oxlint-disable no-debugger -- legacy\n", 1, "oxlint-disable"],
|
|
53
|
+
["debugger; // eslint-disable-line\n", 1, "eslint-disable-line"],
|
|
54
|
+
["//eslint-disable-next-line\n", 1, "eslint-disable-next-line"],
|
|
55
|
+
["/* eslint-disable no-debugger */\n", 1, "eslint-disable"],
|
|
56
|
+
["/*eslint-disable*/\n", 1, "eslint-disable"],
|
|
57
|
+
["/*\n oxlint-disable */\n", 2, "oxlint-disable"],
|
|
58
|
+
["const s = 1;\n\n/* eslint-disable-next-line */\n", 3, "eslint-disable-next-line"],
|
|
59
|
+
])("refuses the lint directive in %j", (text, line, word) => {
|
|
60
|
+
expect(problemsOf(text)).toEqual([`a.ts:${line}: ${LINT(word)}`]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it.each([
|
|
64
|
+
["/// @ts-ignore\n", 1, IGNORE],
|
|
65
|
+
["/* @ts-ignore */ const a = 1;\n", 1, IGNORE],
|
|
66
|
+
["// @ts-ignored, as TypeScript reads any word that starts so\n", 1, IGNORE],
|
|
67
|
+
["/**\n * Some prose first.\n * @ts-ignore */\n", 3, IGNORE],
|
|
68
|
+
["#!/usr/bin/env bun\n// @TS-NOCHECK\n", 2, NOCHECK],
|
|
69
|
+
["// leading\n/* also leading */ //@ts-nocheck: why\n", 2, NOCHECK],
|
|
70
|
+
["// @ts-expect-error parsed above\n", 1, EXPECT],
|
|
71
|
+
["/* @ts-expect-error */\n", 1, EXPECT],
|
|
72
|
+
["/**\n * @ts-expect-error */\n", 2, EXPECT],
|
|
73
|
+
])("refuses the TypeScript directive in %j", (text, line, message) => {
|
|
74
|
+
expect(problemsOf(text)).toEqual([`a.ts:${line}: ${message}`]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
'// @ts-expect-error "lint" is not a pack\n',
|
|
79
|
+
"/* @ts-expect-error the schema allows no pack named lint */\n",
|
|
80
|
+
'export const s = "// oxlint-disable";\n',
|
|
81
|
+
"/** never write eslint-disable here */\n",
|
|
82
|
+
"/** eslint-disable */\n",
|
|
83
|
+
"// eslint-disabled\n",
|
|
84
|
+
"// eslint-disable-next-line-- no space, so oxlint reads no directive\n",
|
|
85
|
+
"// ESLINT-DISABLE\n",
|
|
86
|
+
"// see @ts-ignore in the handbook\n",
|
|
87
|
+
"/** @ts-ignore\n * on a line before the last, which TypeScript does not read\n */\n",
|
|
88
|
+
'import "./b.ts";\n// @ts-nocheck\n',
|
|
89
|
+
"/* @ts-nocheck */\n",
|
|
90
|
+
"// @ts-nocheckers\n",
|
|
91
|
+
])("passes %j", (text) => {
|
|
92
|
+
expect(problemsOf(text)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("reads the multi-line block form as real tsc does: honored only on the block's last line", () => {
|
|
96
|
+
const root = plant({
|
|
97
|
+
"last.ts": "/**\n * Some prose first.\n * @ts-ignore */\nexport const a: number = 'a';\n",
|
|
98
|
+
"first.ts": "/** @ts-ignore\n * more prose\n */\nexport const b: number = 'b';\n",
|
|
99
|
+
});
|
|
100
|
+
const tsc = path.join(path.dirname(Bun.resolveSync("typescript/package.json", import.meta.dirname)), "bin/tsc");
|
|
101
|
+
const run = (rel: string) => captureBun([tsc, "--noEmit", "--strict", rel], root).code;
|
|
102
|
+
expect({ last: run("last.ts"), first: run("first.ts") }).toEqual({ last: 0, first: 2 });
|
|
103
|
+
expect(commentProblems(root, ["last.ts", "first.ts"])).toEqual([`last.ts:3: ${IGNORE}`]);
|
|
104
|
+
}, 30_000);
|
|
105
|
+
|
|
106
|
+
it("scans the comments a file with a parse error still returns", () => {
|
|
107
|
+
expect(problemsOf("// @ts-ignore\nconst = ;\n")).toEqual([`a.ts:1: ${IGNORE}`]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("skips a listed file that cannot be read, and files that are not source", () => {
|
|
111
|
+
const root = plant({ "a.md": "// @ts-ignore\n", "fixtures/a.ts": "// @ts-ignore\n" });
|
|
112
|
+
expect(commentProblems(root, ["gone.ts", "a.md", "fixtures/a.ts"])).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** A git repo with a committed file, an untracked one, and files `sourceFiles` leaves out. */
|
|
116
|
+
function repo(): string {
|
|
117
|
+
const root = plant({
|
|
118
|
+
".gitignore": "dist\n",
|
|
119
|
+
"src/a.ts": "",
|
|
120
|
+
"dist/out.js": "",
|
|
121
|
+
"cli/fixtures/f.ts": "",
|
|
122
|
+
"README.md": "",
|
|
123
|
+
"styles.css": "",
|
|
124
|
+
});
|
|
125
|
+
git(root, "init", "-q");
|
|
126
|
+
git(root, "add", "-A");
|
|
127
|
+
git(root, "-c", "user.email=a@b", "-c", "user.name=a", "commit", "-q", "-m", "init");
|
|
128
|
+
for (const ext of ["tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]) fs.writeFileSync(path.join(root, `b.${ext}`), "");
|
|
129
|
+
return root;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const LISTED = ["b.cjs", "b.cts", "b.js", "b.jsx", "b.mjs", "b.mts", "b.tsx", "src/a.ts"];
|
|
133
|
+
|
|
134
|
+
it("lists tracked and untracked source files, leaving out ignored, fixtures and other extensions", () => {
|
|
135
|
+
expect(sourceFiles(repo())).toEqual({ ok: true, files: LISTED });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("lists the same files under a hook's GIT_INDEX_FILE naming another index", () => {
|
|
139
|
+
const root = repo();
|
|
140
|
+
vi.stubEnv("GIT_INDEX_FILE", path.join(plant({}), "index"));
|
|
141
|
+
expect(sourceFiles(root)).toEqual({ ok: true, files: LISTED });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("says why when git cannot list the files", () => {
|
|
145
|
+
const listed = sourceFiles(plant({}));
|
|
146
|
+
expect(listed.ok).toBe(false);
|
|
147
|
+
expect(listed.ok ? [] : listed.errors).toEqual([
|
|
148
|
+
expect.stringMatching(/^git ls-files failed: .*not a git repository/),
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("checkComments scans the listed files, or reports why it could not list them", () => {
|
|
153
|
+
const root = repo();
|
|
154
|
+
fs.writeFileSync(path.join(root, "src/a.ts"), "// @ts-ignore\n");
|
|
155
|
+
expect(checkComments(root)).toEqual([`src/a.ts:1: ${IGNORE}`]);
|
|
156
|
+
expect(checkComments(plant({}))).toEqual([expect.stringMatching(/^git ls-files failed: /)]);
|
|
157
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comments no rule may hide behind: a lint disable, `@ts-ignore`, `@ts-nocheck`, and an `@ts-expect-error` with no
|
|
3
|
+
* written reason. A script and not an oxlint rule, since oxlint honors its own disable comments. Each directive is
|
|
4
|
+
* matched as the tool that obeys it reads it, so a string or prose that mentions one passes.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { parseSync, type Comment } from "oxc-parser";
|
|
9
|
+
import { isReason } from "./lint/is-reason.ts";
|
|
10
|
+
import { runGit } from "./sync/standards.ts";
|
|
11
|
+
|
|
12
|
+
/** ts tsx mts cts js jsx mjs cjs. */
|
|
13
|
+
const SOURCE = /\.(?:[jt]sx?|[cm][jt]s)$/;
|
|
14
|
+
const LINE_BREAK = /\r\n?|[\n\u2028\u2029]/g;
|
|
15
|
+
const HASHBANG = /^#!.*/;
|
|
16
|
+
/** oxlint's (and eslint's) directive: the word first in the trimmed text, then a space or the end. */
|
|
17
|
+
const DISABLE = /^((?:eslint|oxlint)-disable(?:-next-line|-line)?)(?:\s|$)/;
|
|
18
|
+
/** TypeScript's `commentDirectiveRegExSingleLine`, on the whole `//` comment. */
|
|
19
|
+
const TS_LINE = /^\/\/\/?\s*@(ts-expect-error|ts-ignore)/;
|
|
20
|
+
/** TypeScript's `commentDirectiveRegExMultiLine`, on the trimmed last line of a block comment. */
|
|
21
|
+
const TS_BLOCK = /^(?:\/|\*)*\s*@(ts-expect-error|ts-ignore)/;
|
|
22
|
+
/** TypeScript's `singleLinePragmaRegEx`, read on the `//` comments before the file's first token. */
|
|
23
|
+
const PRAGMA = /^\/\/\/?\s*@([^\s:]+)(?:(?:[^\S\r\n]|:).*)?$/;
|
|
24
|
+
|
|
25
|
+
const IGNORE = '"@ts-ignore" hides a type error; fix the type, or write @ts-expect-error with a reason';
|
|
26
|
+
const NOCHECK = '"@ts-nocheck" turns type checking off for the file; fix the types instead';
|
|
27
|
+
const EXPECT = '"@ts-expect-error" needs a reason: // @ts-expect-error <why this line must not compile>';
|
|
28
|
+
|
|
29
|
+
/** A refused comment: where in the file its directive starts, and what to do instead. */
|
|
30
|
+
type Hit = { at: number; message: string };
|
|
31
|
+
/** The source files of a checkout, or why git could not list them. */
|
|
32
|
+
export type Listed = { ok: true; files: string[] } | { ok: false; errors: string[] };
|
|
33
|
+
|
|
34
|
+
const isSource = (rel: string) => SOURCE.test(rel) && !rel.split("/").includes("fixtures");
|
|
35
|
+
const lineOf = (source: string, at: number) => (source.slice(0, at).match(LINE_BREAK) ?? []).length + 1;
|
|
36
|
+
|
|
37
|
+
function disableHit(comment: Comment): Hit | null {
|
|
38
|
+
const trimmed = comment.value.trimStart();
|
|
39
|
+
const word = DISABLE.exec(trimmed)?.[1];
|
|
40
|
+
if (word === undefined) return null;
|
|
41
|
+
const at = comment.end - (comment.type === "Block" ? 2 : 0) - trimmed.length;
|
|
42
|
+
return { at, message: `"${word}" switches a lint rule off; fix the code, or add a lint.skip entry with a reason` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The text TypeScript reads a directive from, and where it starts: a block comment's last line, from `/*` or after. */
|
|
46
|
+
function directiveLine(comment: Comment): { text: string; at: number } {
|
|
47
|
+
if (comment.type === "Line") return { text: `//${comment.value}`, at: comment.start };
|
|
48
|
+
const raw = `/*${comment.value}*/`;
|
|
49
|
+
const breaks = [...raw.matchAll(LINE_BREAK)];
|
|
50
|
+
const last = breaks.at(-1);
|
|
51
|
+
const offset = last === undefined ? 0 : last.index + last[0].length;
|
|
52
|
+
return { text: raw.slice(offset), at: comment.start + offset };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tsHit(comment: Comment): Hit | null {
|
|
56
|
+
const { text, at } = directiveLine(comment);
|
|
57
|
+
const trimmed = text.trimStart();
|
|
58
|
+
const match = (comment.type === "Line" ? TS_LINE : TS_BLOCK).exec(trimmed);
|
|
59
|
+
if (match === null) return null;
|
|
60
|
+
const start = at + text.length - trimmed.length;
|
|
61
|
+
if (match[1] === "ts-ignore") return { at: start, message: IGNORE };
|
|
62
|
+
const rest = trimmed.slice(match[0].length).replace(/\*\/$/, "");
|
|
63
|
+
return isReason(rest) ? null : { at: start, message: EXPECT };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The comments before the first token, past a `#!` line, where TypeScript reads its file pragmas. */
|
|
67
|
+
function leading(source: string, comments: readonly Comment[]): Comment[] {
|
|
68
|
+
let edge = HASHBANG.exec(source)?.[0].length ?? 0;
|
|
69
|
+
const found: Comment[] = [];
|
|
70
|
+
for (const comment of comments) {
|
|
71
|
+
if (source.slice(edge, comment.start).trim() !== "") break;
|
|
72
|
+
found.push(comment);
|
|
73
|
+
edge = comment.end;
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function nocheckHit(comment: Comment): Hit | null {
|
|
79
|
+
if (comment.type !== "Line") return null;
|
|
80
|
+
const name = PRAGMA.exec(`//${comment.value}`)?.[1];
|
|
81
|
+
return name?.toLowerCase() === "ts-nocheck" ? { at: comment.start, message: NOCHECK } : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Every refused comment of one file, in the order they appear. */
|
|
85
|
+
function hitsOf(rel: string, source: string): Hit[] {
|
|
86
|
+
const { comments } = parseSync(rel, source);
|
|
87
|
+
const nochecks = leading(source, comments).map(nocheckHit);
|
|
88
|
+
const hits = [...comments.flatMap((comment) => [disableHit(comment), tsHit(comment)]), ...nochecks];
|
|
89
|
+
return hits.filter((hit) => hit !== null).sort((a, b) => a.at - b.at);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readSource(file: string): string | null {
|
|
93
|
+
try {
|
|
94
|
+
return fs.readFileSync(file, "utf8");
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One `<file>:<line>: <message>` per refused comment in the source files of `files` (paths relative to `root`); a file
|
|
102
|
+
* that cannot be read is skipped, and one that does not parse is scanned on the comments the parser still returns.
|
|
103
|
+
*/
|
|
104
|
+
export function commentProblems(root: string, files: readonly string[]): string[] {
|
|
105
|
+
return files.filter(isSource).flatMap((rel) => {
|
|
106
|
+
const source = readSource(path.join(root, rel));
|
|
107
|
+
if (source === null) return [];
|
|
108
|
+
return hitsOf(rel, source).map((hit) => `${rel}:${lineOf(source, hit.at)}: ${hit.message}`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The tracked and untracked, not ignored, source files under `root`, outside any `fixtures` dir, sorted. */
|
|
113
|
+
export function sourceFiles(root: string): Listed {
|
|
114
|
+
const run = runGit(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
115
|
+
if (run.status !== 0) return { ok: false, errors: [`git ls-files failed: ${run.stderr.trim()}`] };
|
|
116
|
+
return { ok: true, files: [...new Set(run.stdout.split("\0").filter(isSource))].sort() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `commentProblems` over every source file of the checkout at `root`, as `repo check` runs it. */
|
|
120
|
+
export function checkComments(root: string): string[] {
|
|
121
|
+
const listed = sourceFiles(root);
|
|
122
|
+
return listed.ok ? commentProblems(root, listed.files) : listed.errors;
|
|
123
|
+
}
|