pr-checkmate 3.0.4 → 3.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/dist/{cli.js → cjs/cli.js} +30 -30
- package/dist/{config → cjs/config}/index.d.ts +2 -2
- package/dist/{config → cjs/config}/index.js +8 -7
- package/dist/{config → cjs/config}/loader.d.ts +1 -1
- package/dist/{config → cjs/config}/loader.js +8 -8
- package/dist/{core → cjs/core}/checks/dependencies/banned-imports.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/banned-imports.js +10 -10
- package/dist/{core → cjs/core}/checks/dependencies/circular-deps.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/circular-deps.js +27 -27
- package/dist/{core → cjs/core}/checks/dependencies/dependency-check.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/dependency-check.js +17 -17
- package/dist/{core → cjs/core}/checks/dependencies/grype-scan.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/grype-scan.js +18 -18
- package/dist/{core → cjs/core}/checks/dependencies/license-check.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/license-check.js +12 -12
- package/dist/{core → cjs/core}/checks/dependencies/npm-audit.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/npm-audit.js +14 -14
- package/dist/{core → cjs/core}/checks/dependencies/outdated-deps.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/outdated-deps.js +14 -14
- package/dist/{core → cjs/core}/checks/dependencies/vuln-scan.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/dependencies/vuln-scan.js +14 -14
- package/dist/{core → cjs/core}/checks/findings.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/findings.js +16 -16
- package/dist/{core → cjs/core}/checks/git/custom-rules.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/custom-rules.js +19 -19
- package/dist/{core → cjs/core}/checks/git/diff-smell.d.ts +2 -2
- package/dist/{core → cjs/core}/checks/git/diff-smell.js +13 -13
- package/dist/{core → cjs/core}/checks/git/large-files.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/large-files.js +15 -15
- package/dist/{core → cjs/core}/checks/git/leftover-debug.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/leftover-debug.js +13 -13
- package/dist/{core → cjs/core}/checks/git/lockfile-drift.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/lockfile-drift.js +12 -12
- package/dist/{core → cjs/core}/checks/git/merge-conflict.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/merge-conflict.js +15 -15
- package/dist/{core → cjs/core}/checks/git/missing-tests.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/missing-tests.js +15 -15
- package/dist/{core → cjs/core}/checks/git/todo-fixme.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/git/todo-fixme.js +11 -11
- package/dist/{core → cjs/core}/checks/languages/cpp-format.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/cpp-format.js +18 -18
- package/dist/{core → cjs/core}/checks/languages/external-checks.d.ts +9 -9
- package/dist/{core → cjs/core}/checks/languages/external-checks.js +10 -10
- package/dist/{core → cjs/core}/checks/languages/external-runner.d.ts +2 -2
- package/dist/{core → cjs/core}/checks/languages/external-runner.js +21 -21
- package/dist/{core → cjs/core}/checks/languages/go-format.d.ts +1 -1
- package/dist/cjs/core/checks/languages/go-format.js +202 -0
- package/dist/cjs/core/checks/languages/lint.d.ts +2 -0
- package/dist/{core → cjs/core}/checks/languages/lint.js +21 -21
- package/dist/{core → cjs/core}/checks/languages/prettier.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/prettier.js +18 -18
- package/dist/{core → cjs/core}/checks/languages/python-format.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/python-format.js +14 -14
- package/dist/{core → cjs/core}/checks/languages/python-lint.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/python-lint.js +13 -13
- package/dist/{core → cjs/core}/checks/languages/python-typecheck.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/python-typecheck.js +29 -29
- package/dist/{core → cjs/core}/checks/languages/typecheck.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/languages/typecheck.js +13 -13
- package/dist/{core → cjs/core}/checks/pr/commitlint.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/pr/commitlint.js +12 -12
- package/dist/{core → cjs/core}/checks/pr/pr-body.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/pr/pr-body.js +8 -8
- package/dist/{core → cjs/core}/checks/pr/pr-size-check.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/pr/pr-size-check.js +10 -10
- package/dist/{core → cjs/core}/checks/pr/pr-title-lint.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/pr/pr-title-lint.js +8 -8
- package/dist/{core → cjs/core}/checks/quality/case-collision.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/case-collision.js +7 -7
- package/dist/{core → cjs/core}/checks/quality/config-validation.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/config-validation.js +9 -9
- package/dist/{core → cjs/core}/checks/quality/coverage.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/coverage.js +4 -4
- package/dist/{core → cjs/core}/checks/quality/dead-code.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/dead-code.js +23 -23
- package/dist/{core → cjs/core}/checks/quality/duplicate-check.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/duplicate-check.js +22 -22
- package/dist/{core → cjs/core}/checks/quality/license-header.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/license-header.js +16 -16
- package/dist/{core → cjs/core}/checks/quality/markdown-lint.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/markdown-lint.js +17 -17
- package/dist/{core → cjs/core}/checks/quality/package-size.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/package-size.js +12 -12
- package/dist/{core → cjs/core}/checks/quality/spellcheck.d.ts +2 -2
- package/dist/{core → cjs/core}/checks/quality/spellcheck.js +18 -18
- package/dist/{core → cjs/core}/checks/quality/symlinks.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/symlinks.js +8 -8
- package/dist/{core → cjs/core}/checks/quality/yaml-lint.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/quality/yaml-lint.js +12 -12
- package/dist/{core → cjs/core}/checks/security/diff-security.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/security/diff-security.js +15 -15
- package/dist/{core → cjs/core}/checks/security/dockerfile-security.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/security/dockerfile-security.js +16 -16
- package/dist/{core → cjs/core}/checks/security/migration-safety.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/security/migration-safety.js +17 -17
- package/dist/{core → cjs/core}/checks/security/security.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/security/security.js +21 -21
- package/dist/{core → cjs/core}/checks/security/sensitive-file-guard.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/security/sensitive-file-guard.js +14 -14
- package/dist/{core → cjs/core}/checks/security/workflow-security.d.ts +2 -2
- package/dist/{core → cjs/core}/checks/security/workflow-security.js +14 -14
- package/dist/{core → cjs/core}/checks/tool-install.d.ts +1 -1
- package/dist/{core → cjs/core}/checks/tool-install.js +2 -2
- package/dist/{core → cjs/core}/context.d.ts +1 -1
- package/dist/{core → cjs/core}/context.js +16 -16
- package/dist/cjs/core/index.d.ts +4 -0
- package/dist/{core → cjs/core}/index.js +7 -7
- package/dist/{core → cjs/core}/install/cache.js +2 -2
- package/dist/{core → cjs/core}/install/doctor.d.ts +4 -4
- package/dist/{core → cjs/core}/install/doctor.js +34 -34
- package/dist/{core → cjs/core}/install/ensure.d.ts +2 -2
- package/dist/{core → cjs/core}/install/ensure.js +15 -14
- package/dist/{core → cjs/core}/install/lock.js +4 -4
- package/dist/{core → cjs/core}/install/prepare.d.ts +4 -4
- package/dist/{core → cjs/core}/install/prepare.js +13 -13
- package/dist/{core → cjs/core}/install/profiles.d.ts +2 -2
- package/dist/{core → cjs/core}/install/reuse.d.ts +1 -1
- package/dist/{core → cjs/core}/install/reuse.js +4 -4
- package/dist/{core → cjs/core}/registry.d.ts +1 -1
- package/dist/cjs/core/registry.js +112 -0
- package/dist/{core → cjs/core}/reporters.d.ts +1 -1
- package/dist/{core → cjs/core}/reporters.js +28 -28
- package/dist/{core → cjs/core}/run-checks.d.ts +2 -2
- package/dist/{core → cjs/core}/run-checks.js +31 -31
- package/dist/{core → cjs/core}/targets.d.ts +1 -1
- package/dist/{core → cjs/core}/targets.js +20 -20
- package/dist/{core → cjs/core}/types.d.ts +3 -3
- package/dist/cjs/index.d.ts +12 -0
- package/dist/{index.js → cjs/index.js} +18 -18
- package/dist/{init.js → cjs/init.js} +68 -56
- package/dist/{interfaces → cjs/interfaces}/index.d.ts +3 -3
- package/dist/cjs/package.json +1 -0
- package/dist/{scripts → cjs/scripts}/copy-configs.js +22 -7
- package/dist/{scripts → cjs/scripts}/generate-install-manifests.js +2 -2
- package/dist/{scripts → cjs/scripts}/publish-github.js +4 -4
- package/dist/{utils → cjs/utils}/detect-language.js +12 -12
- package/dist/cjs/utils/execa.js +46 -0
- package/dist/{utils → cjs/utils}/git.js +2 -2
- package/dist/{utils → cjs/utils}/http.js +2 -2
- package/dist/cjs/utils/index.d.ts +15 -0
- package/dist/{utils → cjs/utils}/index.js +13 -13
- package/dist/{utils → cjs/utils}/own-deps.d.ts +2 -0
- package/dist/{utils → cjs/utils}/own-deps.js +1 -0
- package/dist/{utils → cjs/utils}/package-version.js +2 -1
- package/dist/{utils → cjs/utils}/resolve-bin.js +2 -1
- package/dist/{utils → cjs/utils}/update-notifier.js +4 -4
- package/dist/esm/cli.d.ts +2 -0
- package/dist/esm/cli.js +137 -0
- package/dist/esm/config/constants.d.ts +153 -0
- package/dist/esm/config/constants.js +360 -0
- package/dist/esm/config/index.d.ts +40 -0
- package/dist/esm/config/index.js +60 -0
- package/dist/esm/config/loader.d.ts +6 -0
- package/dist/esm/config/loader.js +191 -0
- package/dist/esm/config/pr-checkmate-workflow-docker.yml +79 -0
- package/dist/esm/config/pr-checkmate-workflow.yml +92 -0
- package/dist/esm/core/checks/dependencies/banned-imports.d.ts +2 -0
- package/dist/esm/core/checks/dependencies/banned-imports.js +70 -0
- package/dist/esm/core/checks/dependencies/circular-deps.d.ts +2 -0
- package/dist/esm/core/checks/dependencies/circular-deps.js +155 -0
- package/dist/esm/core/checks/dependencies/dependency-check.d.ts +43 -0
- package/dist/esm/core/checks/dependencies/dependency-check.js +174 -0
- package/dist/esm/core/checks/dependencies/grype-scan.d.ts +22 -0
- package/dist/esm/core/checks/dependencies/grype-scan.js +112 -0
- package/dist/esm/core/checks/dependencies/license-check.d.ts +2 -0
- package/dist/esm/core/checks/dependencies/license-check.js +61 -0
- package/dist/esm/core/checks/dependencies/npm-audit.d.ts +6 -0
- package/dist/esm/core/checks/dependencies/npm-audit.js +80 -0
- package/dist/esm/core/checks/dependencies/outdated-deps.d.ts +2 -0
- package/dist/esm/core/checks/dependencies/outdated-deps.js +66 -0
- package/dist/esm/core/checks/dependencies/vuln-scan.d.ts +2 -0
- package/dist/esm/core/checks/dependencies/vuln-scan.js +54 -0
- package/dist/esm/core/checks/findings.d.ts +299 -0
- package/dist/esm/core/checks/findings.js +701 -0
- package/dist/esm/core/checks/git/custom-rules.d.ts +2 -0
- package/dist/esm/core/checks/git/custom-rules.js +106 -0
- package/dist/esm/core/checks/git/diff-smell.d.ts +19 -0
- package/dist/esm/core/checks/git/diff-smell.js +69 -0
- package/dist/esm/core/checks/git/large-files.d.ts +2 -0
- package/dist/esm/core/checks/git/large-files.js +112 -0
- package/dist/esm/core/checks/git/leftover-debug.d.ts +2 -0
- package/dist/esm/core/checks/git/leftover-debug.js +89 -0
- package/dist/esm/core/checks/git/lockfile-drift.d.ts +2 -0
- package/dist/esm/core/checks/git/lockfile-drift.js +114 -0
- package/dist/esm/core/checks/git/merge-conflict.d.ts +2 -0
- package/dist/esm/core/checks/git/merge-conflict.js +117 -0
- package/dist/esm/core/checks/git/missing-tests.d.ts +2 -0
- package/dist/esm/core/checks/git/missing-tests.js +105 -0
- package/dist/esm/core/checks/git/todo-fixme.d.ts +7 -0
- package/dist/esm/core/checks/git/todo-fixme.js +80 -0
- package/dist/esm/core/checks/languages/cpp-format.d.ts +7 -0
- package/dist/esm/core/checks/languages/cpp-format.js +91 -0
- package/dist/esm/core/checks/languages/external-checks.d.ts +77 -0
- package/dist/esm/core/checks/languages/external-checks.js +197 -0
- package/dist/esm/core/checks/languages/external-runner.d.ts +119 -0
- package/dist/esm/core/checks/languages/external-runner.js +138 -0
- package/dist/esm/core/checks/languages/go-format.d.ts +8 -0
- package/dist/{core → esm/core}/checks/languages/go-format.js +29 -35
- package/dist/esm/core/checks/languages/lint.d.ts +2 -0
- package/dist/esm/core/checks/languages/lint.js +92 -0
- package/dist/esm/core/checks/languages/prettier.d.ts +2 -0
- package/dist/esm/core/checks/languages/prettier.js +85 -0
- package/dist/esm/core/checks/languages/python-format.d.ts +7 -0
- package/dist/esm/core/checks/languages/python-format.js +61 -0
- package/dist/esm/core/checks/languages/python-lint.d.ts +9 -0
- package/dist/esm/core/checks/languages/python-lint.js +55 -0
- package/dist/esm/core/checks/languages/python-typecheck.d.ts +14 -0
- package/dist/esm/core/checks/languages/python-typecheck.js +184 -0
- package/dist/esm/core/checks/languages/ruff-shared.d.ts +10 -0
- package/dist/esm/core/checks/languages/ruff-shared.js +35 -0
- package/dist/esm/core/checks/languages/typecheck.d.ts +2 -0
- package/dist/esm/core/checks/languages/typecheck.js +61 -0
- package/dist/esm/core/checks/pr/commitlint.d.ts +2 -0
- package/dist/esm/core/checks/pr/commitlint.js +102 -0
- package/dist/esm/core/checks/pr/pr-body.d.ts +2 -0
- package/dist/esm/core/checks/pr/pr-body.js +43 -0
- package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
- package/dist/esm/core/checks/pr/pr-size-check.js +42 -0
- package/dist/esm/core/checks/pr/pr-title-lint.d.ts +2 -0
- package/dist/esm/core/checks/pr/pr-title-lint.js +31 -0
- package/dist/esm/core/checks/quality/case-collision.d.ts +8 -0
- package/dist/esm/core/checks/quality/case-collision.js +39 -0
- package/dist/esm/core/checks/quality/config-validation.d.ts +11 -0
- package/dist/esm/core/checks/quality/config-validation.js +188 -0
- package/dist/esm/core/checks/quality/coverage.d.ts +2 -0
- package/dist/esm/core/checks/quality/coverage.js +62 -0
- package/dist/esm/core/checks/quality/dead-code.d.ts +2 -0
- package/dist/esm/core/checks/quality/dead-code.js +124 -0
- package/dist/esm/core/checks/quality/duplicate-check.d.ts +2 -0
- package/dist/esm/core/checks/quality/duplicate-check.js +141 -0
- package/dist/esm/core/checks/quality/license-header.d.ts +7 -0
- package/dist/esm/core/checks/quality/license-header.js +81 -0
- package/dist/esm/core/checks/quality/markdown-lint.d.ts +2 -0
- package/dist/esm/core/checks/quality/markdown-lint.js +104 -0
- package/dist/esm/core/checks/quality/package-size.d.ts +36 -0
- package/dist/esm/core/checks/quality/package-size.js +196 -0
- package/dist/esm/core/checks/quality/spellcheck.d.ts +77 -0
- package/dist/esm/core/checks/quality/spellcheck.js +296 -0
- package/dist/esm/core/checks/quality/symlinks.d.ts +16 -0
- package/dist/esm/core/checks/quality/symlinks.js +57 -0
- package/dist/esm/core/checks/quality/yaml-lint.d.ts +20 -0
- package/dist/esm/core/checks/quality/yaml-lint.js +116 -0
- package/dist/esm/core/checks/security/diff-security.d.ts +2 -0
- package/dist/esm/core/checks/security/diff-security.js +354 -0
- package/dist/esm/core/checks/security/dockerfile-security.d.ts +2 -0
- package/dist/esm/core/checks/security/dockerfile-security.js +98 -0
- package/dist/esm/core/checks/security/migration-safety.d.ts +2 -0
- package/dist/esm/core/checks/security/migration-safety.js +137 -0
- package/dist/esm/core/checks/security/security.d.ts +63 -0
- package/dist/esm/core/checks/security/security.js +201 -0
- package/dist/esm/core/checks/security/sensitive-file-guard.d.ts +2 -0
- package/dist/esm/core/checks/security/sensitive-file-guard.js +160 -0
- package/dist/esm/core/checks/security/workflow-security.d.ts +12 -0
- package/dist/esm/core/checks/security/workflow-security.js +100 -0
- package/dist/esm/core/checks/tool-install.d.ts +102 -0
- package/dist/esm/core/checks/tool-install.js +66 -0
- package/dist/esm/core/commands.d.ts +27 -0
- package/dist/esm/core/commands.js +89 -0
- package/dist/esm/core/context.d.ts +62 -0
- package/dist/esm/core/context.js +241 -0
- package/dist/esm/core/index.d.ts +4 -0
- package/dist/esm/core/index.js +4 -0
- package/dist/esm/core/install/cache.d.ts +58 -0
- package/dist/esm/core/install/cache.js +142 -0
- package/dist/esm/core/install/doctor.d.ts +35 -0
- package/dist/esm/core/install/doctor.js +164 -0
- package/dist/esm/core/install/ensure.d.ts +77 -0
- package/dist/esm/core/install/ensure.js +269 -0
- package/dist/esm/core/install/lock.d.ts +34 -0
- package/dist/esm/core/install/lock.js +92 -0
- package/dist/esm/core/install/merge.d.ts +47 -0
- package/dist/esm/core/install/merge.js +85 -0
- package/dist/esm/core/install/prepare.d.ts +76 -0
- package/dist/esm/core/install/prepare.js +142 -0
- package/dist/esm/core/install/profiles.d.ts +78 -0
- package/dist/esm/core/install/profiles.js +192 -0
- package/dist/esm/core/install/reuse.d.ts +41 -0
- package/dist/esm/core/install/reuse.js +100 -0
- package/dist/esm/core/registry.d.ts +6 -0
- package/dist/esm/core/registry.js +109 -0
- package/dist/esm/core/reporters.d.ts +18 -0
- package/dist/esm/core/reporters.js +266 -0
- package/dist/esm/core/run-checks.d.ts +33 -0
- package/dist/esm/core/run-checks.js +293 -0
- package/dist/esm/core/targets.d.ts +140 -0
- package/dist/esm/core/targets.js +213 -0
- package/dist/esm/core/types.d.ts +195 -0
- package/dist/esm/core/types.js +38 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/init.d.ts +38 -0
- package/dist/esm/init.js +421 -0
- package/dist/esm/interfaces/index.d.ts +432 -0
- package/dist/esm/interfaces/index.js +2 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/scripts/copy-configs.d.ts +1 -0
- package/dist/esm/scripts/copy-configs.js +64 -0
- package/dist/esm/scripts/generate-install-manifests.d.ts +39 -0
- package/dist/esm/scripts/generate-install-manifests.js +115 -0
- package/dist/esm/scripts/publish-github.d.ts +1 -0
- package/dist/esm/scripts/publish-github.js +45 -0
- package/dist/esm/utils/ci.d.ts +11 -0
- package/dist/esm/utils/ci.js +40 -0
- package/dist/esm/utils/detect-language.d.ts +48 -0
- package/dist/esm/utils/detect-language.js +234 -0
- package/dist/esm/utils/direct-deps.d.ts +21 -0
- package/dist/esm/utils/direct-deps.js +220 -0
- package/dist/esm/utils/execa.d.ts +22 -0
- package/dist/{utils → esm/utils}/execa.js +1 -4
- package/dist/esm/utils/git.d.ts +14 -0
- package/dist/esm/utils/git.js +21 -0
- package/dist/esm/utils/glob.d.ts +24 -0
- package/dist/esm/utils/glob.js +40 -0
- package/dist/esm/utils/http.d.ts +59 -0
- package/dist/esm/utils/http.js +115 -0
- package/dist/esm/utils/index.d.ts +15 -0
- package/dist/esm/utils/index.js +26 -0
- package/dist/esm/utils/logger.d.ts +65 -0
- package/dist/esm/utils/logger.js +157 -0
- package/dist/esm/utils/own-deps.d.ts +16 -0
- package/dist/esm/utils/own-deps.js +107 -0
- package/dist/esm/utils/package-version.d.ts +4 -0
- package/dist/esm/utils/package-version.js +38 -0
- package/dist/esm/utils/pool.d.ts +49 -0
- package/dist/esm/utils/pool.js +113 -0
- package/dist/esm/utils/resolve-bin.d.ts +67 -0
- package/dist/esm/utils/resolve-bin.js +156 -0
- package/dist/esm/utils/result-collector.d.ts +33 -0
- package/dist/esm/utils/result-collector.js +44 -0
- package/dist/esm/utils/update-notifier.d.ts +19 -0
- package/dist/esm/utils/update-notifier.js +108 -0
- package/install-manifests/core/package-lock.json +4 -4
- package/install-manifests/core/package.json +1 -1
- package/package.json +15 -13
- package/dist/core/checks/languages/lint.d.ts +0 -2
- package/dist/core/index.d.ts +0 -4
- package/dist/core/registry.js +0 -112
- package/dist/index.d.ts +0 -12
- package/dist/utils/index.d.ts +0 -15
- /package/dist/{cli.d.ts → cjs/cli.d.ts} +0 -0
- /package/dist/{config → cjs/config}/constants.d.ts +0 -0
- /package/dist/{config → cjs/config}/constants.js +0 -0
- /package/dist/{config → cjs/config}/pr-checkmate-workflow-docker.yml +0 -0
- /package/dist/{config → cjs/config}/pr-checkmate-workflow.yml +0 -0
- /package/dist/{core → cjs/core}/checks/languages/ruff-shared.d.ts +0 -0
- /package/dist/{core → cjs/core}/checks/languages/ruff-shared.js +0 -0
- /package/dist/{core → cjs/core}/commands.d.ts +0 -0
- /package/dist/{core → cjs/core}/commands.js +0 -0
- /package/dist/{core → cjs/core}/install/cache.d.ts +0 -0
- /package/dist/{core → cjs/core}/install/lock.d.ts +0 -0
- /package/dist/{core → cjs/core}/install/merge.d.ts +0 -0
- /package/dist/{core → cjs/core}/install/merge.js +0 -0
- /package/dist/{core → cjs/core}/install/profiles.js +0 -0
- /package/dist/{core → cjs/core}/types.js +0 -0
- /package/dist/{init.d.ts → cjs/init.d.ts} +0 -0
- /package/dist/{interfaces → cjs/interfaces}/index.js +0 -0
- /package/dist/{scripts → cjs/scripts}/copy-configs.d.ts +0 -0
- /package/dist/{scripts → cjs/scripts}/generate-install-manifests.d.ts +0 -0
- /package/dist/{scripts → cjs/scripts}/publish-github.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/ci.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/ci.js +0 -0
- /package/dist/{utils → cjs/utils}/detect-language.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/direct-deps.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/direct-deps.js +0 -0
- /package/dist/{utils → cjs/utils}/execa.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/git.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/glob.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/glob.js +0 -0
- /package/dist/{utils → cjs/utils}/http.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/logger.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/logger.js +0 -0
- /package/dist/{utils → cjs/utils}/package-version.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/pool.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/pool.js +0 -0
- /package/dist/{utils → cjs/utils}/resolve-bin.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/result-collector.d.ts +0 -0
- /package/dist/{utils → cjs/utils}/result-collector.js +0 -0
- /package/dist/{utils → cjs/utils}/update-notifier.d.ts +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadProjectConfig } from '../../config/loader.js';
|
|
3
|
+
import { detectLanguageUsage } from '../../utils/detect-language.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import { packageVersion } from '../../utils/package-version.js';
|
|
6
|
+
import { resolveCacheDir, stalePaths } from './cache.js';
|
|
7
|
+
import { cachedProfiles, ensureTooling } from './ensure.js';
|
|
8
|
+
import { profileSatisfiedLocally } from './prepare.js';
|
|
9
|
+
import { planProfiles, PROFILES } from './profiles.js';
|
|
10
|
+
// Re-exported: `doctor` has been the home of this since ticket 064, and the
|
|
11
|
+
// callers that import it from here are not worth churning. The reader itself
|
|
12
|
+
// moved to utils so the report header and the update notice share one answer.
|
|
13
|
+
export { packageVersion };
|
|
14
|
+
/**
|
|
15
|
+
* Everything the install layer knows about this repository, in one shot.
|
|
16
|
+
*
|
|
17
|
+
* `usage` is a parameter rather than always detected here so a caller that
|
|
18
|
+
* already has it — `doctor` prints it — does not scan the repository twice and
|
|
19
|
+
* log two contradictory-looking detection lines for one command.
|
|
20
|
+
*/
|
|
21
|
+
export async function planInstall(cwd = process.cwd(), usage) {
|
|
22
|
+
const config = loadProjectConfig();
|
|
23
|
+
const detected = usage ?? (await detectLanguageUsage(cwd, config.ignoreDirs, config.detect));
|
|
24
|
+
const location = resolveCacheDir(packageVersion(), config.install?.cacheDir);
|
|
25
|
+
return { decisions: planProfiles(detected, config), cacheDir: location?.dir ?? null };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which profiles are usable right now, and why — fetched, or shipped with us.
|
|
29
|
+
*
|
|
30
|
+
* The cache is only half the answer, and reporting only that half was a lie in
|
|
31
|
+
* the client's favour's opposite direction: most profiles ship inside the
|
|
32
|
+
* package, are never fetched, and were listed as "not installed yet" on a machine
|
|
33
|
+
* where they worked perfectly. `doctor` exists to answer "why did my typecheck
|
|
34
|
+
* not run", so telling someone seven profiles are missing when six of them are
|
|
35
|
+
* present sends them to debug nothing.
|
|
36
|
+
*
|
|
37
|
+
* The cached half is read from the tooling directory's own stamp rather than
|
|
38
|
+
* inferred from paths: there is one tree for everything fetched, so what went
|
|
39
|
+
* into it is recorded rather than visible in the layout (ticket 087).
|
|
40
|
+
*/
|
|
41
|
+
function installedProfiles(cacheDir) {
|
|
42
|
+
const present = new Map();
|
|
43
|
+
for (const profile of PROFILES) {
|
|
44
|
+
if (profileSatisfiedLocally(profile))
|
|
45
|
+
present.set(profile.name, 'ships with the package');
|
|
46
|
+
}
|
|
47
|
+
if (cacheDir) {
|
|
48
|
+
for (const name of cachedProfiles(cacheDir))
|
|
49
|
+
present.set(name, 'fetched');
|
|
50
|
+
}
|
|
51
|
+
return present;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fetch every profile this repository needs.
|
|
55
|
+
*
|
|
56
|
+
* Shared by the `install` command and by the run itself, so a client who never
|
|
57
|
+
* runs `install` gets the same result lazily. `all` is what a container build or
|
|
58
|
+
* an air-gapped mirror wants: everything, regardless of what this repository
|
|
59
|
+
* happens to contain.
|
|
60
|
+
*/
|
|
61
|
+
export async function installProfiles(cwd = process.cwd(), all = false) {
|
|
62
|
+
const config = loadProjectConfig();
|
|
63
|
+
const install = config.install ?? {};
|
|
64
|
+
const { decisions } = await planInstall(cwd);
|
|
65
|
+
const denied = new Set(install.deny ?? []);
|
|
66
|
+
const wanted = (all ? PROFILES.map(p => p.name) : decisions.filter(d => d.required).map(d => d.profile)).filter(name => {
|
|
67
|
+
const profile = PROFILES.find(p => p.name === name);
|
|
68
|
+
// Nothing to fetch for a profile whose packages already ship with us, and
|
|
69
|
+
// fetching it anyway is not merely wasted: `--all` downloaded a second copy
|
|
70
|
+
// of every bundled profile — some 800 packages — beside the ones already in
|
|
71
|
+
// `node_modules`. On a container build that lands in the image (ticket 083).
|
|
72
|
+
if (profile && profileSatisfiedLocally(profile)) {
|
|
73
|
+
logger.info(`[install]: profile "${name}" already ships with the package`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const blocked = profile?.packages.filter(pkg => denied.has(pkg)) ?? [];
|
|
77
|
+
if (blocked.length === 0)
|
|
78
|
+
return true;
|
|
79
|
+
logger.info(`[install]: profile "${name}" skipped — install.deny lists ${blocked.join(', ')}`);
|
|
80
|
+
return false;
|
|
81
|
+
});
|
|
82
|
+
// One install for the whole set. Profiles that share hundreds of packages used
|
|
83
|
+
// to be fetched into a tree each, which fought over npm's cache and downloaded
|
|
84
|
+
// the shared half once per profile (ticket 087).
|
|
85
|
+
const result = await ensureTooling(wanted, {
|
|
86
|
+
cwd,
|
|
87
|
+
version: packageVersion(),
|
|
88
|
+
install,
|
|
89
|
+
});
|
|
90
|
+
return [result];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Explain what this repository needs, what it has, and what is missing — and why.
|
|
94
|
+
*
|
|
95
|
+
* The command exists because adaptive installation makes "why did my typecheck
|
|
96
|
+
* not run" a routine question, and the answer must be a command rather than a
|
|
97
|
+
* reading of our source.
|
|
98
|
+
*/
|
|
99
|
+
export async function runDoctor(cwd = process.cwd()) {
|
|
100
|
+
const config = loadProjectConfig();
|
|
101
|
+
const usage = await detectLanguageUsage(cwd, config.ignoreDirs, config.detect);
|
|
102
|
+
const { decisions, cacheDir } = await planInstall(cwd, usage);
|
|
103
|
+
const present = installedProfiles(cacheDir);
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push('PR CheckMate — install doctor');
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(` version ${packageVersion()}`);
|
|
108
|
+
lines.push(` repository ${cwd}`);
|
|
109
|
+
lines.push(` cache ${cacheDir ?? 'unavailable (no writable directory)'}`);
|
|
110
|
+
lines.push(` strategy ${config.install?.strategy ?? 'adaptive'}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(' Languages');
|
|
113
|
+
if (usage.length === 0)
|
|
114
|
+
lines.push(' none detected');
|
|
115
|
+
for (const u of usage) {
|
|
116
|
+
// The distinction that decides downloads: a marker declares a language, a
|
|
117
|
+
// file proves it. Only the second is worth a toolchain.
|
|
118
|
+
const evidence = u.files > 0 ? `${u.files} file(s)` : 'marker file only';
|
|
119
|
+
lines.push(` ${u.lang.padEnd(12)} ${evidence}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(' Profiles');
|
|
123
|
+
for (const decision of decisions) {
|
|
124
|
+
const source = present.get(decision.profile);
|
|
125
|
+
const state = decision.required ? (source ? '✅' : '⬇️ ') : '—';
|
|
126
|
+
// Where a present profile came from, because "it works" and "it works
|
|
127
|
+
// because it was downloaded onto this runner" are different situations for
|
|
128
|
+
// anyone debugging a cache or an air-gapped build.
|
|
129
|
+
const how = decision.required && source ? ` (${source})` : '';
|
|
130
|
+
lines.push(` ${state} ${decision.profile.padEnd(12)} ${decision.reason}${how}`);
|
|
131
|
+
}
|
|
132
|
+
const missing = decisions.filter(d => d.required && !present.has(d.profile));
|
|
133
|
+
if (missing.length > 0) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
|
|
136
|
+
lines.push(' run, or now with: npx pr-checkmate install');
|
|
137
|
+
}
|
|
138
|
+
const stale = stalePaths(packageVersion(), config.install?.cacheDir);
|
|
139
|
+
if (stale.length > 0) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(` ${stale.length} directory(ies) from other versions: npx pr-checkmate cache-prune`);
|
|
142
|
+
}
|
|
143
|
+
logger.print(lines.join('\n'));
|
|
144
|
+
}
|
|
145
|
+
/** Remove cached tooling belonging to other versions of pr-checkmate. */
|
|
146
|
+
export function pruneCache() {
|
|
147
|
+
const config = loadProjectConfig();
|
|
148
|
+
const stale = stalePaths(packageVersion(), config.install?.cacheDir);
|
|
149
|
+
if (stale.length === 0) {
|
|
150
|
+
logger.print('Nothing to prune — no tooling from other versions is cached.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const dir of stale) {
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
logger.print(`removed ${dir}`);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// Another version may be running right now; refusing to delete its
|
|
160
|
+
// tooling is the correct outcome, not an error worth failing on.
|
|
161
|
+
logger.warn(`[cache-prune]: could not remove ${dir}: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { InstallConfig } from '../../interfaces/index.js';
|
|
2
|
+
import type { ProfileName } from './profiles.js';
|
|
3
|
+
/**
|
|
4
|
+
* What happened to the tooling cache, in the words the report and `doctor` use.
|
|
5
|
+
*
|
|
6
|
+
* `skipped` and `failed` are deliberately distinct. Skipped means we chose not
|
|
7
|
+
* to fetch — offline, `strategy: "manual"`, an explicit exclusion — and the
|
|
8
|
+
* client already knows why. Failed means we tried and could not, which is the
|
|
9
|
+
* case that has to reach the report: ticket 057 is the standing reminder that a
|
|
10
|
+
* check quietly vanishing is worse than a check failing loudly.
|
|
11
|
+
*/
|
|
12
|
+
type EnsureStatus = 'cached' | 'installed' | 'skipped' | 'failed';
|
|
13
|
+
export interface EnsureResult {
|
|
14
|
+
status: EnsureStatus;
|
|
15
|
+
reason: string;
|
|
16
|
+
dir?: string;
|
|
17
|
+
/** Profiles the directory holds once this call is done. */
|
|
18
|
+
profiles: ProfileName[];
|
|
19
|
+
durationMs?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Whether the fetch succeeded, and what to quote in the report when it did not. */
|
|
22
|
+
export interface InstallOutcome {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Options `ensureTooling` needs that do not come from the config file. */
|
|
27
|
+
export interface EnsureOptions {
|
|
28
|
+
/** Repository under review — the cwd npm runs in, so the client's `.npmrc` applies. */
|
|
29
|
+
cwd: string;
|
|
30
|
+
/** pr-checkmate's own version; part of the cache path. */
|
|
31
|
+
version: string;
|
|
32
|
+
install: InstallConfig;
|
|
33
|
+
/**
|
|
34
|
+
* The fetch itself. Defaults to `npm ci`; injected by tests.
|
|
35
|
+
*
|
|
36
|
+
* A seam rather than a mock of the module: everything around the fetch — the
|
|
37
|
+
* lock, the cache stamp, the offline and strategy branches, the reasons that
|
|
38
|
+
* end up in the report — is logic worth pinning, and without this the only way
|
|
39
|
+
* to reach it is to actually download tooling from the network in a unit test.
|
|
40
|
+
*/
|
|
41
|
+
installer?: (dir: string) => Promise<InstallOutcome>;
|
|
42
|
+
}
|
|
43
|
+
/** Where the shipped manifests live inside the installed package. */
|
|
44
|
+
export declare function manifestDir(profile: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Which profiles the tooling cache holds, as recorded by the install that filled it.
|
|
47
|
+
*
|
|
48
|
+
* The one way to ask, so the stamp's format has a single owner. There is one tree
|
|
49
|
+
* for all fetched tooling now, so what is in it is written down rather than
|
|
50
|
+
* readable from the directory layout.
|
|
51
|
+
*/
|
|
52
|
+
export declare function cachedProfiles(cacheDir: string): ProfileName[];
|
|
53
|
+
/**
|
|
54
|
+
* Make a set of profiles' tooling available, fetching only what is not already there.
|
|
55
|
+
*
|
|
56
|
+
* Idempotent by design: this is the single entry point for both the `install`
|
|
57
|
+
* command and the lazy path taken when it was never run (`npm ci
|
|
58
|
+
* --ignore-scripts`, a `node_modules` restored from a CI cache). Two code paths
|
|
59
|
+
* for the same job is how the two drift apart and how one of them silently stops
|
|
60
|
+
* working.
|
|
61
|
+
*
|
|
62
|
+
* Everything needed goes into **one** tree. Profiles overlap heavily — `js` and
|
|
63
|
+
* `commitlint` share 30 MB — and a directory each left npm nothing to
|
|
64
|
+
* deduplicate: 267 MB against the 211 one merged tree costs for the same set
|
|
65
|
+
* (ticket 087). The set only ever grows: a run needing a profile the directory
|
|
66
|
+
* lacks reinstalls the union, so nothing already fetched is lost.
|
|
67
|
+
*
|
|
68
|
+
* The install itself is a plain `npm ci` against a manifest merged from the ones
|
|
69
|
+
* we generated from our own lockfile, run from the client's repository so their
|
|
70
|
+
* `.npmrc`, registry and proxy settings apply unchanged. `--ignore-scripts` is
|
|
71
|
+
* not negotiable: we are a supply-chain tool, and running arbitrary install hooks
|
|
72
|
+
* from a security check would be indefensible. Integrity is npm's own, over the
|
|
73
|
+
* whole transitive tree — a tampered tarball aborts with `EINTEGRITY` rather than
|
|
74
|
+
* being installed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function ensureTooling(wanted: ProfileName[], opts: EnsureOptions): Promise<EnsureResult>;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname as __dirnameFn } from 'node:path';
|
|
3
|
+
const __dirname = __dirnameFn(__fileURLToPath(import.meta.url));
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execa } from '../../utils/execa.js';
|
|
8
|
+
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import { findOwnPackageRoot } from '../../utils/own-deps.js';
|
|
10
|
+
import { resolveCacheDir, toolingDir } from './cache.js';
|
|
11
|
+
import { withLock } from './lock.js';
|
|
12
|
+
import { mergeManifests } from './merge.js';
|
|
13
|
+
/** Where the shipped manifests live inside the installed package. */
|
|
14
|
+
export function manifestDir(profile) {
|
|
15
|
+
return path.join(findOwnPackageRoot() ?? path.resolve(__dirname, '../../..'), 'install-manifests', profile);
|
|
16
|
+
}
|
|
17
|
+
const stampPath = (dir) => path.join(dir, '.pr-checkmate-installed');
|
|
18
|
+
const MANIFEST = 'package.json';
|
|
19
|
+
const LOCKFILE = 'package-lock.json';
|
|
20
|
+
/**
|
|
21
|
+
* Merge the shipped manifests for a set of profiles, or say which one is missing.
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` rather than throwing on a missing manifest, because that is an
|
|
24
|
+
* ordinary condition: a stamp written by another version can name a profile this
|
|
25
|
+
* one no longer ships. A *disagreement* between two manifests is different and
|
|
26
|
+
* still throws — see {@link mergeManifests}.
|
|
27
|
+
*/
|
|
28
|
+
function manifestFor(profiles) {
|
|
29
|
+
const dirs = profiles.map(name => ({ name, dir: manifestDir(name) }));
|
|
30
|
+
if (dirs.some(d => !fs.existsSync(path.join(d.dir, LOCKFILE))))
|
|
31
|
+
return null;
|
|
32
|
+
return mergeManifests(dirs);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A fingerprint of the manifest a tooling directory was installed from.
|
|
36
|
+
*
|
|
37
|
+
* Comparing the lockfile hash rather than merely checking that `node_modules`
|
|
38
|
+
* exists is what makes an upgrade correct: the cache path already includes our
|
|
39
|
+
* version, but a client can also pin a `cacheDir` shared between checkouts, and
|
|
40
|
+
* a half-written directory from an interrupted install must not read as ready.
|
|
41
|
+
*/
|
|
42
|
+
function hashOf(manifest) {
|
|
43
|
+
return crypto.createHash('sha256').update(JSON.stringify(manifest.lockfile)).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
function readStamp(dir) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(fs.readFileSync(stampPath(dir), 'utf-8'));
|
|
48
|
+
return Array.isArray(parsed.profiles) && typeof parsed.hash === 'string' ? parsed : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Unreadable, absent, or written by a version that stamped a bare hash.
|
|
52
|
+
// All three mean the same thing here: we cannot claim to know what is there.
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether the directory already holds everything asked for.
|
|
58
|
+
*
|
|
59
|
+
* A **superset** check, not an equality one, and that is the point of the shared
|
|
60
|
+
* directory. `install --all` warms the cache with every profile; a run that needs
|
|
61
|
+
* two of them must read that as ready, or the pre-warming built into our Docker
|
|
62
|
+
* image buys nothing — the failure ticket 083 was about. It is also what makes a
|
|
63
|
+
* language added six months later cost one install rather than one per run.
|
|
64
|
+
*
|
|
65
|
+
* The stamp's own hash is re-derived from the manifests we ship *now*, so a
|
|
66
|
+
* directory installed by a build with different manifests is correctly rejected
|
|
67
|
+
* even when its profile list still looks right.
|
|
68
|
+
*/
|
|
69
|
+
function holdsAll(dir, stamp, needed) {
|
|
70
|
+
if (!stamp || !fs.existsSync(path.join(dir, 'node_modules')))
|
|
71
|
+
return false;
|
|
72
|
+
if (!needed.every(p => stamp.profiles.includes(p)))
|
|
73
|
+
return false;
|
|
74
|
+
const installed = manifestFor(stamp.profiles);
|
|
75
|
+
return installed !== null && hashOf(installed) === stamp.hash;
|
|
76
|
+
}
|
|
77
|
+
/** Write the merged manifest into the cache so `npm ci` has something to read. */
|
|
78
|
+
function writeManifest(dir, manifest) {
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(dir, MANIFEST), JSON.stringify(manifest.pkg, null, 2));
|
|
81
|
+
fs.writeFileSync(path.join(dir, LOCKFILE), JSON.stringify(manifest.lockfile, null, 2));
|
|
82
|
+
}
|
|
83
|
+
/** Sorted, de-duplicated, so a set is one set however it was assembled. */
|
|
84
|
+
const normalise = (profiles) => [...new Set(profiles)].sort();
|
|
85
|
+
/**
|
|
86
|
+
* Which profiles the tooling cache holds, as recorded by the install that filled it.
|
|
87
|
+
*
|
|
88
|
+
* The one way to ask, so the stamp's format has a single owner. There is one tree
|
|
89
|
+
* for all fetched tooling now, so what is in it is written down rather than
|
|
90
|
+
* readable from the directory layout.
|
|
91
|
+
*/
|
|
92
|
+
export function cachedProfiles(cacheDir) {
|
|
93
|
+
const dir = toolingDir(cacheDir);
|
|
94
|
+
if (!fs.existsSync(path.join(dir, 'node_modules')))
|
|
95
|
+
return [];
|
|
96
|
+
return readStamp(dir)?.profiles ?? [];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Make a set of profiles' tooling available, fetching only what is not already there.
|
|
100
|
+
*
|
|
101
|
+
* Idempotent by design: this is the single entry point for both the `install`
|
|
102
|
+
* command and the lazy path taken when it was never run (`npm ci
|
|
103
|
+
* --ignore-scripts`, a `node_modules` restored from a CI cache). Two code paths
|
|
104
|
+
* for the same job is how the two drift apart and how one of them silently stops
|
|
105
|
+
* working.
|
|
106
|
+
*
|
|
107
|
+
* Everything needed goes into **one** tree. Profiles overlap heavily — `js` and
|
|
108
|
+
* `commitlint` share 30 MB — and a directory each left npm nothing to
|
|
109
|
+
* deduplicate: 267 MB against the 211 one merged tree costs for the same set
|
|
110
|
+
* (ticket 087). The set only ever grows: a run needing a profile the directory
|
|
111
|
+
* lacks reinstalls the union, so nothing already fetched is lost.
|
|
112
|
+
*
|
|
113
|
+
* The install itself is a plain `npm ci` against a manifest merged from the ones
|
|
114
|
+
* we generated from our own lockfile, run from the client's repository so their
|
|
115
|
+
* `.npmrc`, registry and proxy settings apply unchanged. `--ignore-scripts` is
|
|
116
|
+
* not negotiable: we are a supply-chain tool, and running arbitrary install hooks
|
|
117
|
+
* from a security check would be indefensible. Integrity is npm's own, over the
|
|
118
|
+
* whole transitive tree — a tampered tarball aborts with `EINTEGRITY` rather than
|
|
119
|
+
* being installed.
|
|
120
|
+
*/
|
|
121
|
+
export async function ensureTooling(wanted, opts) {
|
|
122
|
+
const { install } = opts;
|
|
123
|
+
const needed = normalise(wanted);
|
|
124
|
+
if (needed.length === 0) {
|
|
125
|
+
return { status: 'cached', reason: 'nothing to fetch', profiles: [] };
|
|
126
|
+
}
|
|
127
|
+
const location = resolveCacheDir(opts.version, install.cacheDir);
|
|
128
|
+
if (!location) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'failed',
|
|
131
|
+
reason: 'no writable cache directory (tried the configured path, ~/.cache and the temp dir)',
|
|
132
|
+
profiles: [],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const dir = toolingDir(location.dir);
|
|
136
|
+
const stamp = readStamp(dir);
|
|
137
|
+
if (holdsAll(dir, stamp, needed)) {
|
|
138
|
+
return { status: 'cached', reason: 'already installed', dir, profiles: stamp?.profiles ?? [] };
|
|
139
|
+
}
|
|
140
|
+
// Never shrink: whatever the directory already holds stays, or a repository
|
|
141
|
+
// that alternates between two languages would reinstall on every run.
|
|
142
|
+
const target = normalise([...(stamp?.profiles ?? []), ...needed]);
|
|
143
|
+
let installing = target;
|
|
144
|
+
let manifest = manifestFor(target);
|
|
145
|
+
if (!manifest) {
|
|
146
|
+
// The stamp names a profile this version no longer ships. Keeping it would
|
|
147
|
+
// make the directory permanently un-installable; dropping it costs a refetch
|
|
148
|
+
// of tooling that is obsolete anyway.
|
|
149
|
+
installing = needed;
|
|
150
|
+
manifest = manifestFor(needed);
|
|
151
|
+
}
|
|
152
|
+
if (!manifest) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'failed',
|
|
155
|
+
reason: `no shipped manifest for ${needed.join(', ')}`,
|
|
156
|
+
profiles: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Both of these mean "do not fetch", and both are asked AFTER the cache check
|
|
160
|
+
// above — because neither means "do not use what has already been fetched".
|
|
161
|
+
//
|
|
162
|
+
// `offline` was always in the right place; `manual` was not, and returned before
|
|
163
|
+
// the cache was ever looked at. So `install --all`, the command the product's
|
|
164
|
+
// own log recommends for pre-warming, did nothing for anyone who then set
|
|
165
|
+
// `manual` — including our Docker image, which sets it precisely because
|
|
166
|
+
// everything is supposed to already be there. `Python Types` in that image
|
|
167
|
+
// could not run at all (ticket 083).
|
|
168
|
+
// The env var as well as the config key, exactly as `PR_CHECKMATE_OFFLINE` is
|
|
169
|
+
// read below: the Docker image sets the strategy through the environment, and a
|
|
170
|
+
// guarantee that only holds when it is written in a config file is not a
|
|
171
|
+
// guarantee.
|
|
172
|
+
const strategy = process.env.PR_CHECKMATE_INSTALL_STRATEGY ?? install.strategy;
|
|
173
|
+
if (strategy === 'manual') {
|
|
174
|
+
return {
|
|
175
|
+
status: 'skipped',
|
|
176
|
+
reason: 'install.strategy is "manual" and the tooling is not in the cache',
|
|
177
|
+
dir,
|
|
178
|
+
profiles: stamp?.profiles ?? [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (install.offline || process.env.PR_CHECKMATE_OFFLINE === 'true') {
|
|
182
|
+
return {
|
|
183
|
+
status: 'skipped',
|
|
184
|
+
reason: 'install.offline is set and the tooling is not in the cache',
|
|
185
|
+
dir,
|
|
186
|
+
profiles: stamp?.profiles ?? [],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// The lock file lives in the tooling directory, so the directory has to exist
|
|
190
|
+
// before there is anything to lock. Creating it inside `withLock` would put
|
|
191
|
+
// the creation itself outside the lock, which is the race it exists to close.
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
return {
|
|
197
|
+
status: 'failed',
|
|
198
|
+
reason: `cannot create ${dir}: ${err.message}`,
|
|
199
|
+
profiles: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const release = await withLock(dir, install.waitTimeoutMs ?? 120000);
|
|
203
|
+
if (!release) {
|
|
204
|
+
return { status: 'failed', reason: `cannot write to ${dir}`, dir, profiles: [] };
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
// Re-checked inside the lock: while we waited, the process we were waiting
|
|
208
|
+
// for probably did the work. Without this, waiting is pointless — every
|
|
209
|
+
// queued job would reinstall in turn.
|
|
210
|
+
if (holdsAll(dir, readStamp(dir), needed)) {
|
|
211
|
+
return {
|
|
212
|
+
status: 'cached',
|
|
213
|
+
reason: 'installed by another process',
|
|
214
|
+
dir,
|
|
215
|
+
profiles: readStamp(dir)?.profiles ?? [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return await runInstall(installing, manifest, dir, opts);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
release();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The real fetch: `npm ci` against the staged manifest.
|
|
226
|
+
*
|
|
227
|
+
* Run from the repository under review so the client's `.npmrc` — registry,
|
|
228
|
+
* scopes, proxy, auth — is the one npm reads; we never substitute our own.
|
|
229
|
+
* `--ignore-scripts` is not negotiable in a supply-chain tool.
|
|
230
|
+
*/
|
|
231
|
+
async function npmCi(dir, opts) {
|
|
232
|
+
const result = await execa('npm', ['ci', '--prefix', dir, '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel', 'error'], { cwd: opts.cwd, reject: false, timeout: opts.install.installTimeoutMs ?? 300000 });
|
|
233
|
+
if (result.exitCode === 0)
|
|
234
|
+
return { ok: true };
|
|
235
|
+
const detail = (result.stderr || result.stdout || '').split('\n').slice(-3).join(' ').trim();
|
|
236
|
+
return { ok: false, detail: detail || `exit ${result.exitCode}` };
|
|
237
|
+
}
|
|
238
|
+
/** The fetch itself, once we hold the lock and know it is needed. */
|
|
239
|
+
async function runInstall(profiles, manifest, dir, opts) {
|
|
240
|
+
const started = Date.now();
|
|
241
|
+
writeManifest(dir, manifest);
|
|
242
|
+
const named = profiles.join(', ');
|
|
243
|
+
const count = Object.keys(manifest.lockfile.packages).length - 1;
|
|
244
|
+
// Always logged, never behind --debug: this is a security tool downloading
|
|
245
|
+
// code at run time, and that must be visible in every CI log by default.
|
|
246
|
+
logger.info(`[install]: 📦 fetching tooling for ${named} (${count} packages) → ${dir}`);
|
|
247
|
+
try {
|
|
248
|
+
const install = opts.installer ?? ((target) => npmCi(target, opts));
|
|
249
|
+
const result = await install(dir);
|
|
250
|
+
if (!result.ok) {
|
|
251
|
+
return { status: 'failed', reason: `npm ci failed: ${result.detail}`, dir, profiles: [] };
|
|
252
|
+
}
|
|
253
|
+
const stamp = { hash: hashOf(manifest), profiles };
|
|
254
|
+
fs.writeFileSync(stampPath(dir), JSON.stringify(stamp));
|
|
255
|
+
const durationMs = Date.now() - started;
|
|
256
|
+
logger.info(`[install]: ✅ tooling ready in ${(durationMs / 1000).toFixed(1)}s (${named})`);
|
|
257
|
+
return { status: 'installed', reason: `${count} packages`, dir, profiles, durationMs };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
// A timeout arrives here as a thrown error. It is a skip, not a crash: the
|
|
261
|
+
// rest of the run is still worth doing, and the affected checks say why.
|
|
262
|
+
return {
|
|
263
|
+
status: 'failed',
|
|
264
|
+
reason: `npm ci failed: ${err.message}`,
|
|
265
|
+
dir,
|
|
266
|
+
profiles: [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the process named in a lock is still alive.
|
|
3
|
+
*
|
|
4
|
+
* Signal 0 performs the permission and existence checks without delivering
|
|
5
|
+
* anything. `EPERM` means the process exists but belongs to another user — alive,
|
|
6
|
+
* and not ours to override.
|
|
7
|
+
*/
|
|
8
|
+
export declare function processAlive(pid: number): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Take the lock, waiting for whoever holds it.
|
|
11
|
+
*
|
|
12
|
+
* A held lock is not an error — it is the normal path when jobs run in parallel,
|
|
13
|
+
* and the right response is to wait and then use what the other process fetched.
|
|
14
|
+
* Waiting is announced and its progress printed: a silent wait is indistinguishable
|
|
15
|
+
* from a hang, and the whole point of the log is that a client can tell the
|
|
16
|
+
* difference at a glance.
|
|
17
|
+
*
|
|
18
|
+
* @param dir Profile directory to lock.
|
|
19
|
+
* @param timeoutMs How long to wait before taking the lock by force.
|
|
20
|
+
* @param deps Injected for tests: waiting, the clock, and how liveness of the
|
|
21
|
+
* holding process is decided. The last one matters because `process.kill(pid, 0)`
|
|
22
|
+
* is the one part whose answer depends on the operating system, and a test that
|
|
23
|
+
* depends on it is a test that behaves differently on a CI runner than on the
|
|
24
|
+
* machine it was written on.
|
|
25
|
+
* @returns A release function, or `null` when the lock could not be created at
|
|
26
|
+
* all (an unwritable directory — the caller reports that, it is not fatal here).
|
|
27
|
+
*/
|
|
28
|
+
interface LockDeps {
|
|
29
|
+
sleep?: (ms: number) => Promise<void>;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
alive?: (pid: number) => boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function withLock(dir: string, timeoutMs: number, deps?: LockDeps): Promise<(() => void) | null>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
const lockPath = (dir) => path.join(dir, '.pr-checkmate-install.lock');
|
|
5
|
+
function readLock(file) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether the process named in a lock is still alive.
|
|
15
|
+
*
|
|
16
|
+
* Signal 0 performs the permission and existence checks without delivering
|
|
17
|
+
* anything. `EPERM` means the process exists but belongs to another user — alive,
|
|
18
|
+
* and not ours to override.
|
|
19
|
+
*/
|
|
20
|
+
export function processAlive(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return err.code === 'EPERM';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function withLock(dir, timeoutMs, deps = {}) {
|
|
30
|
+
const sleep = deps.sleep ?? ((ms) => new Promise(r => setTimeout(r, ms)));
|
|
31
|
+
const now = deps.now ?? Date.now;
|
|
32
|
+
const alive = deps.alive ?? processAlive;
|
|
33
|
+
const file = lockPath(dir);
|
|
34
|
+
const deadline = now() + timeoutMs;
|
|
35
|
+
let announced = false;
|
|
36
|
+
for (;;) {
|
|
37
|
+
try {
|
|
38
|
+
const record = { pid: process.pid, startedAt: now(), host: 'local' };
|
|
39
|
+
fs.writeFileSync(file, JSON.stringify(record), { flag: 'wx' });
|
|
40
|
+
return () => {
|
|
41
|
+
try {
|
|
42
|
+
fs.unlinkSync(file);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Already gone: another process broke a lock it believed to be stale.
|
|
46
|
+
// Nothing to undo, and nothing worth failing a run over.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code !== 'EEXIST')
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const held = readLock(file);
|
|
55
|
+
// A lock whose owner died — the job was cancelled, the container was killed —
|
|
56
|
+
// would otherwise block every later run forever. Breaking it needs to be
|
|
57
|
+
// loud: it is the one case where two installs could genuinely overlap.
|
|
58
|
+
if (held && !alive(held.pid)) {
|
|
59
|
+
logger.warn(`[install]: ⚠️ removing a stale lock left by process ${held.pid}, which is no longer running.`);
|
|
60
|
+
try {
|
|
61
|
+
fs.unlinkSync(file);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Someone else got there first; the next iteration will find it gone.
|
|
65
|
+
}
|
|
66
|
+
// Yield before retrying. Both `continue`s below used to loop straight back
|
|
67
|
+
// to `writeFileSync`, so an unlink that kept failing became a tight loop
|
|
68
|
+
// that never returned to the event loop: no output, no timer, no timeout
|
|
69
|
+
// — a process pegged at 100% until something killed it. A hang with no
|
|
70
|
+
// symptom is the worst failure mode this code could have.
|
|
71
|
+
await sleep(50);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (now() >= deadline) {
|
|
75
|
+
logger.warn(`[install]: ⚠️ waited ${Math.round(timeoutMs / 1000)}s for another process to finish installing — taking over.`);
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(file);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Same as above.
|
|
81
|
+
}
|
|
82
|
+
await sleep(50);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!announced) {
|
|
86
|
+
const age = held ? Math.round((now() - held.startedAt) / 1000) : 0;
|
|
87
|
+
logger.info(`[install]: ⏳ another process is installing into ${path.basename(dir)} (started ${age}s ago) — waiting up to ${Math.round(timeoutMs / 1000)}s`);
|
|
88
|
+
announced = true;
|
|
89
|
+
}
|
|
90
|
+
await sleep(500);
|
|
91
|
+
}
|
|
92
|
+
}
|