diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..d20c0fe --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60f5fe1..124bdf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,36 @@ jobs: - name: Test (unit + property + composition + F1) run: cargo test --all-targets + npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.21.0 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - uses: denoland/setup-deno@v2 + + - name: Install JS deps (frozen) + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Build + typecheck (launcher) + run: pnpm build && pnpm typecheck + + - name: Release scripts lint + matrix agreement gate (deno) + run: | + cd scripts + deno task lint + deno task check-matrix + mutation: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..06c7b94 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,365 @@ +# Release pipeline: tag-triggered publish of the five per-platform binary +# packages plus the root launcher, all with npm OIDC provenance (no static +# tokens). Version comes only from the tag (KTD5); platform packages are +# published before the root so the root's optionalDependencies resolve. +# +# Fork safety: `push: tags` — only users with push access can push tags. +# pull_request_target is intentionally not used. The npm trusted-publisher +# record binds workflow filename + environment, not tag patterns; the +# `on: refs/tags/v*` filter is the tag gate. +# +# Note: release flows from tags only; ci.yml gates the default branch. + +name: Release + +on: + push: + tags: + - 'v*' + +permissions: {} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # One job per target in the matrix: build, gate, smoke, upload artifacts, + # publish the platform package with provenance (OIDC, no static token). + release: + name: release-${{ matrix.suffix }} + runs-on: ${{ matrix.runner }} + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + suffix: linux-x64 + runner: ubuntu-latest + - target: aarch64-unknown-linux-gnu + suffix: linux-arm64 + runner: ubuntu-24.04-arm + - target: x86_64-apple-darwin + suffix: darwin-x64 + runner: macos-13 + - target: aarch64-apple-darwin + suffix: darwin-arm64 + runner: macos-14 + - target: x86_64-pc-windows-msvc + suffix: win32-x64 + runner: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + # KTD2: the binary name comes from the canonical table, not a second + # copy of the win32->.exe rule. + - name: Resolve binary name from table + shell: bash + run: | + BINARY_NAME="$(jq -r --arg target "${{ matrix.target }}" '.[] | select(.target == $target) | .bin' scripts/release/targets.json)" + test -n "$BINARY_NAME" || { + echo "target ${{ matrix.target }} missing from scripts/release/targets.json" >&2 + exit 1 + } + echo "BINARY_NAME=$BINARY_NAME" >> "$GITHUB_ENV" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 + with: + targets: ${{ matrix.target }} + + - name: Linux arm64 cross-linker + # Only needed when building aarch64 from an x64 runner. The native + # ubuntu-24.04-arm runner builds aarch64 without a cross toolchain. + if: matrix.target == 'aarch64-unknown-linux-gnu' && matrix.runner == 'ubuntu-latest' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + { + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" + echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" + } >> "$GITHUB_ENV" + + - name: Cache cargo + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('Cargo.lock') }} + + - name: Build release binary + run: cargo build --release --target ${{ matrix.target }} + + - name: Install Deno (release scripts) + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + + - name: Setup Node 24 (publish; npm >= 11.5.1 for trusted publishing) + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + # NODE_AUTH_TOKEN intentionally unset — the registry-url wiring + # performs the OIDC exchange for trusted publishing. + + - name: Install pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.21.0 + + - name: "Gate: check-matrix (table vs workflow agreement)" + run: | + deno run --allow-read=scripts/release/targets.json,npm/packages/comment-checker/package.json,.github/workflows/release.yml scripts/release/check-matrix.ts + + - name: "Gate: binary exists" + shell: bash + run: | + test -f "target/${{ matrix.target }}/release/${BINARY_NAME}" + + - name: "Gate: binary smoke tests" + shell: bash + run: | + BIN="target/${{ matrix.target }}/release/${BINARY_NAME}" + echo '{"tool_name":"Write","tool_input":{"file_path":"src/client.py","content":"# SPDX-License-Identifier: Apache-2.0\ndef load(path):\n return open(path).read()\n"}}' | "$BIN" + rc=$? + test "$rc" -eq 0 || { echo "clean payload exited $rc, expected 0" >&2; exit 1; } + echo '{"tool_name":"Write","tool_input":{"file_path":"src/load_config.py","content":"def load_config(path):\n # Parse the config file\n data = json.load(open(path))\n # TODO: fix this later\n return data\n"}}' | "$BIN" >/dev/null 2>&1 + rc=$? + test "$rc" -eq 2 || { echo "flagged payload exit $rc, expected 2" >&2; exit 1; } + + - name: Stage platform package (outside workspace) + record binarySha256 + shell: bash + run: | + # Portable sha256: sha256sum (Linux/Windows Git Bash), else shasum + # (macOS). Must agree with the cross-check step's digest. + sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi + } + STAGE="$RUNNER_TEMP/platform-${{ matrix.suffix }}" + mkdir -p "$STAGE" + BIN="target/${{ matrix.target }}/release/${BINARY_NAME}" + cp "$BIN" "$STAGE/${BINARY_NAME}" + SHA="$(sha256_of "$STAGE/${BINARY_NAME}")" + deno run \ + --allow-read=scripts/release/targets.json,npm/packages/comment-checker/package.json \ + --allow-write="$STAGE" \ + scripts/release/generate-platform-manifest.ts \ + --suffix "${{ matrix.suffix }}" \ + --version "${GITHUB_REF#refs/tags/v}" \ + --binary-sha256 "$SHA" \ + --out "$STAGE" + echo "$SHA" > "$RUNNER_TEMP/binary-${{ matrix.suffix }}.sha256" + echo "STAGE=$STAGE" >> "$GITHUB_ENV" + + - name: Upload tar.gz + sha sidecar + shell: bash + run: | + cd "target/${{ matrix.target }}/release" + tar -czf "$RUNNER_TEMP/comment-checker-${{ matrix.target }}.tar.gz" "${BINARY_NAME}" + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: release-${{ matrix.suffix }} + path: | + ${{ runner.temp }}/comment-checker-${{ matrix.target }}.tar.gz + ${{ runner.temp }}/binary-${{ matrix.suffix }}.sha256 + + - name: Publish platform package (OIDC provenance) + shell: bash + run: | + cd "$STAGE" + pnpm publish --provenance --access public --no-git-checks + + publish-npm-main: + name: Publish root launcher + needs: release + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: "Tag gate: derive VERSION from tag" + shell: bash + run: | + VERSION="${GITHUB_REF#refs/tags/v}" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "invalid tag semver: '$VERSION'" >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + - name: "Tag gate: commit reachable from default branch" + shell: bash + run: | + DEFAULT_BRANCH="$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)" + git fetch origin "$DEFAULT_BRANCH" --depth=1 + git merge-base --is-ancestor "$GITHUB_SHA" "origin/$DEFAULT_BRANCH" || { + echo "tag commit not an ancestor of $DEFAULT_BRANCH; refusing publish" >&2 + exit 1 + } + + - name: Setup Node 24 (npm >= 11.5.1 for trusted publishing) + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + # No NODE_AUTH_TOKEN — OIDC trusted publishing only. + + - name: Install pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.21.0 + + - name: Install Deno (release scripts) + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + + - name: Download recorded binary sha sidecars + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: release-* + merge-multiple: true + path: sidecars + + - name: "Gate: every platform package published with table os/cpu/libc" + shell: bash + run: | + # Shape-normalize BOTH sides: npm view reports libc as an array + # ("libc":["glibc"]) while the table stores a bare string, and + # darwin/win32 rows have no libc at all. Comparing through the same + # normalization (libc as array, absent when null) makes the deep + # equality meaningful instead of always-true or always-false. + libc_norm='{os, cpu} + (if (.libc // null) != null then {libc} else {} end)' + SUFFIXES="$(jq -r '.[].suffix' scripts/release/targets.json)" + test -n "$SUFFIXES" || { echo "targets.json empty" >&2; exit 1; } + for SUFFIX in $SUFFIXES; do + PKG="@systemfsoftware/claude-code-comment-checker-${SUFFIX}" + META="$(npm view "$PKG@$VERSION" version os cpu libc --json)" || { + echo "platform package $PKG@$VERSION missing" >&2 + exit 1 + } + EXPECTED="$(jq -c --arg suffix "$SUFFIX" '.[] | select(.suffix == $suffix) | {os: [.os], cpu: [.cpu]} + (if (.libc // null) != null then {libc: [.libc]} else {} end)' scripts/release/targets.json)" + echo "$META" | jq -e -c --arg v "$VERSION" --argjson want "$EXPECTED" \ + '.version == $v and ('"$libc_norm"') == $want' >/dev/null || { + echo "$PKG@$VERSION mismatch: $(echo "$META" | jq -c '{version, os, cpu, libc}') want $EXPECTED" >&2 + exit 1 + } + echo "$SUFFIX ok" + done + + - name: Cross-check published tarballs vs recorded binary sha + shell: bash + run: | + # npm registry read-after-write is eventually consistent: the + # platform publish finished only seconds ago, so a first `npm pack` + # may still 404. Retry a bounded number of times before failing. + sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi + } + SUFFIXES="$(jq -r '.[].suffix' scripts/release/targets.json)" + test -n "$SUFFIXES" || { echo "targets.json empty" >&2; exit 1; } + for SUFFIX in $SUFFIXES; do + SHA_RECORDED="$(cat "sidecars/binary-${SUFFIX}.sha256")" + PKG="@systemfsoftware/claude-code-comment-checker-${SUFFIX}" + TARBALL="" + for attempt in 1 2 3 4 5; do + TARBALL="$(npm pack "$PKG@${VERSION}" --pack-destination "$RUNNER_TEMP" 2>/dev/null | tail -1)" + if [ -n "$TARBALL" ] && [ -f "$RUNNER_TEMP/$TARBALL" ]; then + break + fi + echo "npm pack $PKG@$VERSION attempt $attempt/5 failed; retrying" >&2 + TARBALL="" + sleep 5 + done + if [ -z "$TARBALL" ]; then + echo "$PKG@$VERSION not retrievable from registry after 5 attempts" >&2 + exit 1 + fi + rm -rf "$RUNNER_TEMP/unpack" + mkdir -p "$RUNNER_TEMP/unpack" + tar -xzf "$RUNNER_TEMP/$TARBALL" -C "$RUNNER_TEMP/unpack" + BIN="$(find "$RUNNER_TEMP/unpack" -type f \( -name 'comment-checker' -o -name 'comment-checker.exe' \) | head -1)" + SHA_PUBLISHED="$(sha256_of "$BIN")" + if [ "$SHA_RECORDED" != "$SHA_PUBLISHED" ]; then + echo "binary sha mismatch for $SUFFIX: recorded $SHA_RECORDED, got $SHA_PUBLISHED" >&2 + exit 1 + fi + echo "$SUFFIX ok" + done + + - name: Build launcher (frozen) + run: | + pnpm install --frozen-lockfile --registry https://registry.npmjs.org + pnpm -r build + + - name: Sync root version + optionalDependencies from tag + shell: bash + run: | + # --allow-env required: VERSION arrives via the environment (KTD5: + # the git tag is the single version source). The deno.jsonc + # manifest:sync-root task declares the identical permission set. + VERSION="$VERSION" deno run \ + --allow-env \ + --allow-read=scripts/release/targets.json,npm/packages/comment-checker/package.json \ + --allow-write=npm/packages/comment-checker/package.json \ + scripts/release/sync-root-version.ts + + - name: Publish root launcher (OIDC provenance) + shell: bash + run: | + cd npm/packages/comment-checker + pnpm publish --provenance --access public --no-git-checks + + - name: Verify root publish + exact pins + shell: bash + run: | + ROOT_META="$(npm view "@systemfsoftware/claude-code-comment-checker@$VERSION" version optionalDependencies --json)" + echo "$ROOT_META" | jq -e -c --arg v "$VERSION" '.version == $v' >/dev/null || { + echo "root version mismatch: $(echo "$ROOT_META" | jq -c '.version')" >&2 + exit 1 + } + SUFFIXES="$(jq -r '.[].suffix' scripts/release/targets.json)" + for SUFFIX in $SUFFIXES; do + echo "$ROOT_META" | jq -e --arg k "@systemfsoftware/claude-code-comment-checker-${SUFFIX}" --arg v "$VERSION" \ + '.optionalDependencies[$k] == $v' >/dev/null || { + echo "optional pin missing for $SUFFIX@$VERSION" >&2 + exit 1 + } + done + + upload-gh-release-assets: + needs: release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Download platform artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: release-assets + + - name: Attach tarballs to GitHub release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + files: release-assets/**/*.tar.gz \ No newline at end of file diff --git a/.gitignore b/.gitignore index bf75c86..0fe729e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules *.log mutants.out* npm/bin +.turbo/ .DS_Store dist +dist-types *.tsbuildinfo diff --git a/CONCEPTS.md b/CONCEPTS.md index 17c2077..a5ba0ce 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -42,6 +42,30 @@ A per-kind/precision-recall gate on the corpus that trips when a kind's classifier weakens — including a single-case kind that goes wrong — so a weakness in one kind cannot hide inside an aggregate F1 score. +## npm distribution + +### Launcher +The root npm package (`@systemfsoftware/claude-code-comment-checker`) whose +`bin` is the `comment-checker` shim. It resolves the host platform package by +identity at runtime and spawns the binary — the only package that declares a +bin. + +### Platform package +One per os-cpu pair (`-linux-x64`, `-darwin-arm64`, …), generated from +`scripts/release/targets.json`: ships only the compiled binary and its +manifest (`os`/`cpu`/`libc` fields, no `bin`). The launcher's +`optionalDependencies` pins all five to the release version. + +The committed launcher manifest never lists these packages as +`optionalDependencies` — pnpm cannot lock unpublished platform packages +(pnpm#3960), so the pins are injected from the targets table at publish +time; absence in-tree is expected, not a defect. + +### Release lane +A matrix row in the release workflow: one platform/arch build, gate, smoke, +and publish run on its native runner. Platforms publish before the launcher, +and the release cannot proceed if any lane fails. + ## Flagged ambiguities - "context" had been used for both the language (scope/position) and the diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 9f30d41..17f5e22 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,48 @@ cargo install --git https://github.com/systemfsoftware/comment-checker --package Requires Rust 1.85+. Each [GitHub release](https://github.com/systemfsoftware/comment-checker/releases) also attaches `comment-checker-.tar.gz` tarballs for direct download. +## Publishing + +Releases are tag-triggered: pushing a tag `vX.Y.Z` to `main` runs +[`.github/workflows/release.yml`](.github/workflows/release.yml), which builds +all five target binaries, publishes the five platform packages and then the +root launcher — all with npm OIDC trusted publishing and provenance, no static +tokens in CI. The exact step sequence and per-package trusted-publisher bindings are documented +in the release plan +(`docs/plans/2026-08-17-001-feat-npm-distribution-release-plan.md`) and the +first-release checklist (`docs/publishing/first-release-checklist.md`). + +To release: + +1. Create the six [npm trusted-publisher + entries](https://docs.npmjs.com/generating-provenance-statements) with the + bindings below (one time). +2. Push a semver tag: `git tag v0.1.0 && git push origin v0.1.0`. +3. Watch CI; the root package is published only after every platform package + exists and its published binary matches the recorded sha256. +4. Verify post-publish: a fresh `pnpm dlx`/`npm i -g` install of the root + package, run the binary on Linux and on one non-Linux platform, and confirm + `npm view @systemfsoftware/claude-code-comment-checker provenance` shows + provenance. + +The npm trusted-publisher records bind the publishing identity to this +workflow — the registry-side record has no tag-pattern field, so +`refs/tags/v*` is enforced by the workflow's `on: push: tags` filter, never by +the registry-side record: + +| Package | Trusted publisher binding | +|---|---| +| `@systemfsoftware/claude-code-comment-checker` | Org: `systemfsoftware`, repo: `comment-checker`, workflow: `.github/workflows/release.yml` | +| `@systemfsoftware/claude-code-comment-checker-linux-x64` | same | +| `@systemfsoftware/claude-code-comment-checker-linux-arm64` | same | +| `@systemfsoftware/claude-code-comment-checker-darwin-x64` | same | +| `@systemfsoftware/claude-code-comment-checker-darwin-arm64` | same | +| `@systemfsoftware/claude-code-comment-checker-win32-x64` | same | + +The version pinned in the committed launcher manifest (`0.1.0`) may lag behind +releases by design — the git tag is the single version source; the release +workflow rewrites the published manifest. + ## Quick Start 1. Install (above). diff --git a/docs/plans/2026-08-17-001-feat-npm-distribution-release-plan.md b/docs/plans/2026-08-17-001-feat-npm-distribution-release-plan.md new file mode 100644 index 0000000..9475006 --- /dev/null +++ b/docs/plans/2026-08-17-001-feat-npm-distribution-release-plan.md @@ -0,0 +1,325 @@ +--- +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-plan-bootstrap +execution: code +created: 2026-08-17 +updated: 2026-08-17 +deepened: 2026-08-17 +type: feat +--- + +# npm Distribution Release - Plan + +## Goal Capsule + +- **Objective:** Publish the first npm release of comment-checker — five platform binary packages plus a root launcher package, shipped by a tag-triggered GitHub Actions release pipeline with OIDC provenance, no install-time scripts, and verified installs. +- **Product authority:** User-directed. The user confirmed the distribution scope ("OK plan the release of this thing") on 2026-08-17; the distribution pattern (per-platform packages + `optionalDependencies`, Node launcher) was examined in-session and accepted. Publishing to npm under the `systemfsoftware` org is a human-controlled action per AGENTS.md — the pipeline stages everything; a human cuts the tag. +- **Open blockers:** None. npm trusted-publisher entries (one per package, six total) and the package names require one-time org-admin setup, owned by the first-release checklist (U5). +- **Stop conditions:** the plan is done when `v0.1.0` is live on npm with provenance, a fresh install on Linux and on one of macOS/Windows runs the real binary, and `--ignore-scripts` installs still work. + +--- + +## Summary + +Comment-checker is a Rust CLI distributed today via `cargo install` and GitHub-release tarballs. This plan lands the npm distribution layer built in `npm/packages/comment-checker/`: five per-platform binary packages named `@systemfsoftware/claude-code-comment-checker--`, and a root launcher package `@systemfsoftware/claude-code-comment-checker`. Package managers install exactly one platform package per machine; the Node launcher resolves and spawns it. The release pipeline rebuilds the un-merged `feat/npm-optional-dependencies` branch work with its gaps fixed: a tag-triggered GitHub Actions workflow publishing platform packages first, then the root, all with OIDC provenance and no static token. + +## Problem Frame + +A Claude Code hook distributed via npm must give each user a working native binary with zero friction and zero install-time execution. The previous distribution experiments (`feat/npm-optional-dependencies`, `feat/modern-npm-distribution-pnpm-11`) produced an implemented launcher and a draft release workflow but never merged: the current tree has no launcher manifest, no release workflow, and no published packages. The SOTA classifier plan (`docs/plans/2026-08-12-002-feat-sota-comment-adjudication-plan.md`) explicitly deferred the npm distribution layer to follow-up. The npm layer changes no Rust code; it makes the binary installable. + +**Premise for this release.** The audience is Claude Code hook users, and Claude Code runs on Node — every hook user therefore already has a Node runtime, while `cargo install` demands a Rust toolchain this audience commonly lacks. The existing paths (cargo install from git, GitHub tarballs) stay live for Rust users and for non-npm sinks; npm closes the gap for the primary audience at zero install-time code execution. The release decision is user-directed in-session; demand validation (download counts vs the cargo-install era, install-friction issues) is tracked as an open adoption signal after `v0.1.0` rather than a precondition. + +## Requirements + +**Packaging** + +- R1. Per-platform packages. Exactly one npm package per supported target, named `@systemfsoftware/claude-code-comment-checker--`, containing only the native binary and a manifest declaring `os`/`cpu` (`libc` on Linux, see KTD4). Binary name is `comment-checker` on POSIX, `comment-checker.exe` on Windows. +- R2. Root launcher package. `@systemfsoftware/claude-code-comment-checker` exposes `bin` → the built ESM launcher, ships only the built launcher in `files`, and runs no `postinstall` or any install-time script. +- R3. Install-time selection. A plain install on Linux x64 installs exactly `…-linux-x64` and yields a working binary. Supported: Linux (x64, arm64), macOS (x64, arm64), Windows (x64). + +**Release pipeline** + +- R4. Tag-triggered publish. Pushing tag `vX.Y.Z` builds the release binary for all five targets, publishes the five platform packages then the root package at version `X.Y.Z`, with `optionalDependencies` pinned to that exact version (never a range). +- R5. Trusted publishing. All publishes use OIDC (`id-token: write`) and npm provenance; no static token is stored in CI or repo. +- R6. GitHub release tarballs. The per-target `.tar.gz` archives remain attached to the release for non-npm users. + +**Verification** + +- R7. Every platform's binary is smoke-tested before publish; the published root is verified after publish; the launcher resolution is covered by automated tests. + +**Documentation** + +- R8. README install/publish claims match the live state after the first release, and maintainers have documented publish steps (tag flow, trusted-publisher setup, post-publish verification). + +## Key Decisions + +- KD1 — Ship the README-documented matrix (5 targets: Linux x64/arm64, macOS x64/arm64, Windows x64). No musl, no win32-arm64 in the first release; matches the README/FAQ, prior plan, and workflow drafts. +- KD2 — No install-time download or fallback. A missing binary fails cleanly with `BinaryNotFound` naming the platform. Chosen over a Sentry-style postinstall fallback: the fallback reintroduces network access and script execution at install time, and the launcher's current behavior already satisfies this. *(session-settled: user-approved — chosen over an install-time download fallback: keeps zero install-time network/script execution.)* +- KD3 — Provenance on every published package and no static tokens (OIDC identity from the Actions runner). +- KD4 — Distribution via per-platform packages + `optionalDependencies` in a root launcher package. *(session-settled: user-approved — chosen over single-package postinstall download and all-platforms-in-one-tarball: managers select one platform package natively, no install scripts.)* +- KD5 — The package entry is a Node launcher. *(session-settled: user-approved — chosen over Deno/Bun shims: npm/pnpm/yarn are Node programs, so Node is the one runtime the distribution channel guarantees.)* + +--- + +## Planning Contract + +### Key Technical Decisions + +- **KTD1 — Pin the Effect v4 RC deps to the vendored revision.** The launcher depends on `effect@4.0.0-rc.108` and `@effect/platform-node@4.0.0-rc.108` — the exact versions matching the `repos/effect` subtree — resolved from the npm registry by pnpm and bundled by tsdown from the installed packages (which ship TS source; `node:` built-ins stay external). The vendored subtree is reference only, never a build input. *(Alternative rejected: `file:`-linking the vendored subtree's packages into the pnpm workspace — pulls a multi-package upstream subtree into the workspace and inflates install.)* The published surface is the packed tarball, not the workspace build; the rehearsal verifies `npm pack` output per package (U3, Verification Contract). +- **KTD2 — One canonical targets table.** `scripts/release/targets.json` declares the five targets (triple, suffix, `os`/`cpu`/`libc`, bin name). The manifest generator and the workflow's matrix check consume it; a unit test keeps it consistent. The launcher derives package/group names by identity from `process.platform`/`process.arch`, so the only drift surface is the table vs the workflow matrix, closed by a workflow check step (`check-matrix`). +- **KTD3 — Release workflow shape.** `push: tags ['v*']` → one `release` job per target in a 5-row matrix (build, gate, smoke, tar.gz upload, then publish the platform package via `pnpm publish --provenance`), then `publish-npm-main` (`needs: release`) installs frozen lockfile, builds the launcher, syncs versions from the tag, and publishes the root; `upload-gh-release-assets` attaches tarballs. The un-merged draft workflow is precedent; this shape adds the pre-publish gate and smoke, and a tested version-sync script. +- **KTD4 — Linux packages declare `"libc": ["glibc"]`.** Without it, musl/Alpine users (also `linux-x64`) install the glibc binary and hit a loader crash at runtime instead of the intended clean `BinaryNotFound`. Modern npm and pnpm honor the `libc` field and skip a mismatched platform package. +- **KTD5 — The Git tag is the single version source.** The workflow rewrites launcher `version` and every `optionalDependencies` value to the tag version via a tested script; the committed manifest may lag behind (stays at `0.1.0` until the first bump). Full version automation is deferred. + +### High-Level Technical Design + +**Target matrix** + +| Target | Runner | Suffix | os | cpu | libc | Bin | +|---|---|---|---|---|---|---| +| `x86_64-unknown-linux-gnu` | ubuntu-latest | `linux-x64` | linux | x64 | glibc | `comment-checker` | +| `aarch64-unknown-linux-gnu` | ubuntu-latest + cross linker | `linux-arm64` | linux | arm64 | glibc | `comment-checker` | +| `x86_64-apple-darwin` | macos-13 | `darwin-x64` | darwin | x64 | — | `comment-checker` | +| `aarch64-apple-darwin` | macos-14 | `darwin-arm64` | darwin | arm64 | — | `comment-checker` | +| `x86_64-pc-windows-msvc` | windows-2022 | `win32-x64` | win32 | x64 | — | `comment-checker.exe` | + +**Release flow** + +```mermaid +flowchart LR + T["vX.Y.Z tag"] --> M["matrix job × 5 targets"] + M --> B["cargo build --release --target"] + B --> G["gate: binary exists + executable + smoke run"] + G --> A["upload tar.gz artifact"] + G --> P["publish platform pkg --provenance"] + A --> U["upload-gh-release-assets"] + P --> N["publish-npm-main (needs matrix)"] + N --> S["sync version + optionalDeps to tag"] + S --> R["publish root pkg"] + R --> V2["verify: npm view + fresh install"] +``` + +**Consumer path** + +```mermaid +flowchart LR + I["npm/pnpm install root pkg"] --> M2["manager matches os/cpu/libc"] + M2 --> P["installs exactly one platform pkg"] + P --> L["node dist/index.mjs"] + L --> RC["require.resolve(platform pkg/package.json)"] + RC --> S2["spawn binary, inherit stdio"] + RC -. not installed .-> E["BinaryNotFound: platform + arch + package"] +``` + +### Assumptions + +- The npm org admin pre-creates trusted-publisher entries for all six packages and the five package names are available; missing setup fails fast at publish. +- The `systemfsoftware` npm identity is authorized to publish all six names. The human who cuts the tag is the authority gate per AGENTS.md. +- `pnpm-workspace.yaml` (packages `npm/packages/*`) stays unchanged; the launcher resolves Effect from the registry pinned at KTD1. +- Node `>=18` is the launcher floor (matches `@effect/platform-node` engines); CI gate runs node 20; publish jobs run Node 24 (npm ≥ 11.5.1 required for trusted publishing). + +--- + +## Implementation Units + +### U1. Launcher package manifest + workspace lockfile + +**Goal:** Give `npm/packages/comment-checker/` a valid publishable manifest and make pnpm workspace build/typecheck/install work. + +**Requirements:** R2, R3. + +**Dependencies:** none. + +**Files:** +- `npm/packages/comment-checker/package.json` — create +- `npm/packages/comment-checker/tsdown.config.ts` — confirm the single ESM output builds from the Effect source with `node:` built-ins external; adjust only if the Effect source breaks the single-file build +- `pnpm-lock.yaml` — generate and commit the root lockfile (none exists today; a first non-frozen `pnpm install` produces it, and the CI `--frozen-lockfile` steps only hold once it is committed) +- `package.json` (root) — unchanged + +**Approach:** +- Author the manifest (it does not exist in this worktree; the `feat/npm-optional-dependencies` variant is precedent only): `name: "@systemfsoftware/claude-code-comment-checker"`; `bin: { "comment-checker": "./dist/index.mjs" }`; `files: ["dist"]`; `scripts: { "build": "tsdown", "typecheck": "tsc -p tsconfig.json --noEmit" }` (satisfies the root `pnpm -r build`/`typecheck`); no `postinstall`/`prepare`; no `private` field (the workspace root's `private: true` does not propagate); `dependencies`: `effect` + `@effect/platform-node` pinned `4.0.0-rc.108` (KTD1); `@types/node` as devDependency; `engines: { "node": ">=18" }` (verified at rehearsal — the floor is asserted until `dist` runs on Node 18, not inherited); `publishConfig: { access: "public", provenance: true }`; `optionalDependencies` listing the five suffixed names at the package version. +- Replace the hardcoded `version: "0.1.0"` in the launcher with a runtime read of the launcher's own `package.json` (via `createRequire`) so `comment-checker --version` can never drift from the published manifest (KTD5); `src/index.ts` is edited in U1, `sync-root-version.mjs` stays the only external writer. +- Generate the lockfile with a first non-frozen `pnpm install` pinned to the canonical registry (`--registry https://registry.npmjs.org`) at the workspace root; commit it; thereafter frozen installs in CI only — the lockfile is never regenerated arbitrarily. +- Verify `pnpm -r build` emits `dist/index.mjs` and `pnpm -r typecheck` passes with the Effect language-service rules intact. + +**Approach detail:** the launcher source already implements `optionalDepName(platform, arch)` returning `@systemfsoftware/claude-code-comment-checker--` — keep it; the manifest must list exactly those names. + +**Test scenarios:** +- Manifest assertions: no `postinstall`; `files` = `["dist"]`; `optionalDependencies` has five entries equal to the launcher's name convention; `publishConfig.provenance` true; `engines.node` = `">=18"`. +- `pnpm -r build` and `pnpm typecheck` succeed; `dist/index.mjs` exists. +- Running `node dist/index.mjs` on a machine without the platform package exits non-zero and prints `BinaryNotFound` naming the expected package. + +**Verification:** `pnpm -r build` + the `BinaryNotFound` smoke (until the platform package exists, that is the expected launcher behavior; at release it must not trip). Also: `pnpm lint` stays green once the new `scripts/` and `tests/` files exist (U2/U4 own that surface), and the built `dist/index.mjs` runs on Node 18 (rehearsal proves this — engines is asserted, not inherited). + +### U2. Release scripts and unit tests + +**Goal:** Extract platform-manifest generation and version sync into tested scripts behind `targets.json`, so the workflow is thin and verifiable without a real release. + +**Requirements:** R1, R4, R7. + +**Dependencies:** U1 (manifest shape); defines the five names U1 must list. + +**Files:** +- `scripts/release/targets.json` — five entries `({ target, suffix, os, cpu, libc?, bin })`. +- `scripts/release/generate-platform-manifest.mjs` — given a suffix and version, emits the platform package.json: name, version, description, license, repository, os/cpu, libc when the table says so, files `[bin]`, and `publishConfig: { access: "public", provenance: true }` (mirrors the launcher manifest so provenance attaches even if an operator publishes without the `--provenance` flag). Supports `--dry-run` (prints, does not write). +- `scripts/release/sync-root-version.mjs` — rewrites the launcher manifest's `version` and all `optionalDependencies` values from a `VERSION` env; validates the value against `^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$` and errors before any write on a malformed tag (no inline `node -e` JSON concatenation anywhere — the script is the only writer). Dry-run prints the diff. +- `scripts/release/check-matrix.mjs` — asserts the five table entries and the workflow matrix agree (os/cpu derivable from suffix by identity). +- `tests/release/release-scripts.test.mjs` — `node:test` suite. + +**Approach:** +- Dependency-free Node scripts; `node:test` runner (Node 18+ built-in), no new toolchain. +- `targets.json` is the single source for suffix → os/cpu/libc; `check-matrix` and the unit tests keep it consistent with the launcher's identity naming (KTD2). +- Brownfield note: neither `scripts/` nor `tests/` exists at repo root today — the entire `scripts/release/` and `tests/release/` trees are net-new; no precedent path is reused from the un-merged branch. + +**Test scenarios:** +- For each of the five table entries: `generate-platform-manifest` emits a manifest with correct os/cpu/libc/bin, name `@systemfsoftware/claude-code-comment-checker-`, `libc` present only for linux entries, `.exe` bin on win32. +- `--dry-run` leaves no files changed (tree identical before/after). +- `sync-root-version` with `VERSION=0.1.0` sets `version` + all five optionalDependencies to `0.1.0`; with `0.2.0` the only diff is the version fields. +- Malformed tag values (`v0.1.0"`, `0.1.0\n--provenance=false`, `not-a-version`) → `sync-root-version` exits non-zero and writes nothing (semver gate). +- Unknown suffix → error listing the five supported suffixes. +- `check-matrix` fails when a table entry's os/cpu is inconsistent with the identity convention; passes otherwise. + +**Verification:** `node --test tests/release` green. + +### U3. Release workflow + +**Goal:** The GitHub Actions pipeline that builds, gates, publishes all six packages, and attaches tarballs. + +**Requirements:** R1–R7. + +**Dependencies:** U1, U2. + +**Files:** +- `.github/workflows/release.yml` — create (supersedes the un-merged draft on `feat/npm-optional-dependencies`). +- `.github/workflows/ci.yml` — unchanged; note in the new file that release flows from tags, not from CI (which gates `main`). + +**Approach:** +- Workflow-level `permissions: {}` — nothing inherited; each job grants only what it needs. `release` matrix jobs: `id-token: write` (npm OIDC); `upload-gh-release-assets`: `contents: write` via the default `GITHUB_TOKEN` only (no PAT, no per-job secrets — re-stated in the DoD); `publish-npm-main`: `id-token: write`. +- `concurrency: { group: release-${{ github.ref }}, cancel-in-progress: false }` at workflow level — two simultaneous tag pushes must never race the publish (npm versions are immutable; cancelling mid-publish would strand a half-published root). `cancel-in-progress: false` is deliberate. +- Publish jobs use `actions/setup-node@v4` with `node-version: 24` (npm ≥ 11.5.1 is required for trusted-publishing OIDC) and `registry-url: https://registry.npmjs.org` — the registry URL is what wires the npm OIDC exchange; do NOT set `NODE_AUTH_TOKEN` (an empty token line overriding OIDC is the documented breakage). `dtolnay/rust-toolchain@stable` with the target; linux-arm64 installs `gcc-aarch64-linux-gnu` and sets `CC_aarch64_unknown_linux_gnu`/`CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER`/`AR_aarch64_unknown_linux_gnu` (env-var approach keeps the `.cargo/config.toml` `[env]` intact for TSLP_LINK_MODE=static). Cargo cache key includes the matrix target (`${{ runner.os }}-${{ matrix.target }}-cargo-…`) so linux x64/arm64 do not thrash one cache. +- Action pinning: every third-party action is declared by tag name in this plan for identification, and pinned to its full commit SHA in the workflow file (per DoD); the SHA → tag mapping lives in the U5 maintenance duty alongside a dependabot `github-actions` group so pins are updated deliberately. +- Tag-gate before any publish (top of `publish-npm-main` and re-checked in the matrix): derive `VERSION` from `${GITHUB_REF#refs/tags/v}` and reject anything failing the semver pattern (U2's gate) — exit non-zero before any write or publish. Also verify the tag's commit is reachable from the default branch (`git merge-base --is-ancestor `) so a stray tag can't publish un-reviewed content. +- Gate before platform publish (per target): `check-matrix` assertion; binary exists; binary is executable (POSIX); smoke — pipe the README's clean payload (expect exit 0) and one flagged payload (expect exit 2); record `binarySha256` (from `sha256sum` of the exact staged binary) into the generated manifest and upload a sidecar artifact. +- Publish platform: stage in a temp dir **outside** the workspace (`$RUNNER_TEMP/…`) with `generate-platform-manifest` + the binary; dependency-free manifests need no `pnpm install`, and staging out-of-tree avoids workspace-context surprises; then `pnpm publish --provenance --access public` from that dir. +- After the matrix, before the root publish, `publish-npm-main` re-verifies the registry: `npm view -@ version os cpu libc` for all five suffixes — equality with `targets.json`, plus the `npm pack` + `sha256sum` cross-check against the recorded `binarySha256`. Any mismatch fails the run before the root publish (the root's `optionalDependencies` must not point at missing packages). +- Then: `pnpm install --frozen-lockfile` (registry pinned), `pnpm -r build`, `VERSION=… node scripts/release/sync-root-version.mjs` (tested script; no inline `node -e`), `pnpm publish --no-git-checks --provenance --access public` from the launcher dir → verify root via `npm view` (exact optional pins + provenance available). +- `upload-gh-release-assets` (`needs: release`, `permissions: { contents: write }`): `actions/download-artifact@v4` with `pattern: release-*`, `merge-multiple: true`; attach via `softprops/action-gh-release@v2` (SHA-pinned). `actions/attest-build-provenance` for the tarballs is deferrable hardening. +- Inline comment on the `on:` block: fork safety derives from `push: tags` (only repo writers push tags); `pull_request_target` is intentionally not used; the trusted-publisher record binds workflow filename + environment, not tag patterns (the `on:` filter is the tag gate — see U5). + +**Test scenarios:** (unit) none — pure CI config; the behavioral contract is the rehearsal and first release in the Verification Contract. +**Acceptance (non-unit):** the gate must fail fast when the binary is missing; a non-semver tag must fail before any publish; the root must never be published when any platform package is absent or when a `binarySha256` cross-check fails (ordering gate). + +**Verification:** rehearsal per Verification Contract; first real tag publishes and the `npm view`/fresh-install checks pass. The packed tarball — not the workspace build — is the surface consumers install, so the rehearsal verifies the `pnpm publish --dry-run`/`npm pack` output per package; provenance is only provable on the real publish. + +### U4. Launcher integration tests + +**Goal:** Prove resolver+spawn end-to-end against fixtures — and pin the platform-name convention shared with the release tooling. + +**Requirements:** R2, R3, R7. + +**Dependencies:** U1 (build output). + +**Files:** +- `npm/packages/comment-checker/src/platform.ts` — extract pure helpers `platform`/`arch`-derived names (`optionalDepName(platform, arch)`, `binaryFileName(platform)`) that `index.ts` uses; add a second tsdown entry so tests can import `dist/platform.mjs` without executing the CLI. The module must import no Effect client (`effect`, `@effect/*`) — the `@effect/language-service` floatingEffect rule is error in this tsconfig and the module must stay pure. +- `tests/npm-launcher/launcher.test.mjs` — `node:test` black-box suite driving `node dist/index.mjs`. + +**Approach:** +- Black-box: fixture dir `node_modules/@systemfsoftware/claude-code-comment-checker-/` with a fake `package.json` + a shim `comment-checker` script (echoes args, exits with a chosen code); run under `NODE_PATH=` so `createRequire` resolution finds it. +- Negative: no fixture → `BinaryNotFound` with the exact package-name suffix, os, arch; non-zero exit. + +**Test scenarios:** +- Happy path: `node dist/index.mjs --prompt hello` spawns the shim with `--prompt hello` and passes its exit code through (0 and non-zero). +- Missing fixture: stderr names the platform package (`…-linux-x64`-style), `process.platform` value, and exit non-zero. +- `optionalDepName('win32','x64')` equals the win32 entry of `targets.json`'s suffix; `binaryFileName('win32')` = `comment-checker.exe`; `binaryFileName('linux')` = `comment-checker`. +- The five table entries' suffixes round-trip through `optionalDepName`/`binaryFileName` (KTD2). + +**Verification:** `node --test tests/npm-launcher` after `pnpm -r build`. + +### U5. Docs, publish how-to, first-release checklist + +**Goal:** README claims match reality, publishing instructions and one-time org setup documented. + +**Requirements:** R8. + +**Files:** +- `README.md` — reword the "pre-release" status once live; add a "Publishing" section: tag flow, verification steps, and the trusted-publisher binding table: six packages (`@systemfsoftware/claude-code-comment-checker` + the five suffix names), each bound to this repo's `Organization`/`Repository`, `Workflow Filename` = `.github/workflows/release.yml`, and (recommended) `Environment` = `npm-release`. npm's trusted-publisher form has no tag-pattern field — `refs/tags/v*` is enforced by the workflow's `on: push: tags:` filter, never by the registry-side record; the registry-side guard is a fixed workflow filename (any other workflow in this repo is rejected). +- First-release checklist (docs): a brand-new package name may need a one-time seed publish before its trusted-publisher record can be configured (npm docs; confirm and budget a human seed per name if needed); create the six trusted-publisher entries with the exact bindings above; confirm GitHub repo default workflow permissions are read-only; recommended GitHub Environment `npm-release` with required reviewers on the publish jobs (deferrable — if skipped, the convention is exact semver tags only); record the action SHA→tag mapping as a dependabot `github-actions` group maintenance duty; confirm no PAT exists in any release job (default `GITHUB_TOKEN` only); record the manual macOS + Windows fresh-install runs. +- `AGENTS.md` — Locked surface: propose in the release PR a one-line directory-map addition naming the release workflow under `.github/workflows/`. Do not add rules or restate existing boundaries there — the Human Approval Boundaries contract already gates publishing. + +**Test scenarios:** none — docs + one-time admin setup; `Test expectation: none -- documentation; the release rehearsal + cross-platform install checks verify the claims.` + +**Verification:** README publish/install sections match the U3 rehearsal output; DoD cross-platform check recorded. + +--- + +## Verification Contract + +Run in this order: + +| # | Check | Command / outcome | +|---|---|---| +| 1 | Repo gate | `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test --all-targets` (Rust side untouched) | +| 2 | JS lint | `pnpm lint` — new `scripts/`, `tests/` files stay lint-clean (no ignorePatterns widening) | +| 3 | Scripts unit | `node --test tests/release` (includes malformed-tag rejection) | +| 4 | Launcher build | `pnpm -r build` → `dist/index.mjs` + `dist/platform.mjs` | +| 5 | Launcher tests | `node --test tests/npm-launcher` | +| 6 | Frozen install | `pnpm install --frozen-lockfile` (root lockfile committed in U1; hash stable under frozen install) | +| 7 | Rehearsal | `generate-platform-manifest` per target (dry-run), `check-matrix`, engines floor on Node 18, `pnpm publish --dry-run`/`npm pack` in a copy of each platform dir + root (registry pinned); record binary `binarySha256` values; Node-24 OIDC path noted as provable only on the real publish | +| 8 | First publish | `v0.1.0` tag → six packages at `0.1.0`; `npm view` per suffix shows `version` + `os`/`cpu`/`libc` equal to `targets.json`, root shows exact optional pins; per-platform `npm pack` re-fetch + `sha256sum` cross-check passes; provenance visible on npm | +| 9 | Fresh install | `pnpm dlx`/`npm i -g` the published package and run the binary on Linux (CI) and manually on macOS + Windows (before DoD) | +| 10 | `--ignore-scripts` | install with scripts disabled; npm logs must not report any executed lifecycle script, and the binary still runs | + +## Definition of Done + +- [ ] `npm/packages/comment-checker/package.json` + lockfile live; frozen install and build green (U1). +- [ ] Release scripts + `targets.json` + green `node:test` (U2). +- [ ] `release.yml` replaces the draft; pre-publish gate + smoke in place (U3). +- [ ] Launcher tests green; `platform.ts` extracted (U4). +- [ ] Docs and first-release checklist up (U5). +- [ ] `v0.1.0` live: six packages with provenance, exact `optionalDependencies` for the tag; fresh install runs the real binary on Linux and on ≥1 of macOS/Windows (second platform recorded manually). +- [ ] No static token anywhere in CI assets; `id-token` only; workflow-level `permissions: {}` with per-job grants; every third-party action pinned by full commit SHA (SHA→tag mapping in U5); default `GITHUB_TOKEN` is the only repo token; no PAT. +- [ ] Publish jobs run Node 24 (npm ≥ 11.5.1) with `registry-url: https://registry.npmjs.org` and no `NODE_AUTH_TOKEN`; workflow `concurrency` group present; tag-reachability check in the release gate. +- [ ] Version sync runs only through `sync-root-version.mjs` (no inline `node -e`); malformed-tag rejection is tested; the launcher reads its version at runtime (no hardcoded literal). +- [ ] Per-suffix registry verification (`npm view` of version/os/cpu/libc) passes for all five platform packages before the root publish. +- [ ] Root lockfile committed in U1 (generated against the canonical registry) and stable under frozen install in CI. +- [ ] Binary `binarySha256` recorded pre-publish and cross-checked against the published packages (U3). +- [ ] Cleanup: no scratch repos/temp staging dirs (gitignored `npm/bin` and temp dirs) left in the diff. + +## Risks & Dependencies + +- **musl / Alpine loader crash** — without the `libc` tag (KTD4), musl machines install the glibc binary and crash instead of a clean error. Gated by a U2 test on the linux entries. +- **Non-atomic publish race** — platform-first ordering + `needs: release` on the root publish prevents the root depending on packages that did not publish; a workflow-level `concurrency` group (per tag) prevents two tags racing; `binarySha256` cross-check catches a wrong-binary publish within one run. On a wrong-binary catch or duplicate-version E409, the recovery is `npm deprecate @` + ship the fix as the next tag (versions are immutable; no re-publish). +- **Wrong-binary published despite checks** — the `npm pack` + `sha256sum` audit runs on the same runner that just published (self-trusting); recorded `binarySha256` sidecars mitigate drift, and a separate clean-runner audit job is hardening (decided later). Document the deprecate-and-bump escape hatch in the U5 checklist. +- **Malformed tag / version injection** — a hostile or mistyped tag is rejected by the semver gate before any write or publish (U2/U3); the version writer is `sync-root-version.mjs`, never inline JSON concatenation. +- **Stray tag on an unreviewed commit** — tag-reachability check (commit must be an ancestor of the default branch) in the release gate; mitigates a writer tagging an unreviewed commit. +- **Actions supply chain** — every third-party action pinned by full commit SHA; workflow defaults `permissions: {}`; GitHub repo default is read-only; SHA→tag mapping maintained via a dependabot `github-actions` group. +- **npm OIDC/provenance quirks** — trusted publishing requires npm ≥ 11.5.1 (publish jobs run Node 24) and `registry-url` set without `NODE_AUTH_TOKEN`; pnpm 11.21.0 pinned; rehearsal exercises every runner job; Windows-runner OIDC + pnpm is the least-validated path and gets a dedicated rehearsal note; macOS 13 runner deprecation would need a darwin-x64 build fallback (cross-compile from macos-14 or a large runner) — re-check before the release. +- **Trusted-publisher setup friction** — six records must exist; a brand-new package name may need a one-time seed publish first (npm docs); bindings are repo + workflow filename + environment (no tag field); wrong or missing setup fails fast at first publish. +- **Version drift committed-vs-published** — by design; the tag is truth (KTD5); the launcher reads its version at runtime so `--version` never drifts; documented in README publishing notes. +- **Registry-side mutation of the pinned Effect rc** — the committed root lockfile hash (generated once in U1, never regenerated arbitrarily) plus `--frozen-lockfile` and an explicit `--registry` pin are the defense; the pin must be bumped together with a `repos/effect` subtree bump. + +## System-Wide Impact + +- **Supply chain:** OIDC-only credentials; the only token in the pipeline is the default `GITHUB_TOKEN` (no PAT, no per-job secrets); every published package carries provenance; workflow default permissions are none; GitHub repo default read-only. +- **CI:** release runs cost 7 runner-jobs (5 matrix + publish-npm-main + upload-gh-release-assets); rehearsal adds none. +- **Org boundary:** tab-cutting is the human gate (AGENTS.md Human Approval Boundaries). +- **Git-flow:** release from tags; note that `ci.yml` triggers on `main` while AGENTS.md names `master` — deferred. + +## Deferred to Follow-Up Work + +- musl/Alpine platform packages (the `libc` field is already wired). +- winarm64 and FreeBSD targets. +- Automated version management (changesets) — the tag is the single source today. +- CI e2e fresh-install tests on every runner post-publish (manual today). +- A separate clean-runner publish-audit job (registers `binarySha256` verification off the publishing runner) — hardening. +- Re-review of maintained Rust→npm release tooling (cargo-dist, napi-rs) at the first new platform or version-automation ticket; the hand-rolled pipeline stays unless that review changes the calculus. +- Deprecating the direct GH tarball path once the npm path is proven. +- Reconciling `ci.yml` `main` trigger with AGENTS.md `master` naming — resolve in the first release PR (or the release review notes the mismatch). + +## Sources & Research + +- `README.md` — distribution intent, install and FAQ contract. +- Prior un-merged work: `feat/npm-optional-dependencies` (draft workflow at `.github/workflows/release.yml`, launcher manifest at `npm/packages/comment-checker/package.json`, plan at `docs/plans/2026-08-12-001-feat-modern-npm-binary-distribution-pnpm-11-plan.md`); `feat/modern-npm-distribution-pnpm-11`. +- Web: Sentry "publishing binaries on npm"; napi-rs release docs (publish order, immutability, gates); mux CLI dist plan; npm trusted-publishing docs; pnpm 11.11–11.14 release notes (`libc`/`os`/`cpu` filters, `--no-optional` semantics); esbuild `optionalDependencies` precedent. +- In-repo conventions: `.cargo/config.toml` (TSLP static env), `Cargo.toml` release profile (opt-level 3, lto=fat, strip='symbols', codegen-units=1, panic=abort), `pnpm-workspace.yaml` packages glob, `.gitignore` (`npm/bin`, `dist` already reserved; docs under `docs/` root per CE conventions). \ No newline at end of file diff --git a/docs/publishing/first-publish-bootstrap.md b/docs/publishing/first-publish-bootstrap.md new file mode 100644 index 0000000..8a0887c --- /dev/null +++ b/docs/publishing/first-publish-bootstrap.md @@ -0,0 +1,122 @@ +# First npm Publish — Bootstrap (OIDC cannot precede existence) + +Run once, by a human with `systemfsoftware` npm org access, before the first +tag-triggered release. After this bootstrap, `release.yml` publishes everything +with OIDC provenance and no static tokens. + +## Why this one-time step exists + +npm's OIDC trusted publishing is configured **per package** on the package's +settings page, and `npm trust` has an explicit "package must exist" prerequisite: +. First-publish via OIDC is +still not supported upstream (open issue: +). So the six package names must be +claimed once with a token, then the trusted-publisher records are configured. + +Measured 2026-08-19: all six names are unclaimed (`npm view` returns E404): +`@systemfsoftware/claude-code-comment-checker` and the five platform packages +(`-linux-x64`, `-linux-arm64`, `-darwin-x64`, `-darwin-arm64`, `-win32-x64`). + +## Prerequisites + +- npm account, logged in, 2FA enabled, member of the `systemfsoftware` org + with publish rights on the `@systemfsoftware` scope. +- `npm -v` >= 11.15.0 (needed for `npm trust`; `npm i -g npm@latest` if older). +- This repo checked out; commands run from the repo root. + +## Publish six placeholders (dummy version) + +Every generated manifest carries `publishConfig.provenance: true` (npm honors +that setting; on a laptop there is no OIDC token), so every bootstrap publish +**must** pass `--no-provenance` or npm attempts OIDC and fails. + +```bash +cd /home/ryan/Documents/projects/comment-checker.worktrees/comment-checker-npm +DUMMY=0.0.0-dummy-npm # lowest semver; the real 0.1.0 becomes "latest" + +# 5 platform packages +for SUFFIX in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do + STAGE="/tmp/cc-bootstrap-$SUFFIX" + rm -rf "$STAGE" && mkdir -p "$STAGE" + deno run \ + --allow-read=scripts/release/targets.json,npm/packages/comment-checker/package.json \ + --allow-write="$STAGE" \ + scripts/release/generate-platform-manifest.ts \ + --suffix "$SUFFIX" --version "$DUMMY" --out "$STAGE" + touch "$STAGE/$(jq -r '.files[0]' "$STAGE/package.json")" # placeholder binary in tarball + # prerelease version (0.0.0-dummy-npm) requires an explicit --tag; "next" + # keeps the placeholder off the "latest" dist-tag + (cd "$STAGE" && npm publish --access public --no-provenance --tag next) +done + +# root launcher (staged copy; repo file untouched) +ROOT_STAGE=/tmp/cc-bootstrap-root +rm -rf "$ROOT_STAGE" && mkdir -p "$ROOT_STAGE/dist" +cp npm/packages/comment-checker/package.json "$ROOT_STAGE/package.json" +touch "$ROOT_STAGE/dist/index.mjs" +VERSION="$DUMMY" deno run --allow-env \ + --allow-read=scripts/release/targets.json,"$ROOT_STAGE/package.json" \ + --allow-write="$ROOT_STAGE/package.json" \ + scripts/release/sync-root-version.ts --manifest-path "$ROOT_STAGE/package.json" +(cd "$ROOT_STAGE" && npm publish --access public --no-provenance --tag next) +``` + +Two npm gotchas this accounts for: + +- A prerelease version (hyphen suffix, e.g. `0.0.0-dummy-npm`) is rejected + without an explicit `--tag` ("You must specify a tag using --tag when + publishing a prerelease version"). `--tag next` satisfies it and keeps the + placeholder off `latest`. +- npm strips a `bin` entry whose path starts with `./` (`"bin": {…, + "./dist/index.mjs"}` is silently removed at publish). The committed launcher + manifest must use a bare relative path (`dist/index.mjs`). + +## Configure one trusted publisher per package + +CLI (first call prompts 2FA; the "skip 2FA for 5 minutes" option covers the +rest; `--file` takes the workflow **filename only**, not a path per +): + +```bash +for PKG in \ + @systemfsoftware/claude-code-comment-checker \ + @systemfsoftware/claude-code-comment-checker-linux-x64 \ + @systemfsoftware/claude-code-comment-checker-linux-arm64 \ + @systemfsoftware/claude-code-comment-checker-darwin-x64 \ + @systemfsoftware/claude-code-comment-checker-darwin-arm64 \ + @systemfsoftware/claude-code-comment-checker-win32-x64; do + npm trust github "$PKG" --file release.yml --repo systemfsoftware/comment-checker --allow-publish -y + sleep 2 +done + +npm trust list @systemfsoftware/claude-code-comment-checker # sanity check +``` + +Web form (equivalent, per package): npmjs.com -> package -> Settings -> +Trusted publishing -> GitHub Actions -> org `systemfsoftware`, repo +`comment-checker`, workflow file `release.yml`, allowed action `npm publish`. + +## First real release (provenance on all six) + +```bash +git tag v0.1.0 && git push origin v0.1.0 +``` + +`release.yml` then builds and gates, publishes the five platform packages with +`--provenance`, cross-checks published sha256 against the recorded sidecars, +syncs the root version + optionalDependencies pins from the tag, publishes the +root, and verifies the pins. All auth via OIDC. + +## Cleanup and don'ts + +- After 0.1.0 lands, the placeholders can be deprecated: + + ```bash + for PKG in @systemfsoftware/claude-code-comment-checker{,-linux-x64,-linux-arm64,-darwin-x64,-darwin-arm64,-win32-x64}; do + npm deprecate "$PKG@$DUMMY" "placeholder used to bootstrap OIDC trusted publishing" + done + ``` + +- Do **not** `npm unpublish` a placeholder: deleting the only version deletes + the package and its trusted-publisher config, breaking OIDC. Deprecation + keeps name, config, and provenance trail intact. \ No newline at end of file diff --git a/docs/publishing/first-release-checklist.md b/docs/publishing/first-release-checklist.md new file mode 100644 index 0000000..5238979 --- /dev/null +++ b/docs/publishing/first-release-checklist.md @@ -0,0 +1,99 @@ +# First npm Release Checklist + +The release pipeline (`.github/workflows/release.yml`) stages everything; this +checklist is the one-time org-admin setup plus the manual verification steps +that only a human with `systemfsoftware` access can run (AGENTS.md Human +Approval Boundaries). Work through it top to bottom. + +## 1. One-time org setup (npm + GitHub) + +- [ ] Confirm the GitHub repo default workflow permissions are read-only + (Settings → Actions → General → Workflow permissions → Read repository + contents and packages permissions). The workflow declares its own + minimal per-job grants on top. +- [ ] Create the six npm trusted-publisher entries + (`npm access` / web form on the npm org): + `@systemfsoftware/claude-code-comment-checker` plus the five platform + packages (`-linux-x64`, `-linux-arm64`, `-darwin-x64`, `-darwin-arm64`, + `-win32-x64`). Every entry binds to: + - Organization / Repository: `systemfsoftware` / `comment-checker` + - Workflow Filename: `release.yml` (filename only; npm's + trusted-publisher form rejects full paths) + - Environment: `npm-release` (recommended; see note below) + npm's trusted-publisher form has **no tag-pattern field** — the + `refs/tags/v*` gate is enforced by the workflow's `on: push: tags` + filter, never by the registry-side record. +- [ ] Brand-new package names: npm's trusted-publisher record requires the + package to already exist (no first-publish via OIDC; see npm/cli#8544). + Run the one-time token bootstrap in + `docs/publishing/first-publish-bootstrap.md` (publishes a + `0.0.0-dummy-npm` placeholder per name, then configures the six records; + ~2 minutes). +- [ ] Recommended: create a GitHub Environment named `npm-release` and add + required reviewers to the publish jobs. Deferrable — if skipped, the + convention is exact semver tags only. If added, the environment must be + referenced in `publish-npm-main`'s `environment:` key in release.yml. +- [ ] Confirm **no PAT** exists in any release job: only the default + `GITHUB_TOKEN` (for uploading release assets) and `id-token: write` for + npm OIDC provenance. Pull requests and tags must not carry secrets. + +## 2. Before the tag + +- [ ] `pnpm lint` and `deno task lint` green (repo gate). +- [ ] `pnpm install --frozen-lockfile` succeeds from a fresh clone, and + `pnpm -r build` + `pnpm -r typecheck` are green. +- [ ] `scripts/release/check-matrix.ts` passes with `.github/workflows/release.yml` + present: `deno run --allow-read=scripts/release/targets.json,npm/packages/comment-checker/package.json,.github/workflows/release.yml scripts/release/check-matrix.ts` +- [ ] Cargo side (the release workflow runs its own build; there is no local + build requirement, but the Rust gate is `cargo fmt --check && cargo + clippy --all-targets -- -D warnings && cargo test --all-targets`). + +## 3. Tag and watch + +- [ ] Confirm the tag commit is an ancestor of the default branch (the + workflow enforces this; also true by construction for the merge). +- [ ] `git tag v0.1.0 && git push origin v0.1.0`. +- [ ] Watch the release run: five `release-*` matrix jobs (one per target), + `publish-npm-main`, `upload-gh-release-assets`. Each `release-*` job + gates on check-matrix, binary existence, smoke (exit 0 clean / exit 2 + flagged), records the binary sha256 sidecar, and publishes its platform + package with provenance. +- [ ] The run fails fast if any gate trips; the root is never published when a + platform package is missing or its published binary sha256 differs. + +## 4. Post-publish verification + +- [ ] `npm view @systemfsoftware/claude-code-comment-checker@v0.1.0 version` is + exactly `0.1.0`, and `optionalDependencies` pins all five platform + packages at `0.1.0` exactly (never a range). +- [ ] Per suffix: `npm view @systemfsoftware/claude-code-comment-checker-@0.1.0` + shows `version: 0.1.0` and `os`/`cpu`/`libc` equal to + `scripts/release/targets.json` (workflow already gates this; re-check by + hand here). +- [ ] Provenance visible: `npm view @systemfsoftware/claude-code-comment-checker@0.1.0 provenance` (npmjs.org shows the OIDC origin). +- [ ] Fresh install on Linux (CI simulates): `pnpm dlx @systemfsoftware/claude-code-comment-checker` or `npm i -g` and run the hook binary with a clean and a flagged payload. +- [ ] Manual fresh install on macOS and on Windows (record both runs' outputs). + +## 5. Sanctions and escape hatches + +- [ ] Wrong binary / duplicate version recovery: npm versions are immutable — + do **not** force-republish. Use `npm deprecate @` and ship + the fixed binary as the next tag. +- [ ] Amending an already-published version (rolling back) is not possible; + the tag-reachability gate prevents stray tags from publishing + unreviewed commits, but the final guard is the human before `git push` + of the tag. + +## 6. Maintenance duty (recorded, one-off) + +- [ ] The workflow pins every third-party action to a full commit SHA. Keep + the SHA→tag mapping fresh via a dependabot `github-actions` group so + pins are updated deliberately, never silently. + +## 7. After the first release + +- [ ] README: remove the "pre-release" status note under Install and keep the + npm install command as primary. +- [ ] Record actual download counts vs the cargo-install era as the adoption + signal (open item; not a release precondition). +- [ ] Optionally deprecate the direct GitHub tarball path once npm is proven. \ No newline at end of file diff --git a/docs/solutions/architecture-patterns/rust-cli-npm-distribution.md b/docs/solutions/architecture-patterns/rust-cli-npm-distribution.md new file mode 100644 index 0000000..8608b3c --- /dev/null +++ b/docs/solutions/architecture-patterns/rust-cli-npm-distribution.md @@ -0,0 +1,225 @@ +--- +title: Distributing a compiled Rust CLI as per-platform npm packages +date: 2026-08-17 +category: architecture-patterns +module: npm distribution (npm/packages/comment-checker + scripts/release + .github/workflows/release.yml) +problem_type: architecture_pattern +component: tooling +severity: medium +applies_when: + - Distributing a CLI compiled from Rust (or another compiled language) as npm packages to Linux, macOS, and Windows consumers + - The binary must arrive as a plain dependency with no postinstall build or download step + - Consumers or CI install with a frozen lockfile (pnpm) while the platform packages are unpublished until tag time + - The npm org supports trusted publishing so release credentials can be OIDC-only + - Native runners are available in CI for each platform/arch lane +tags: + - npm-distribution + - optional-dependencies + - platform-packages + - rust-cli + - oidc-provenance + - github-actions + - pnpm + - release-pipeline +--- + +# Distributing a compiled Rust CLI as per-platform npm packages + +## Context + +comment-checker is a Rust CLI shipped as a Claude Code hook; the npm +distribution must drop a working binary on every consumer's machine with +`npm i -g` / `npx` — no postinstall build, no download step. One package cannot +serve linux (x64 + arm64, glibc), darwin (x64 + arm64), and win32 x64 from a +single artifact, so the release surface is six packages: a root launcher plus +five per-platform binary packages. That shape creates two hard constraints: + +1. **pnpm cannot lock unresolvable optional deps (pnpm#3960).** The platform + packages do not exist in the registry until publish time, so a committed + launcher manifest that names them in `optionalDependencies` breaks + `pnpm install --frozen-lockfile` for every developer and CI run. The + committed manifest must stay clean; the pins are injected at publish time + (`scripts/release/sync-root-version.ts:18-22`). +2. **Six packages by hand is exactly what a human gets wrong.** The pipeline + must be tag-triggered (version = tag), run the same build → gate → smoke → + publish sequence on every tag, publish platforms before the root, and fail + loudly instead of shipping an absent or wrong-arch binary. + +## Guidance + +1. **Launcher resolves its platform package by identity at runtime.** + `npm/packages/comment-checker/src/platform.ts` defines two pure helpers: + `optionalDepName(platform, arch)` returns + `--`; `binaryFileName(platform)` returns + `comment-checker.exe` on win32, else `comment-checker`. The launcher + (`npm/packages/comment-checker/src/index.ts`) resolves the platform + package's own `package.json` via `createRequire` and joins the binary name + to its directory. Missing package surfaces as a + typed `BinaryNotFound` naming the package; a spawn-time ENOENT would be a + corrupt install npm would not have produced. +2. **One canonical targets table.** `scripts/release/targets.json` is the + single source of truth: five entries, each `{target, suffix, os, cpu, + libc?, bin}`. Everything else consumes the table instead of re-deriving + the platform set — the workflow resolves the per-lane binary name with + `jq` rather than duplicating the win32→`.exe` rule, + `generate-platform-manifest.ts` rejects unknown suffixes against the table, + and `check-matrix.ts` builds the agreement tests from it. +3. **Platform manifests are generated, cheap, and carry no `bin`.** + `generate-platform-manifest.ts` renders each platform `package.json`: name + = launcher name + `-`, `os`/`cpu`/`libc` from the table, + `files: [entry.bin]`, and **no `bin` field** — a platform-level bin would + create a top-level `comment-checker` shim colliding with the launcher's own + (esbuild precedent, comment at lines 60-62). `binarySha256` is recorded + into the manifest when the caller passes it. +4. **The committed launcher manifest carries NO `optionalDependencies`.** + pnpm cannot record unresolvable optional deps in a lockfile, so listing + unpublished platform packages breaks frozen installs. `sync-root-version.ts` + validates `VERSION` (strict semver regex, before any write), then injects + `version` plus the five pins from `targets.json`, preserving the manifest's + own formatting so an unchanged sync is byte-identical; `--dry-run` prints an + LCS diff. +5. **Gate the matrix, not the script.** `check-matrix.ts` names the product + platform set (`EXPECTED_SUFFIXES`, five entries — the known set, not a copy + of the table), then checks three agreements: the table names exactly that + set; the launcher manifest pins match the table exactly when present; and + the workflow matrix rows match the table triples in both directions — + missing, extra, and swapped `target`/`suffix` pairs are all failures. +6. **Release pipeline: one lane per platform, platforms before root.** + `.github/workflows/release.yml` triggers only on `push: tags: v*` with + `permissions: {}` at the top. Five matrix lanes, `fail-fast: false`, each: + build → `check-matrix` gate → binary-exists gate → in-lane smoke (exit 0 + for a clean payload, 2 for a flagged one) → stage the platform package + outside the workspace in `$RUNNER_TEMP` plus a binary sha256 sidecar → + `pnpm publish --provenance` (OIDC, no `NODE_AUTH_TOKEN`, npm ≥ 11.5.1) → + upload tarball + sha sidecar. The root job `publish-npm-main` needs all + lanes, re-derives `VERSION` from the tag, requires the tag commit to be an + ancestor of the default branch, verifies every published platform + package's `version`/`os`/`cpu`/`libc` against the table, cross-checks the + published tarballs' binary sha against the recorded sidecars, builds + frozen, runs `sync-root-version.ts` with `VERSION` from the environment, + publishes the root, and verifies the root's five pins are exact version + pins. A final job attaches the tarballs to the GitHub release. +7. **Humans own one-time trust setup only.** OIDC trusted publishing is the + no-token story: the npm trusted-publisher record binds workflow filename + + environment (the npm form has no tag-pattern field), so the `tags: v*` + filter is the tag gate and `pull_request_target` is deliberately unused. + `docs/publishing/first-release-checklist.md` covers the six trusted- + publisher records and post-publish manual spot checks. + +## Why This Matters + +- **The pnpm failure mode is a landmine, not an annoyance.** The moment + someone adds `optionalDependencies` naming the platform packages to the + committed manifest, every `pnpm install --frozen-lockfile` — developers and + CI alike — breaks because the packages don't exist yet (pnpm#3960). It is + caught by `check-matrix.ts`'s absence-is-expected branch and by the + `pnpm install --frozen-lockfile` step of `docs/publishing/first-release-checklist.md`. +- **Version skew is structurally impossible at the consumer.** The root pins + each platform package to the exact tag version (verified against the + registry at release time). Because the root is published after the + platforms, a consumer's install either gets the pinned, gated binary or + fails to resolve — no in-between state. +- **The silent gate failure modes were observed, so the gates are shaped + against them.** (a) `jq` libc shape: `npm view` reports `libc` as an array + (`["glibc"]`) while the table stores a bare string, and darwin/win32 rows + have no `libc` at all — a naive compare is always-true or always-false, so + the workflow normalizes both sides before deep equality. (b) Cross-arch + smoke: the smoke only proves anything on the lane's own native runner; + each matrix row maps target→runner (arm64 lanes use an ARM runner). (c) + `--allow-env`: `VERSION` arrives via the environment, and `deno run` is + deny-by-default, so a dropped flag fails at tag time, not at PR time. +- **No static token exists anywhere.** Publishing is OIDC-only. + +## When to Apply + +- **Apply:** any compiled CLI (Rust, Go, C) distributed as an npm `bin` to + heterogeneous consumers — especially when you want `npm i -g` / `npx` to + just work, you have native CI runners per platform, and the npm org supports + trusted publishing. +- **Avoid when:** single-platform or single-arch tooling (one package with + `files`, no matrix); N-API addons (in-process bindings via `process.dlopen` + are a different architecture — no launcher spawn, no platform shim); a + binary that must be compiled on the consumer machine (postinstall builds + are their own failure mode). +- **Trust prerequisites:** OIDC trusted publishing is a hard dependency of + the no-token story, and the npm org needs one trusted-publisher record per + package name; brand-new names may require a seed publish before the record + can be configured (`docs/publishing/first-release-checklist.md`). + +## Examples + +`npm/packages/comment-checker/src/platform.ts` — the platform surface is two +pure helpers: + +```ts +export const binaryFileName = (platform: string): string => + platform === "win32" ? "comment-checker.exe" : "comment-checker" + +export const optionalDepName = (platform: string, arch: string): string => + `@systemfsoftware/claude-code-comment-checker-${platform}-${arch}` +``` + +`scripts/release/targets.json` — the table is the platform contract; every +entry carries `os`/`cpu`/`libc` consumed by manifest generation, the +workflow's binary-name resolution, and the registry gate: + +```json +{ + "target": "x86_64-unknown-linux-gnu", + "suffix": "linux-x64", + "os": "linux", + "cpu": "x64", + "libc": "glibc", + "bin": "comment-checker" +} +``` + +`scripts/release/sync-root-version.ts` — the inject-at-publish move that +keeps the committed manifest frozen-install-clean while the published root is +fully pinned: + +```ts +manifest.optionalDependencies = Object.fromEntries( + targets.map((entry) => [`${manifest.name}-${entry.suffix}`, version]), +) +``` + +`scripts/release/check-matrix.ts` — the product policy the table must name: + +```ts +const EXPECTED_SUFFIXES = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64'] +``` + +`.github/workflows/release.yml` — version comes only from the tag, and the +tag commit must be an ancestor of the default branch before anything +publishes: + +```yaml +- name: "Tag gate: derive VERSION from tag" + run: | + VERSION="${GITHUB_REF#refs/tags/v}" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "invalid tag semver: '$VERSION'" >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" +``` + +The binary-sha cross-check records a sha256 sidecar per lane at build time, +then re-packs the published tarball from the registry and recomputes the +digest — the gate that catches a wrong or swapped binary being published. + +## Related + +- Pipeline: `.github/workflows/release.yml` +- Platform table: `scripts/release/targets.json` +- Scripts: `scripts/release/generate-platform-manifest.ts`, + `scripts/release/sync-root-version.ts`, `scripts/release/check-matrix.ts` +- Human gate: `docs/publishing/first-release-checklist.md` +- pnpm#3960 — the constraint that makes listing optional deps a + frozen-lockfile landmine +- Residual advisories (open GitHub issues on this repo): #3 force-pushed tag + gate; #4 platform peerDependencies cross-link; #5 concurrency group vs + force-moved tags; #6 smoke exit-code contract; #7 sha sidecar self-trust; + #8 check-matrix regex-scrape fragility; #9 no actionlint / workflow YAML + validation in CI \ No newline at end of file diff --git a/npm/packages/comment-checker/oxlint.config.ts b/npm/packages/comment-checker/oxlint.config.ts new file mode 100644 index 0000000..cd9cf55 --- /dev/null +++ b/npm/packages/comment-checker/oxlint.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "oxlint" + +// `plugins` REPLACES oxlint's default plugin set rather than merging into it, +// so every plugin that should contribute rules must be listed explicitly. +// Omitting `oxc` (or `unicorn`) silently drops their whole rule families while +// `correctness` still looks enabled. +export default defineConfig({ + plugins: ["typescript", "unicorn", "oxc"], + categories: { + correctness: "error", + suspicious: "error", + perf: "warn", + }, + ignorePatterns: ["node_modules/**", "dist/**", "dist-types/**", "repos/**"], +}) \ No newline at end of file diff --git a/npm/packages/comment-checker/package.json b/npm/packages/comment-checker/package.json new file mode 100644 index 0000000..8a1d7ab --- /dev/null +++ b/npm/packages/comment-checker/package.json @@ -0,0 +1,37 @@ +{ + "name": "@systemfsoftware/claude-code-comment-checker", + "version": "0.1.0", + "description": "Claude Code PostToolUse hook that blocks unnecessary comments (npm distribution launcher)", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/systemfsoftware/comment-checker.git" + }, + "type": "module", + "bin": { + "comment-checker": "dist/index.mjs" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsdown", + "typecheck": "tsc -b", + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + }, + "devDependencies": { + "@effect/platform-node": "4.0.0-rc.108", + "@types/node": "latest", + "effect": "4.0.0-rc.108", + "oxlint": "latest", + "tsdown": "latest", + "typescript": "latest" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} \ No newline at end of file diff --git a/npm/packages/comment-checker/src/index.ts b/npm/packages/comment-checker/src/index.ts new file mode 100755 index 0000000..cdafe3a --- /dev/null +++ b/npm/packages/comment-checker/src/index.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env node +import { createRequire } from "node:module" +import { Data, Effect, Option, Path, Runtime } from "effect" +import { NodeRuntime, NodeServices } from "@effect/platform-node" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { Command, Flag } from "effect/unstable/cli" +import { binaryFileName, optionalDepName } from "./platform.js" + +const require = createRequire(import.meta.url) + +class BinaryNotFound extends Data.TaggedError("BinaryNotFound")<{ + readonly platform: string + readonly arch: string + readonly package: string + readonly message: string +}> { } + +class ChildProcessExited extends Data.TaggedError("ChildProcessExited")<{ + readonly exitCode: ChildProcessSpawner.ExitCode +}> { + override get [Runtime.errorExitCode](): ChildProcessSpawner.ExitCode { + return this.exitCode + } + override readonly [Runtime.errorReported] = false +} + +const getBinaryPath = Effect.gen(function* () { + const path = yield* Path.Path + + const platform = process.platform + const arch = process.arch + const pkg = optionalDepName(platform, arch) + + const pkgJsonPath = yield* Effect.try({ + try: () => require.resolve(`${pkg}/package.json`), + catch: () => + new BinaryNotFound({ + platform, + arch, + package: pkg, + message: `the npm platform package for ${platform}/${arch} (${pkg}) is not installed`, + }), + }) + + return path.join(path.dirname(pkgJsonPath), binaryFileName(platform)) +}) + +const command = Command.make( + "comment-checker", + { + prompt: Flag.optional(Flag.string("prompt")), + }, + (config) => + Effect.gen(function* () { + const binaryPath = yield* getBinaryPath + const args = Option.match(config.prompt, { + onNone: () => [], + onSome: (prompt) => ["--prompt", prompt], + }) + + const child = ChildProcess.make(binaryPath, args, { + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }) + + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const code = yield* spawner.exitCode(child) + + if (code !== ChildProcessSpawner.ExitCode(0)) { + return yield* new ChildProcessExited({ exitCode: code }) + } + }) +) + +const { version } = require("../package.json") as { version: string } + +NodeRuntime.runMain( + Command.run(command, { version }).pipe(Effect.provide(NodeServices.layer)) +) diff --git a/npm/packages/comment-checker/src/platform.ts b/npm/packages/comment-checker/src/platform.ts new file mode 100644 index 0000000..81c7979 --- /dev/null +++ b/npm/packages/comment-checker/src/platform.ts @@ -0,0 +1,5 @@ +export const binaryFileName = (platform: string): string => + platform === "win32" ? "comment-checker.exe" : "comment-checker" + +export const optionalDepName = (platform: string, arch: string): string => + `@systemfsoftware/claude-code-comment-checker-${platform}-${arch}` \ No newline at end of file diff --git a/npm/packages/comment-checker/tsconfig.json b/npm/packages/comment-checker/tsconfig.json new file mode 100644 index 0000000..62d6181 --- /dev/null +++ b/npm/packages/comment-checker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": [ + "@systemfsoftware/tsconfig/bundler/no-dom" + ], + "compilerOptions": { + "customConditions": [ + "@systemfsoftware/source" + ] + }, + "include": [ + "src" + ], +} diff --git a/npm/packages/comment-checker/tsconfig.node.json b/npm/packages/comment-checker/tsconfig.node.json new file mode 100644 index 0000000..f3d9f06 --- /dev/null +++ b/npm/packages/comment-checker/tsconfig.node.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/node", + "include": [ + "tsdown.config.ts" + ] +} diff --git a/npm/packages/comment-checker/tsdown.config.ts b/npm/packages/comment-checker/tsdown.config.ts new file mode 100644 index 0000000..6bf311d --- /dev/null +++ b/npm/packages/comment-checker/tsdown.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsdown" + +export default defineConfig({ + tsconfig: "./tsconfig.json", + entry: ["src/index.ts", "src/platform.ts"], + format: ["esm"], + clean: true, + sourcemap: true, + outDir: "dist", +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..3cbaa13 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "claude-code-comment-checker", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@11.21.0", + "scripts": { + "build": "turbo build", + "typecheck": "turbo typecheck", + "lint": "turbo lint" + }, + "devDependencies": { + "@effect/tsgo": "latest", + "@systemfsoftware/tsconfig": "^1.3.1", + "tsdown": "latest", + "turbo": "^2.10.5", + "typescript": "latest" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..03cfb52 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1530 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@effect/tsgo': + specifier: latest + version: 0.36.5 + '@systemfsoftware/tsconfig': + specifier: ^1.3.1 + version: 1.3.1 + tsdown: + specifier: latest + version: 0.22.14(typescript@7.0.2) + turbo: + specifier: ^2.10.5 + version: 2.10.10 + typescript: + specifier: latest + version: 7.0.2 + + npm/packages/comment-checker: + devDependencies: + '@effect/platform-node': + specifier: 4.0.0-rc.108 + version: 4.0.0-rc.108(effect@4.0.0-rc.108)(ioredis@5.11.1) + '@types/node': + specifier: latest + version: 26.2.0 + effect: + specifier: 4.0.0-rc.108 + version: 4.0.0-rc.108 + oxlint: + specifier: latest + version: 1.78.0 + tsdown: + specifier: latest + version: 0.22.14(typescript@7.0.2) + typescript: + specifier: latest + version: 7.0.2 + +packages: + + '@effect/platform-node-shared@4.0.0-rc.109': + resolution: {integrity: sha512-jhZJnf81N7Z5+Z41Qr0cKT75WzGu6M1W05xrSdu/V8PK0kSFtU5hJMBxSGDWNlgWhBwK0WRZtnMHY8HLdONg1w==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-rc.109 + + '@effect/platform-node@4.0.0-rc.108': + resolution: {integrity: sha512-Nof78154BaHGdSYr4TPQFZ5+Dg+HkpmbI3SQUdwsby5QNs6yahGJPu2AgdIVqdx7pKZ2w7j/bvdnqoMmkG0PbA==} + engines: {node: '>=18.0.0'} + peerDependencies: + effect: ^4.0.0-rc.108 + ioredis: '>=5.7.0 <6.0.0' + + '@effect/tsgo-darwin-arm64@0.36.5': + resolution: {integrity: sha512-+JPS65Ekod5NS41Kg9OIyuUsygNcSw5/4Y+UNbY6Wob6dvP+CkEa51pGBAmHKQp3Z/D3g/A9GNE66ndu9kz8dw==} + cpu: [arm64] + os: [darwin] + + '@effect/tsgo-darwin-x64@0.36.5': + resolution: {integrity: sha512-S67mS1GTSvfeN5Tsij7QarJNuv3q7U/PMhjTbGtKO9mqgDTOA2IOlxq7YPIxkWq+AiOIv0H3aHGk7ToRjcPWxg==} + cpu: [x64] + os: [darwin] + + '@effect/tsgo-linux-arm64@0.36.5': + resolution: {integrity: sha512-bNLzLrQ/4Sf0N7NlAqcEpr+g+RW/ERIvmSFQN9Nu+hSFti4Kx9Nrfo1LJ4V0qASry5Rj2DG4Wa9C5ydQAD9qqA==} + cpu: [arm64] + os: [linux] + + '@effect/tsgo-linux-arm@0.36.5': + resolution: {integrity: sha512-UqtTPUgoVMRHAOcHiK7sddxjUEj6kOkXAlu4Y2TL/MhGiu81ZBzZrg8TXf0ApNeEMOD0EIK8hwU2kik8O+buxA==} + cpu: [arm] + os: [linux] + + '@effect/tsgo-linux-x64@0.36.5': + resolution: {integrity: sha512-QWdyuUcAb1kZBeItycHg1ZKloNbVDtlVm1C0bbDVHqZIZ/d2rqhKi6Zajm64GnHtfdwPTn8Oi1iOCH1IYgo0IQ==} + cpu: [x64] + os: [linux] + + '@effect/tsgo-win32-arm64@0.36.5': + resolution: {integrity: sha512-5tO5em1DfplFz5GqpQVGRSrAoE+kEgIc7Y0ClbT9jkxaK18IFVq9KIYybt4d6VT4+QnNQGdqWOahMcGE4JAZ8w==} + cpu: [arm64] + os: [win32] + + '@effect/tsgo-win32-x64@0.36.5': + resolution: {integrity: sha512-pmaKwdYAIs9GFpMV9glIUks0UZDHE1/xwP6+GCGhSFxB76cDLMnrHr/IBt5VfGddT65SK+Dyw/vAtcX7fRp4KA==} + cpu: [x64] + os: [win32] + + '@effect/tsgo@0.36.5': + resolution: {integrity: sha512-BHxVjeRK1/XlqYHWXkbT4W9JpOrT3sNA2wrfwQPBTojD5OSyxI2TUIltobYhWudyfuCrn770qP6uOpDjdrmghA==} + hasBin: true + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} + + '@oxlint/binding-android-arm-eabi@1.78.0': + resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.78.0': + resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.78.0': + resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.78.0': + resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.78.0': + resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.78.0': + resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.78.0': + resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.78.0': + resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.78.0': + resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.78.0': + resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.78.0': + resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.78.0': + resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.78.0': + resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.78.0': + resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.78.0': + resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.78.0': + resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.78.0': + resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.78.0': + resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@systemfsoftware/tsconfig@1.3.1': + resolution: {integrity: sha512-aOxvcGH/vhjo3cll8llWZpbd9r7y4GJ/FVhx+gWe1pc5usKEQODxc7zo0g1PUbsTQry79OBXe/uR5EumOcXbiw==} + + '@turbo/darwin-64@2.10.10': + resolution: {integrity: sha512-gFDD+wRP5hWxBRghGyEbjpbLOY7aIU/wvsnKdMM7odQcp/wHMrnI83p0FyxxMRZnFH9ZD+S59MvcpOC5b+nrCA==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.10.10': + resolution: {integrity: sha512-VZYsxZ6yjyDosUqtiroAVSXPLmx/qBxdHJgIxdMH9RyNmLdOLOWtJnYMnI4qckwCgQMK85G3fu94/xk5+iBCgw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.10.10': + resolution: {integrity: sha512-lAvW+yEnmsCKMEIwNugjozawvYytHKPhU0kfLBizu83MIs8OUb9KobYvkZ56L5akSM6K7+gBFLEIfQkaceh90g==} + cpu: [x64] + os: [android, linux] + + '@turbo/linux-arm64@2.10.10': + resolution: {integrity: sha512-MSJ+NkRTd79Z9+YEZpUV9VOWVOOigFhE+v/ETNYJEuTJp3r00y9YgFvDXrmM+DP8Kal6tk3U6xSugD2/Ojh+Jg==} + cpu: [arm64] + os: [android, linux] + + '@turbo/windows-64@2.10.10': + resolution: {integrity: sha512-ycWpXDkUfnDFDY9d+4Qna/UZotDB0wj+s9agrlmNt0Q7a3XHORhK8GPKJdzgzeutXu9EW5P/jyabTEHlohuDXw==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.10.10': + resolution: {integrity: sha512-PMk6zQN0csUFklLe+1hz/5G9uU1YmV0cEIey2R/bSeA6o69qcBTlN4A3jOqkgenOO5dOpMHmq2sEUZo8r1+Ssg==} + cpu: [arm64] + os: [win32] + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@yuku-codegen/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + effect@4.0.0-rc.108: + resolution: {integrity: sha512-KmI3DlKZWPvCL4QQ2FMaPOuxMt/7DrKMENCY/gQ+MkDR5QYw25wgU5Zmh/wVLboNjIci1gNOgNCFe4xqgxli3A==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + + mime@4.1.0: + resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} + engines: {node: '>=16'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + oxlint@1.78.0: + resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.27.14: + resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.22.14: + resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.14 + '@tsdown/exe': 0.22.14 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + + turbo@2.10.10: + resolution: {integrity: sha512-/90KTW+USzvYOPmafRZHVKLBsHXQ5810Ao/HdtJYAqguIhZ+XruS6eIUjqJUDtrSxaZYynNFht68qckGKAOWTA==} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} + + yuku-codegen@0.8.7: + resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==} + + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + +snapshots: + + '@effect/platform-node-shared@4.0.0-rc.109(effect@4.0.0-rc.108)': + dependencies: + '@types/ws': 8.18.1 + effect: 4.0.0-rc.108 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/platform-node@4.0.0-rc.108(effect@4.0.0-rc.108)(ioredis@5.11.1)': + dependencies: + '@effect/platform-node-shared': 4.0.0-rc.109(effect@4.0.0-rc.108) + effect: 4.0.0-rc.108 + ioredis: 5.11.1 + mime: 4.1.0 + undici: 8.10.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@effect/tsgo-darwin-arm64@0.36.5': + optional: true + + '@effect/tsgo-darwin-x64@0.36.5': + optional: true + + '@effect/tsgo-linux-arm64@0.36.5': + optional: true + + '@effect/tsgo-linux-arm@0.36.5': + optional: true + + '@effect/tsgo-linux-x64@0.36.5': + optional: true + + '@effect/tsgo-win32-arm64@0.36.5': + optional: true + + '@effect/tsgo-win32-x64@0.36.5': + optional: true + + '@effect/tsgo@0.36.5': + optionalDependencies: + '@effect/tsgo-darwin-arm64': 0.36.5 + '@effect/tsgo-darwin-x64': 0.36.5 + '@effect/tsgo-linux-arm': 0.36.5 + '@effect/tsgo-linux-arm64': 0.36.5 + '@effect/tsgo-linux-x64': 0.36.5 + '@effect/tsgo-win32-arm64': 0.36.5 + '@effect/tsgo-win32-x64': 0.36.5 + + '@ioredis/commands@1.10.0': {} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + + '@oxc-project/types@0.144.0': {} + + '@oxlint/binding-android-arm-eabi@1.78.0': + optional: true + + '@oxlint/binding-android-arm64@1.78.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.78.0': + optional: true + + '@oxlint/binding-darwin-x64@1.78.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.78.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.78.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.78.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.78.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.78.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.78.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.78.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.78.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.78.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.78.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.78.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.78.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.78.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.78.0': + optional: true + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.2.4': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.4': + optional: true + + '@rolldown/binding-darwin-x64@1.2.4': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.4': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.4': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.4': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.4': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.4': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.4': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.4': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@systemfsoftware/tsconfig@1.3.1': {} + + '@turbo/darwin-64@2.10.10': + optional: true + + '@turbo/darwin-arm64@2.10.10': + optional: true + + '@turbo/linux-64@2.10.10': + optional: true + + '@turbo/linux-arm64@2.10.10': + optional: true + + '@turbo/windows-64@2.10.10': + optional: true + + '@turbo/windows-arm64@2.10.10': + optional: true + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.2.0 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@yuku-codegen/binding-android-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-codegen/binding-win32-x64@0.8.7': + optional: true + + '@yuku-parser/binding-android-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-darwin-x64@0.8.7': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-win32-x64@0.8.7': + optional: true + + '@yuku-toolchain/types@0.8.7': {} + + ansis@4.3.1: {} + + cac@7.0.0: {} + + cluster-key-slot@1.1.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + defu@6.1.7: {} + + denque@2.1.0: {} + + detect-libc@2.1.2: + optional: true + + dts-resolver@3.0.0: {} + + effect@4.0.0-rc.108: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.9.0 + kubernetes-types: 1.30.0 + msgpackr: 2.0.5 + uuid: 14.0.1 + + empathic@2.0.1: {} + + fast-check@4.9.0: + dependencies: + pure-rand: 8.4.2 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + hookable@6.1.1: {} + + import-without-cache@0.4.0: {} + + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + kubernetes-types@1.30.0: {} + + mime@4.1.0: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 + optional: true + + msgpackr@2.0.5: + optionalDependencies: + msgpackr-extract: 3.0.4 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + obug@2.1.4: {} + + oxlint@1.78.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.78.0 + '@oxlint/binding-android-arm64': 1.78.0 + '@oxlint/binding-darwin-arm64': 1.78.0 + '@oxlint/binding-darwin-x64': 1.78.0 + '@oxlint/binding-freebsd-x64': 1.78.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 + '@oxlint/binding-linux-arm-musleabihf': 1.78.0 + '@oxlint/binding-linux-arm64-gnu': 1.78.0 + '@oxlint/binding-linux-arm64-musl': 1.78.0 + '@oxlint/binding-linux-ppc64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-gnu': 1.78.0 + '@oxlint/binding-linux-riscv64-musl': 1.78.0 + '@oxlint/binding-linux-s390x-gnu': 1.78.0 + '@oxlint/binding-linux-x64-gnu': 1.78.0 + '@oxlint/binding-linux-x64-musl': 1.78.0 + '@oxlint/binding-openharmony-arm64': 1.78.0 + '@oxlint/binding-win32-arm64-msvc': 1.78.0 + '@oxlint/binding-win32-ia32-msvc': 1.78.0 + '@oxlint/binding-win32-x64-msvc': 1.78.0 + + picomatch@4.0.5: {} + + pure-rand@8.4.2: {} + + quansync@1.0.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.27.14(rolldown@1.2.4)(typescript@7.0.2): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.4 + rolldown: 1.2.4 + yuku-ast: 0.8.7 + yuku-codegen: 0.8.7 + yuku-parser: 0.8.7 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.2.4: + dependencies: + '@oxc-project/types': 0.144.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 + + standard-as-callback@2.1.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tree-kill@1.2.2: {} + + tsdown@0.22.14(typescript@7.0.2): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.5 + rolldown: 1.2.4 + rolldown-plugin-dts: 0.27.14(rolldown@1.2.4)(typescript@7.0.2) + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.3.2 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - oxc-resolver + - vue-tsc + + turbo@2.10.10: + optionalDependencies: + '@turbo/darwin-64': 2.10.10 + '@turbo/darwin-arm64': 2.10.10 + '@turbo/linux-64': 2.10.10 + '@turbo/linux-arm64': 2.10.10 + '@turbo/windows-64': 2.10.10 + '@turbo/windows-arm64': 2.10.10 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@8.3.0: {} + + undici@8.10.0: {} + + uuid@14.0.1: {} + + verkit@0.3.2: {} + + ws@8.21.3: {} + + yuku-ast@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + + yuku-codegen@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-x64': 0.8.7 + '@yuku-codegen/binding-freebsd-x64': 0.8.7 + '@yuku-codegen/binding-linux-arm-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm-musl': 0.8.7 + '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm64-musl': 0.8.7 + '@yuku-codegen/binding-linux-x64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-x64-musl': 0.8.7 + '@yuku-codegen/binding-win32-arm64': 0.8.7 + '@yuku-codegen/binding-win32-x64': 0.8.7 + + yuku-parser@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..ef687f3 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - 'npm/packages/*' +allowBuilds: + msgpackr-extract: false diff --git a/scripts/deno.jsonc b/scripts/deno.jsonc new file mode 100644 index 0000000..8a38f73 --- /dev/null +++ b/scripts/deno.jsonc @@ -0,0 +1,20 @@ +{ + "imports": { + "@std/assert": "jsr:@std/assert@1.0.19", + "@std/cli": "jsr:@std/cli@1.0.15", + "@std/path": "jsr:@std/path@1.1.6", + "@libs/diff": "jsr:@libs/diff@4.0.0" + }, + "tasks": { + "manifest:generate": "deno run --allow-read=release/targets.json,../npm/packages/comment-checker/package.json release/generate-platform-manifest.ts", + "manifest:sync-root": "deno run --allow-env --allow-read=release/targets.json,../npm/packages/comment-checker/package.json --allow-write=../npm/packages/comment-checker/package.json release/sync-root-version.ts", + "check-matrix": "deno run --allow-read=release/targets.json,../npm/packages/comment-checker/package.json,../.github/workflows/release.yml release/check-matrix.ts", + "lint": "deno lint --config ./deno.jsonc ." + }, + "fmt": { + "lineWidth": 100, + "indentWidth": 2, + "singleQuote": true, + "semiColons": false + } +} diff --git a/scripts/deno.lock b/scripts/deno.lock new file mode 100644 index 0000000..0ea2b72 --- /dev/null +++ b/scripts/deno.lock @@ -0,0 +1,42 @@ +{ + "version": "5", + "specifiers": { + "jsr:@libs/diff@4.0.0": "4.0.0", + "jsr:@std/assert@1.0.19": "1.0.19", + "jsr:@std/cli@1.0.15": "1.0.15", + "jsr:@std/internal@^1.0.12": "1.0.14", + "jsr:@std/internal@^1.0.14": "1.0.14", + "jsr:@std/path@1.1.6": "1.1.6" + }, + "jsr": { + "@libs/diff@4.0.0": { + "integrity": "f4ebf2acd54b3266093c335d4a3f89ae0cb65e92c4baf1b93dbaeff8491708fd" + }, + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal@^1.0.12" + ] + }, + "@std/cli@1.0.15": { + "integrity": "e79ba3272ec710ca44d8342a7688e6288b0b88802703f3264184b52893d5e93f" + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + }, + "@std/path@1.1.6": { + "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", + "dependencies": [ + "jsr:@std/internal@^1.0.14" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@libs/diff@4.0.0", + "jsr:@std/assert@1.0.19", + "jsr:@std/cli@1.0.15", + "jsr:@std/path@1.1.6" + ] + } +} diff --git a/scripts/release/check-matrix.ts b/scripts/release/check-matrix.ts new file mode 100755 index 0000000..60cb59a --- /dev/null +++ b/scripts/release/check-matrix.ts @@ -0,0 +1,171 @@ +#!/usr/bin/env -S deno run --allow-read +import { resolve } from '@std/path' +import { parseCliArgs } from './cli.ts' +import { + LAUNCHER_MANIFEST_PATH, + type LauncherManifest, + RELEASE_WORKFLOW_PATH, + type Target, + TARGETS_PATH, +} from './shared.ts' + +// The product platform set: a known list the table must name, not a copy +// derived from the table under check. +const EXPECTED_SUFFIXES = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64'] + +const failures: string[] = [] +const fail = (reason: string) => failures.push(reason) +const note = (message: string) => console.error(`check-matrix: note: ${message}`) + +const flags = parseCliArgs({ + alias: { 'manifest-path': 'manifestPath', 'workflow-path': 'workflowPath' }, + string: ['targets', 'manifest-path', 'workflow-path'], +}) +const targetsPath = typeof flags.targets === 'string' ? resolve(flags.targets) : TARGETS_PATH +const manifestPath = typeof flags.manifestPath === 'string' + ? resolve(flags.manifestPath) + : LAUNCHER_MANIFEST_PATH +const workflowPath = typeof flags.workflowPath === 'string' + ? resolve(flags.workflowPath) + : RELEASE_WORKFLOW_PATH + +async function readJsonOrExit(path: string, label: string): Promise { + try { + return JSON.parse(await Deno.readTextFile(path)) + } catch (error) { + console.error( + `check-matrix: FAIL: cannot read ${label} ${path}: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + Deno.exit(1) + } +} + +function checkTable(targets: Target[]) { + const suffixes = targets.map((t) => t.suffix) + const missing = EXPECTED_SUFFIXES.filter((s) => !suffixes.includes(s)) + const extra = suffixes.filter((s) => !(EXPECTED_SUFFIXES as string[]).includes(s)) + if (missing.length > 0 || extra.length > 0) { + fail( + `targets table must name exactly the supported platform set; missing: ${ + missing.join(', ') || 'none' + }, extra: ${extra.join(', ') || 'none'}`, + ) + } + for (const entry of targets) { + if (entry.suffix !== `${entry.os}-${entry.cpu}`) { + fail( + `target ${entry.target}: suffix "${entry.suffix}" must equal os-cpu "${entry.os}-${entry.cpu}"`, + ) + } + if ((entry.os === 'win32') !== (entry.bin === 'comment-checker.exe')) { + fail( + `target ${entry.target}: bin must be comment-checker.exe iff os is win32 (os: ${entry.os}, bin: ${entry.bin})`, + ) + } + if (entry.os === 'linux' && entry.libc !== 'glibc') { + fail( + `target ${entry.target}: linux targets must carry libc "glibc", got ${ + JSON.stringify(entry.libc) + }`, + ) + } + if (entry.os !== 'linux' && entry.libc !== undefined) { + fail( + `target ${entry.target}: non-linux targets must not carry libc, got ${ + JSON.stringify(entry.libc) + }`, + ) + } + } +} + +function checkManifest(manifest: LauncherManifest, targets: Target[]) { + const expectedNames = targets.map((t) => `${manifest.name}-${t.suffix}`) + const declaredNames = Object.keys(manifest.optionalDependencies ?? {}) + if (declaredNames.length === 0) { + note( + 'launcher manifest carries no optionalDependencies (pre-publish); sync-root-version.ts injects the five platform pins from targets.json', + ) + } else { + const missingNames = expectedNames.filter((name) => !declaredNames.includes(name)) + const extraNames = declaredNames.filter((name) => !expectedNames.includes(name)) + if (missingNames.length > 0 || extraNames.length > 0) { + fail( + `launcher manifest optionalDependencies must be exactly the platform packages from the table; missing: ${ + missingNames.join(', ') || 'none' + }, extra: ${extraNames.join(', ') || 'none'}`, + ) + } + for (const name of expectedNames) { + const pin = manifest.optionalDependencies?.[name] + if (pin !== manifest.version) { + fail( + `optionalDependency ${name} must be pinned to the root version ${manifest.version}, got ${ + JSON.stringify(pin) + }`, + ) + } + } + } +} + +async function checkWorkflow(workflowPath: string, targets: Target[]) { + try { + await Deno.lstat(workflowPath) + const content = await Deno.readTextFile(workflowPath) + // Each matrix row lists its suffix on the line after target. + const workflowPairs = new Map( + [...content.matchAll( + /^\s*-\s*target:\s*((?:x86_64|aarch64)-[a-z0-9-]+)\s*\n\s*suffix:\s*([a-z0-9-]+)\s*$/gm, + )].map((m) => [m[1], m[2]]), + ) + const tablePairs = new Map(targets.map((t) => [t.target, t.suffix])) + for (const [target, suffix] of tablePairs) { + if (!workflowPairs.has(target)) { + fail(`release.yml does not list release target ${target}`) + } else if (workflowPairs.get(target) !== suffix) { + fail( + `release.yml lists ${target} with suffix ${ + workflowPairs.get(target) + }, table says ${suffix}`, + ) + } + } + for (const [target, suffix] of workflowPairs) { + if (!tablePairs.has(target)) { + fail(`release.yml lists ${target}, which is not a row in targets.json`) + } else if (suffix !== tablePairs.get(target)) { + fail( + `release.yml lists ${target} with suffix ${suffix}, table says ${tablePairs.get(target)}`, + ) + } + } + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + note(`skipped: ${workflowPath} not found (workflow agreement not checked)`) + } else { + fail(`cannot check workflow ${workflowPath}: ${String(error)}`) + } + } +} + +const rawTargets = await readJsonOrExit(targetsPath, 'targets file') +if (!Array.isArray(rawTargets)) { + fail('targets table is not an array') +} else { + checkTable(rawTargets as Target[]) +} +const rawManifest = await readJsonOrExit(manifestPath, 'launcher manifest') +checkManifest(rawManifest as LauncherManifest, rawTargets as Target[]) +await checkWorkflow(workflowPath, rawTargets as Target[]) + +if (failures.length > 0) { + for (const reason of failures) { + console.error(`check-matrix: FAIL: ${reason}`) + } + Deno.exit(1) +} + +console.error('check-matrix: ok') diff --git a/scripts/release/cli.ts b/scripts/release/cli.ts new file mode 100644 index 0000000..b452158 --- /dev/null +++ b/scripts/release/cli.ts @@ -0,0 +1,30 @@ +import { parseArgs, type ParseOptions } from '@std/cli/parse-args' + +const die = (message: string): never => { + console.error(message) + Deno.exit(1) +} + +// @std/cli parses a string flag given without a value as "". +export function parseCliArgs( + options: ParseOptions, +): ReturnType { + const flags = parseArgs(Deno.args, { + ...options, + unknown: (arg) => die(`unknown argument: ${arg}`), + }) + if (flags._.length > 0) { + die(`unknown argument: ${flags._[0]}`) + } + for (const name of stringFlags(options.string)) { + if (flags[name] === '') { + die(`missing value for --${name}`) + } + } + return flags +} + +function stringFlags(names: string | readonly string[] | undefined): string[] { + if (names === undefined) return [] + return typeof names === 'string' ? [names] : [...names] +} diff --git a/scripts/release/generate-platform-manifest.ts b/scripts/release/generate-platform-manifest.ts new file mode 100755 index 0000000..6df7318 --- /dev/null +++ b/scripts/release/generate-platform-manifest.ts @@ -0,0 +1,68 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write +import { join } from '@std/path' +import { parseCliArgs } from './cli.ts' +import { + LAUNCHER_MANIFEST_PATH, + type LauncherManifest, + type Target, + TARGETS_PATH, +} from './shared.ts' + +const TARGETS: Target[] = JSON.parse(await Deno.readTextFile(TARGETS_PATH)) +const LAUNCHER: LauncherManifest = JSON.parse(await Deno.readTextFile(LAUNCHER_MANIFEST_PATH)) + +const args = parseCliArgs({ + alias: { 'binary-sha256': 'binarySha256', 'dry-run': 'dryRun' }, + boolean: ['dry-run'], + string: ['suffix', 'version', 'out', 'binary-sha256'], +}) + +for (const flag of ['suffix', 'version', 'out'] as const) { + if (typeof args[flag] !== 'string') { + console.error(`generate-platform-manifest: missing required --${flag}`) + Deno.exit(1) + } +} +const suffix = args.suffix as string +const version = args.version as string +const out = args.out as string +const binarySha256 = args.binarySha256 + +const entry = TARGETS.find((t) => t.suffix === suffix) +if (!entry) { + console.error( + `generate-platform-manifest: unknown suffix "${suffix}"; supported suffixes: ${ + TARGETS.map((t) => t.suffix).join(', ') + }`, + ) + Deno.exit(1) +} + +const pkg: Record = { + name: `${LAUNCHER.name}-${entry.suffix}`, + version, + description: `${LAUNCHER.name} ${entry.suffix} platform package`, + license: 'Apache-2.0', + repository: LAUNCHER.repository, + os: [entry.os], + cpu: [entry.cpu], + files: [entry.bin], + // No bin field — a platform package's bin would collide with the launcher's + // own comment-checker shim. + publishConfig: { access: 'public', provenance: true }, +} +if (entry.libc !== undefined) { + pkg.libc = [entry.libc] +} +if (binarySha256 !== undefined) { + pkg.binarySha256 = binarySha256 as string +} + +const output = JSON.stringify(pkg, null, 2) + '\n' + +if (args.dryRun) { + await Deno.stdout.write(new TextEncoder().encode(output)) +} else { + await Deno.mkdir(out, { recursive: true }) + await Deno.writeTextFile(join(out, 'package.json'), output) +} diff --git a/scripts/release/shared.ts b/scripts/release/shared.ts new file mode 100644 index 0000000..8548904 --- /dev/null +++ b/scripts/release/shared.ts @@ -0,0 +1,29 @@ +import { join } from '@std/path' + +const ROOT = join(import.meta.dirname!, '..', '..') + +export const TARGETS_PATH = join(ROOT, 'scripts', 'release', 'targets.json') +export const LAUNCHER_MANIFEST_PATH = join( + ROOT, + 'npm', + 'packages', + 'comment-checker', + 'package.json', +) +export const RELEASE_WORKFLOW_PATH = join(ROOT, '.github', 'workflows', 'release.yml') + +export interface Target { + target: string + suffix: string + os: string + cpu: string + libc?: string + bin: string +} + +export interface LauncherManifest { + name: string + version: string + repository: { type: string; url: string } + optionalDependencies?: Record +} diff --git a/scripts/release/sync-root-version.ts b/scripts/release/sync-root-version.ts new file mode 100755 index 0000000..3e8b51b --- /dev/null +++ b/scripts/release/sync-root-version.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write +import { resolve } from '@std/path' +import { diff } from '@libs/diff' +import { parseCliArgs } from './cli.ts' +import { + LAUNCHER_MANIFEST_PATH, + type LauncherManifest, + type Target, + TARGETS_PATH, +} from './shared.ts' + +const VERSION_RE = /^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$/ + +// Validated before any write. `$` matches before a trailing newline, so reject +// one explicitly. +const version = Deno.env.get('VERSION') ?? '' +if (!VERSION_RE.test(version) || version.includes('\n')) { + console.error(`sync-root-version: invalid VERSION: ${JSON.stringify(version)}`) + Deno.exit(1) +} + +const flags = parseCliArgs({ + alias: { 'dry-run': 'dryRun', 'manifest-path': 'manifestPath' }, + boolean: ['dry-run'], + string: ['manifest-path'], +}) +const dryRun = flags.dryRun === true +const manifestPath = typeof flags.manifestPath === 'string' + ? resolve(flags.manifestPath) + : LAUNCHER_MANIFEST_PATH + +const targets: Target[] = JSON.parse(await Deno.readTextFile(TARGETS_PATH)) +if (!Array.isArray(targets) || targets.length !== 5) { + console.error('sync-root-version: targets.json must declare exactly five platform targets') + Deno.exit(1) +} + +const original = await Deno.readTextFile(manifestPath) +const manifest: LauncherManifest = JSON.parse(original) +manifest.version = version +// The committed manifest carries no optionalDependencies — pnpm cannot lock +// unpublished platform packages — so inject the pins at publish time, when they exist. +manifest.optionalDependencies = Object.fromEntries( + targets.map((entry) => [`${manifest.name}-${entry.suffix}`, version]), +) + +// An unchanged sync must stay byte-identical: keep the file's indent and trailing newline. +const next = JSON.stringify(manifest, null, 2) + (original.endsWith('\n') ? '\n' : '') + +if (dryRun) { + // @libs/diff (patience algorithm) produces a real unified patch. + console.log(diff(original, next)) +} else { + await Deno.writeTextFile(manifestPath, next) +} diff --git a/scripts/release/targets.json b/scripts/release/targets.json new file mode 100644 index 0000000..68b3dc5 --- /dev/null +++ b/scripts/release/targets.json @@ -0,0 +1,39 @@ +[ + { + "target": "x86_64-unknown-linux-gnu", + "suffix": "linux-x64", + "os": "linux", + "cpu": "x64", + "libc": "glibc", + "bin": "comment-checker" + }, + { + "target": "aarch64-unknown-linux-gnu", + "suffix": "linux-arm64", + "os": "linux", + "cpu": "arm64", + "libc": "glibc", + "bin": "comment-checker" + }, + { + "target": "x86_64-apple-darwin", + "suffix": "darwin-x64", + "os": "darwin", + "cpu": "x64", + "bin": "comment-checker" + }, + { + "target": "aarch64-apple-darwin", + "suffix": "darwin-arm64", + "os": "darwin", + "cpu": "arm64", + "bin": "comment-checker" + }, + { + "target": "x86_64-pc-windows-msvc", + "suffix": "win32-x64", + "os": "win32", + "cpu": "x64", + "bin": "comment-checker.exe" + } +] diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..5af97e3 --- /dev/null +++ b/turbo.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://v2-10-1.turborepo.dev/schema.json", + "tasks": { + "build": { + "outputLogs": "new-only", + "inputs": [ + "src/**", + "tsconfig.json", + "tsconfig.*.json", + "package.json", + "tsdown.config.*" + ], + "outputs": [ + "dist/**" + ], + "dependsOn": [ + "^build" + ], + "cache": true + }, + "typecheck": { + "outputLogs": "errors-only", + "inputs": [ + "$TURBO_DEFAULT$", + "tsconfig.json", + "tsconfig.*.json", + "!**/*.md" + ], + "outputs": [], + "dependsOn": [ + "^build" + ], + "cache": true + }, + "lint": { + "inputs": [ + "$TURBO_DEFAULT$", + "oxlint.config.ts", + "!**/*.md" + ], + "outputs": [], + "env": [ + "OXLINT_FORMAT", + "AGENT", + "GITHUB_ACTIONS" + ], + "cache": true + } + } +} \ No newline at end of file