diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml new file mode 100644 index 0000000..9308c3b --- /dev/null +++ b/.github/workflows/provenance.yml @@ -0,0 +1,50 @@ +name: 📦 Publish + +# Dependencies: +# - SocketDev/socket-registry/.github/workflows/provenance.yml +# +# socket-addon is a publish-only repo. The .node binaries are downloaded +# from socket-btm's GitHub Releases by scripts/publish.mts at publish +# time, verified against embedded SHA-256 checksums, then republished +# under the @socketaddon/* scope. +# +# No setup-script is needed — packages are static, the publish script +# pulls binaries on demand. + +on: + workflow_dispatch: + inputs: + dist-tag: + description: 'npm dist-tag (latest, next, beta, canary, backport, etc.)' + required: false + default: 'latest' + type: string + debug: + description: 'Enable debug output' + required: false + default: '0' + type: choice + options: + - '0' + - '1' + publish-without-sfw: + description: 'Publish directly to npm, bypassing Socket firewall shims' + required: false + default: false + type: boolean + +permissions: + contents: write + id-token: write + +jobs: + publish: + uses: SocketDev/socket-registry/.github/workflows/provenance.yml@b74ae5083d662df0045731bcf35b4e54b1e03d37 # main + with: + debug: ${{ inputs.debug }} + dist-tag: ${{ inputs.dist-tag }} + publish-script: 'publish:ci' + publish-without-sfw: ${{ inputs.publish-without-sfw }} + use-trusted-publishing: true + secrets: + SOCKET_API_KEY: ${{ secrets.SOCKET_API_KEY }} diff --git a/docs/trusted-publisher-migration.md b/docs/trusted-publisher-migration.md new file mode 100644 index 0000000..d6f3d70 --- /dev/null +++ b/docs/trusted-publisher-migration.md @@ -0,0 +1,109 @@ +# npm Trusted-Publisher Migration + +This is a one-time set of npm UI + GitHub steps to move trusted-publisher +ownership of the `@socketaddon/iocraft*` packages from `SocketDev/socket-cli` +to `SocketDev/socket-addon`. After this is done, `socket-cli`'s OIDC +token can no longer publish iocraft packages — only this repo can. + +## Prerequisites + +- npm 2FA enabled on the org (already done) +- Admin role on the `@socketaddon` npm org +- Admin role on `SocketDev/socket-addon` GitHub repo +- All 9 placeholders already published at `0.0.0`: + - `@socketaddon/iocraft` + - `@socketaddon/iocraft-darwin-arm64` + - `@socketaddon/iocraft-darwin-x64` + - `@socketaddon/iocraft-linux-arm64` + - `@socketaddon/iocraft-linux-arm64-musl` + - `@socketaddon/iocraft-linux-x64` + - `@socketaddon/iocraft-linux-x64-musl` + - `@socketaddon/iocraft-win32-arm64` + - `@socketaddon/iocraft-win32-x64` + +> Note: the legacy names `@socketaddon/iocraft-win-arm64` and +> `@socketaddon/iocraft-win-x64` are deprecated and not part of this +> migration. + +## Step 1 — Add socket-addon as a trusted publisher (per package) + +For each of the 9 packages above: + +1. https://www.npmjs.com/package//access (signed in as an admin) +2. Scroll to **Trusted Publishers** +3. Click **Add trusted publisher** +4. Fill in: + - **Publisher**: GitHub Actions + - **Repository owner**: `SocketDev` + - **Repository name**: `socket-addon` + - **Workflow filename**: `provenance.yml` + - **Environment** *(optional, leave blank — we don't gate publishing on a GH environment)* +5. Save. + +This authorizes `SocketDev/socket-addon`'s `provenance.yml` workflow to +publish that package via OIDC. **Do not remove the existing socket-cli +trusted publisher yet** — it remains the active publisher until step 3. + +## Step 2 — Run a dry publish from socket-addon + +Locally: + +```sh +cd ~/projects/socket-addon +pnpm install +pnpm run publish:dry +``` + +This runs `scripts/publish.mts --dry-run` — it stages each per-platform +package, downloads the `.node` from socket-btm, verifies the SHA-256, +and runs `pnpm publish --dry-run`. Nothing hits the registry. + +Verify the output shows: + +- `Using iocraft release: iocraft-20260424-18f0f46` *(or current tag)* +- For each per-platform package: + - `downloading iocraft-...-.node from socket-btm@` + - `verified iocraft-...-.node ( bytes, sha-256 ok)` + - `--- 9 packages will be published` + +If any checksum fails, the run aborts before any publish. That's the +fail-loudly intent. + +## Step 3 — Run the workflow once (real publish) + +Once the dry run is clean: + +1. Bump the placeholder versions from `0.0.0` to e.g. `1.0.0-pre.0` in + each of the 9 `package.json` files (all per-platform packages must + advance in lockstep with the umbrella's `optionalDependencies` pins). +2. Commit + push. +3. Trigger the workflow: + - https://github.com/SocketDev/socket-addon/actions/workflows/provenance.yml + - **Run workflow** → branch `main` → `dist-tag = pre` +4. Watch the run. Each per-platform package publishes via OIDC, then + the umbrella publishes last. + +## Step 4 — Remove socket-cli as a trusted publisher (per package) + +After the socket-addon publish succeeds and you've verified the new +versions are installable (`npm install @socketaddon/iocraft@pre`), come +back to each package's `/access` page and remove the +`SocketDev/socket-cli` trusted-publisher entry. + +This is the irreversible step: socket-cli can no longer publish these +packages after it's removed. + +## Step 5 — Update socket-cli to consume @socketaddon/iocraft + +See the companion task in socket-cli — strip +`scripts/download-iocraft-binaries.mts`, the `iocraft` block in +`provenance.yml`, and add `@socketaddon/iocraft: ` as a +dependency in `packages/cli/package.json`. + +## Rollback + +If something goes wrong between steps 1 and 4, the original socket-cli +trusted publisher is still active — re-trigger socket-cli's +provenance workflow with `iocraft = true` to publish a hotfix. After +step 4 the rollback path requires re-adding socket-cli as a trusted +publisher, which is reversible but takes a couple minutes per package. diff --git a/package.json b/package.json index dfb3187..2570ff8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "lint": "oxlint", "format": "oxfmt --write .", "format:check": "oxfmt --check .", - "test": "vitest run" + "test": "vitest run", + "publish": "node scripts/publish.mts", + "publish:ci": "node scripts/publish.mts --tag ${DIST_TAG:-latest}", + "publish:dry": "node scripts/publish.mts --dry-run" }, "devDependencies": { "@socketsecurity/lib": "catalog:", diff --git a/packages/iocraft-darwin-arm64/LICENSE b/packages/iocraft-darwin-arm64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-darwin-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-darwin-arm64/README.md b/packages/iocraft-darwin-arm64/README.md new file mode 100644 index 0000000..e724d0e --- /dev/null +++ b/packages/iocraft-darwin-arm64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-darwin-arm64 + +iocraft native bindings for **macOS Apple Silicon (arm64)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-darwin-arm64/package.json b/packages/iocraft-darwin-arm64/package.json new file mode 100644 index 0000000..c7bcdf4 --- /dev/null +++ b/packages/iocraft-darwin-arm64/package.json @@ -0,0 +1,44 @@ +{ + "name": "@socketaddon/iocraft-darwin-arm64", + "version": "0.0.0", + "description": "iocraft native bindings for macOS Apple Silicon (arm64).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["darwin"], + "cpu": ["arm64"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "darwin", + "arm64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-darwin-x64/LICENSE b/packages/iocraft-darwin-x64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-darwin-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-darwin-x64/README.md b/packages/iocraft-darwin-x64/README.md new file mode 100644 index 0000000..50565e5 --- /dev/null +++ b/packages/iocraft-darwin-x64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-darwin-x64 + +iocraft native bindings for **macOS Intel (x64)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-darwin-x64/package.json b/packages/iocraft-darwin-x64/package.json new file mode 100644 index 0000000..fbd578b --- /dev/null +++ b/packages/iocraft-darwin-x64/package.json @@ -0,0 +1,44 @@ +{ + "name": "@socketaddon/iocraft-darwin-x64", + "version": "0.0.0", + "description": "iocraft native bindings for macOS Intel (x64).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["darwin"], + "cpu": ["x64"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "darwin", + "x64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-linux-arm64-musl/LICENSE b/packages/iocraft-linux-arm64-musl/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-linux-arm64-musl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-linux-arm64-musl/README.md b/packages/iocraft-linux-arm64-musl/README.md new file mode 100644 index 0000000..daf6990 --- /dev/null +++ b/packages/iocraft-linux-arm64-musl/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-linux-arm64-musl + +iocraft native bindings for **Linux arm64 (musl)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-linux-arm64-musl/package.json b/packages/iocraft-linux-arm64-musl/package.json new file mode 100644 index 0000000..f4760d6 --- /dev/null +++ b/packages/iocraft-linux-arm64-musl/package.json @@ -0,0 +1,46 @@ +{ + "name": "@socketaddon/iocraft-linux-arm64-musl", + "version": "0.0.0", + "description": "iocraft native bindings for Linux arm64 (musl).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["musl"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "linux", + "arm64", + "musl" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-linux-arm64/LICENSE b/packages/iocraft-linux-arm64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-linux-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-linux-arm64/README.md b/packages/iocraft-linux-arm64/README.md new file mode 100644 index 0000000..bc364f5 --- /dev/null +++ b/packages/iocraft-linux-arm64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-linux-arm64 + +iocraft native bindings for **Linux arm64 (glibc)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-linux-arm64/package.json b/packages/iocraft-linux-arm64/package.json new file mode 100644 index 0000000..0d5133d --- /dev/null +++ b/packages/iocraft-linux-arm64/package.json @@ -0,0 +1,45 @@ +{ + "name": "@socketaddon/iocraft-linux-arm64", + "version": "0.0.0", + "description": "iocraft native bindings for Linux arm64 (glibc).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["linux"], + "cpu": ["arm64"], + "libc": ["glibc"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "linux", + "arm64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-linux-x64-musl/LICENSE b/packages/iocraft-linux-x64-musl/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-linux-x64-musl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-linux-x64-musl/README.md b/packages/iocraft-linux-x64-musl/README.md new file mode 100644 index 0000000..4dc6a13 --- /dev/null +++ b/packages/iocraft-linux-x64-musl/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-linux-x64-musl + +iocraft native bindings for **Linux x64 (musl)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-linux-x64-musl/package.json b/packages/iocraft-linux-x64-musl/package.json new file mode 100644 index 0000000..a53a3b0 --- /dev/null +++ b/packages/iocraft-linux-x64-musl/package.json @@ -0,0 +1,46 @@ +{ + "name": "@socketaddon/iocraft-linux-x64-musl", + "version": "0.0.0", + "description": "iocraft native bindings for Linux x64 (musl).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["linux"], + "cpu": ["x64"], + "libc": ["musl"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "linux", + "x64", + "musl" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-linux-x64/LICENSE b/packages/iocraft-linux-x64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-linux-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-linux-x64/README.md b/packages/iocraft-linux-x64/README.md new file mode 100644 index 0000000..1efe239 --- /dev/null +++ b/packages/iocraft-linux-x64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-linux-x64 + +iocraft native bindings for **Linux x64 (glibc)**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-linux-x64/package.json b/packages/iocraft-linux-x64/package.json new file mode 100644 index 0000000..cdd96e6 --- /dev/null +++ b/packages/iocraft-linux-x64/package.json @@ -0,0 +1,45 @@ +{ + "name": "@socketaddon/iocraft-linux-x64", + "version": "0.0.0", + "description": "iocraft native bindings for Linux x64 (glibc).", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["linux"], + "cpu": ["x64"], + "libc": ["glibc"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "linux", + "x64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-win32-arm64/LICENSE b/packages/iocraft-win32-arm64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-win32-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-win32-arm64/README.md b/packages/iocraft-win32-arm64/README.md new file mode 100644 index 0000000..467e2a7 --- /dev/null +++ b/packages/iocraft-win32-arm64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-win32-arm64 + +iocraft native bindings for **Windows arm64**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-win32-arm64/package.json b/packages/iocraft-win32-arm64/package.json new file mode 100644 index 0000000..865dc84 --- /dev/null +++ b/packages/iocraft-win32-arm64/package.json @@ -0,0 +1,44 @@ +{ + "name": "@socketaddon/iocraft-win32-arm64", + "version": "0.0.0", + "description": "iocraft native bindings for Windows arm64.", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["win32"], + "cpu": ["arm64"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "win32", + "arm64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft-win32-x64/LICENSE b/packages/iocraft-win32-x64/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft-win32-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft-win32-x64/README.md b/packages/iocraft-win32-x64/README.md new file mode 100644 index 0000000..bcf146a --- /dev/null +++ b/packages/iocraft-win32-x64/README.md @@ -0,0 +1,16 @@ +# @socketaddon/iocraft-win32-x64 + +iocraft native bindings for **Windows x64**. + +This is a per-platform package. You almost certainly want +[`@socketaddon/iocraft`](https://www.npmjs.com/package/@socketaddon/iocraft) +instead, which loads the matching per-platform binary automatically. + +```sh +npm install @socketaddon/iocraft +``` + +The `.node` binary in this package is built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished from a signed GitHub Release +with SHA-256 verification. diff --git a/packages/iocraft-win32-x64/package.json b/packages/iocraft-win32-x64/package.json new file mode 100644 index 0000000..a400ccc --- /dev/null +++ b/packages/iocraft-win32-x64/package.json @@ -0,0 +1,44 @@ +{ + "name": "@socketaddon/iocraft-win32-x64", + "version": "0.0.0", + "description": "iocraft native bindings for Windows x64.", + "license": "MIT", + "main": "iocraft.node", + "files": [ + "LICENSE", + "README.md", + "iocraft.node" + ], + "os": ["win32"], + "cpu": ["x64"], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "win32", + "x64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/iocraft/LICENSE b/packages/iocraft/LICENSE new file mode 100644 index 0000000..58d3a30 --- /dev/null +++ b/packages/iocraft/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/iocraft/README.md b/packages/iocraft/README.md new file mode 100644 index 0000000..c425740 --- /dev/null +++ b/packages/iocraft/README.md @@ -0,0 +1,40 @@ +# @socketaddon/iocraft + +Node.js bindings for [iocraft](https://github.com/ciscoheat/iocraft), a +Rust-based TUI library that provides fast, beautiful terminal interfaces +with React-like declarative syntax. + +## Installation + +```sh +npm install @socketaddon/iocraft +``` + +The package automatically pulls in the correct native binary for your +platform via `optionalDependencies`. Supported platforms: + +- macOS: `darwin-arm64`, `darwin-x64` +- Linux: `linux-arm64`, `linux-x64` (glibc and musl) +- Windows: `win32-arm64`, `win32-x64` + +If installation reports it can't find a matching native binary, your +platform is not yet supported. + +## Usage + +```js +import iocraft from '@socketaddon/iocraft' + +const tree = iocraft.view([ + iocraft.text('Hello from iocraft'), +]) + +console.log(iocraft.renderToString(tree)) +``` + +## Source + +Native binaries are built in +[`SocketDev/socket-btm`](https://github.com/SocketDev/socket-btm) under +`packages/iocraft-builder/` and republished here from signed GitHub +Releases with SHA-256 verification. diff --git a/packages/iocraft/index.d.ts b/packages/iocraft/index.d.ts new file mode 100644 index 0000000..9d1b0ce --- /dev/null +++ b/packages/iocraft/index.d.ts @@ -0,0 +1,102 @@ +/** + * TypeScript definitions for @socketaddon/iocraft + * + * Node.js bindings for iocraft TUI library. + */ + +export interface ComponentNode { + children?: ComponentNode[] + type: 'View' | 'Text' + content?: string + + border_style?: string + border_color?: string + + background_color?: string + + color?: string + weight?: string + align?: string + wrap?: string + underline?: boolean + italic?: boolean + bold?: boolean + dim_color?: boolean + strikethrough?: boolean + + flex_direction?: string + justify_content?: string + align_items?: string + flex_grow?: number + flex_shrink?: number + flex_basis?: number | string + flex_wrap?: string + overflow_x?: string + overflow_y?: string + + width?: number + height?: number + width_percent?: number + height_percent?: number + + padding?: number + padding_top?: number + padding_right?: number + padding_bottom?: number + padding_left?: number + padding_x?: number + padding_y?: number + + margin?: number + margin_top?: number + margin_right?: number + margin_bottom?: number + margin_left?: number + margin_x?: number + margin_y?: number + + gap?: number + row_gap?: number + column_gap?: number +} + +export function text(content: string): ComponentNode + +export function view(children: ComponentNode[]): ComponentNode + +export function renderToString(tree: ComponentNode): string + +export function renderToStringWithWidth(tree: ComponentNode, maxWidth: number): string + +export function printComponent(tree: ComponentNode): void + +export function eprintComponent(tree: ComponentNode): void + +export function getTerminalSize(): [number, number] + +export class TuiRenderer { + constructor() + setTree(tree: ComponentNode): Promise + isRunning(): boolean + getSize(): [number, number] + renderOnce(): Promise + renderWithWidth(maxWidth: number): Promise + print(): Promise + eprint(): Promise +} + +export function init(): void + +declare const iocraft: { + text: typeof text + view: typeof view + renderToString: typeof renderToString + renderToStringWithWidth: typeof renderToStringWithWidth + printComponent: typeof printComponent + eprintComponent: typeof eprintComponent + getTerminalSize: typeof getTerminalSize + TuiRenderer: typeof TuiRenderer + init: typeof init +} + +export default iocraft diff --git a/packages/iocraft/index.mjs b/packages/iocraft/index.mjs new file mode 100644 index 0000000..effad7b --- /dev/null +++ b/packages/iocraft/index.mjs @@ -0,0 +1,76 @@ +/** + * @socketaddon/iocraft — Node.js bindings for iocraft TUI library. + * + * Loads the per-platform .node binary from the matching + * @socketaddon/iocraft--[-] optional dependency. + */ + +import { createRequire } from 'node:module' +import { arch, platform } from 'node:os' + +const require = createRequire(import.meta.url) + +function getPlatformIdentifier() { + const platformName = platform() + const archName = arch() + + if (platformName !== 'darwin' && platformName !== 'linux' && platformName !== 'win32') { + throw new Error( + `Unsupported platform: ${platformName} ${archName}\n` + + `iocraft native bindings are only available for:\n` + + ` - macOS (darwin): arm64, x64\n` + + ` - Linux (linux): arm64, x64 (glibc and musl)\n` + + ` - Windows (win32): arm64, x64`, + ) + } + + if (archName !== 'arm64' && archName !== 'x64') { + throw new Error( + `Unsupported architecture: ${platformName} ${archName}\n` + + `iocraft native bindings are only available for arm64 and x64.`, + ) + } + + let libcSuffix = '' + if (platformName === 'linux') { + try { + const { spawnSync } = require('node:child_process') + const lddResult = spawnSync('ldd', ['--version'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }) + const output = lddResult.stdout || '' + if (output.includes('musl')) { + libcSuffix = '-musl' + } + } catch { + // ldd missing — assume glibc. + } + } + + return `${platformName}-${archName}${libcSuffix}` +} + +function loadNativeAddon() { + const platformId = getPlatformIdentifier() + const packageName = `@socketaddon/iocraft-${platformId}` + + try { + return require(packageName) + } catch (e) { + if (e?.code === 'MODULE_NOT_FOUND') { + throw new Error( + `Failed to load iocraft native addon for ${platformId}.\n` + + `The package ${packageName} is not installed.\n` + + `This usually means your platform is not supported, or the optionalDependencies were not installed correctly.\n\n` + + `Try reinstalling with: npm install --force @socketaddon/iocraft`, + { cause: e }, + ) + } + throw e + } +} + +const iocraft = loadNativeAddon() + +export default iocraft diff --git a/packages/iocraft/package.json b/packages/iocraft/package.json new file mode 100644 index 0000000..dfc1f9f --- /dev/null +++ b/packages/iocraft/package.json @@ -0,0 +1,60 @@ +{ + "name": "@socketaddon/iocraft", + "version": "0.0.0", + "description": "Node.js bindings for iocraft — a Rust-based TUI library", + "license": "MIT", + "type": "module", + "main": "./index.mjs", + "types": "./index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.mjs" + } + }, + "files": [ + "LICENSE", + "README.md", + "index.d.ts", + "index.mjs" + ], + "optionalDependencies": { + "@socketaddon/iocraft-darwin-arm64": "0.0.0", + "@socketaddon/iocraft-darwin-x64": "0.0.0", + "@socketaddon/iocraft-linux-arm64": "0.0.0", + "@socketaddon/iocraft-linux-arm64-musl": "0.0.0", + "@socketaddon/iocraft-linux-x64": "0.0.0", + "@socketaddon/iocraft-linux-x64-musl": "0.0.0", + "@socketaddon/iocraft-win32-arm64": "0.0.0", + "@socketaddon/iocraft-win32-x64": "0.0.0" + }, + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-addon.git" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "homepage": "https://github.com/SocketDev/socket-addon", + "bugs": { + "url": "https://github.com/SocketDev/socket-addon/issues" + }, + "keywords": [ + "socket", + "iocraft", + "tui", + "terminal", + "native", + "bindings", + "rust" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/scripts/lib/error-utils.mts b/scripts/lib/error-utils.mts new file mode 100644 index 0000000..3ded0a7 --- /dev/null +++ b/scripts/lib/error-utils.mts @@ -0,0 +1,18 @@ +/** + * Helpers for extracting displayable text from unknown caught values. + * + * Thin wrappers around `@socketsecurity/lib/errors` that preserve this + * package's "always return a non-empty string" contract for `errorStack`. + */ + +import { + errorMessage as libErrorMessage, + errorStack as libErrorStack, +} from '@socketsecurity/lib/errors' + +export const errorMessage = libErrorMessage + +export function errorStack(error: unknown): string { + const stack = libErrorStack(error) + return stack ? stack : libErrorMessage(error) +} diff --git a/scripts/publish.mts b/scripts/publish.mts new file mode 100644 index 0000000..d986821 --- /dev/null +++ b/scripts/publish.mts @@ -0,0 +1,484 @@ +/** + * Publish all `@socketaddon/*` packages to npm. + * + * Per-platform packages embed a single `iocraft.node` binary. The binary + * is NOT in the working tree — it's downloaded from a signed + * socket-btm GitHub Release at publish time, verified against the + * embedded SHA-256 in `packages/build-infra/release-assets.json`, and + * copied into the staged tmpdir before `pnpm publish` runs. + * + * Order of operations per platform: + * 1. Skip if `@` already on npm (unless --force). + * 2. Stage source dir to tmpdir, rewrite `workspace:*` + `catalog:`. + * 3. Download `iocraft--.node` from btm Release. + * 4. verifyReleaseChecksum → fail loudly on mismatch. + * 5. Copy verified `.node` into stage as `iocraft.node`. + * 6. `pnpm publish` from stage with `--access public --no-git-checks + * --ignore-scripts` (and `--provenance` in CI). + * 7. Clean up stage dir. + * + * After all per-platform packages succeed, publish the umbrella + * `@socketaddon/iocraft` last — its `optionalDependencies` reference the + * per-platform packages, so they must exist on the registry first. + * + * Usage: + * pnpm run publish:ci # publish with dist-tag=latest + * pnpm run publish -- --tag next # alt dist-tag + * pnpm run publish -- --dry-run # stage + verify, skip publish + * pnpm run publish -- --force # publish even if version exists + * pnpm run publish -- --platforms=darwin-arm64,linux-x64 + * # subset for testing + * pnpm run publish -- --skip-umbrella # only per-platform + */ + +import { execFileSync } from 'node:child_process' +import { promises as fs, readFileSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { safeDelete, safeDeleteSync } from '@socketsecurity/lib/fs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { downloadReleaseAsset } from '@socketsecurity/lib/releases/github-downloads' +import { SOCKET_BTM_REPO } from '@socketsecurity/lib/releases/socket-btm' + +import { verifyReleaseChecksum } from '../packages/build-infra/lib/release-checksums.mts' +import { errorMessage } from './lib/error-utils.mts' + +const logger = getDefaultLogger() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..') + +interface PackageInfo { + /** Directory name under `packages/`. */ + dir: string + /** Full npm package name. */ + name: string + /** Current version from its `package.json`. */ + version: string + /** + * Asset suffix used in the btm Release filename, e.g. `darwin-arm64` or + * `linux-x64-musl` or `win-x64`. Undefined for the umbrella package. + * + * Note: btm uses `win-` (release naming); the npm package uses `win32-` + * (process.platform naming). This field is the btm side. + */ + releaseSuffix?: string +} + +/** + * Per-platform packages publish first; umbrella references them via + * optionalDependencies and publishes last. Order within per-platform + * doesn't matter — each is independent. + */ +const PER_PLATFORM_PACKAGES: ReadonlyArray<{ dir: string; releaseSuffix: string }> = [ + { dir: 'iocraft-darwin-arm64', releaseSuffix: 'darwin-arm64' }, + { dir: 'iocraft-darwin-x64', releaseSuffix: 'darwin-x64' }, + { dir: 'iocraft-linux-arm64', releaseSuffix: 'linux-arm64' }, + { dir: 'iocraft-linux-arm64-musl', releaseSuffix: 'linux-arm64-musl' }, + { dir: 'iocraft-linux-x64', releaseSuffix: 'linux-x64' }, + { dir: 'iocraft-linux-x64-musl', releaseSuffix: 'linux-x64-musl' }, + { dir: 'iocraft-win32-arm64', releaseSuffix: 'win-arm64' }, + { dir: 'iocraft-win32-x64', releaseSuffix: 'win-x64' }, +] + +const UMBRELLA_DIR = 'iocraft' + +interface ReleaseAssets { + iocraft?: { + tag: string + checksums: Record + } +} + +function readPackage(dir: string, releaseSuffix?: string): PackageInfo { + const pkgPath = path.join(rootDir, 'packages', dir, 'package.json') + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { + name: string + version: string + } + return { dir, name: pkg.name, version: pkg.version, releaseSuffix } +} + +function readReleaseAssets(): ReleaseAssets { + const assetPath = path.join( + rootDir, + 'packages', + 'build-infra', + 'release-assets.json', + ) + return JSON.parse(readFileSync(assetPath, 'utf8')) as ReleaseAssets +} + +function run(cmd: string, args: string[], cwd: string): string { + return execFileSync(cmd, args, { cwd, encoding: 'utf8', stdio: 'pipe' }) +} + +function runInteractive(cmd: string, args: string[], cwd: string): void { + execFileSync(cmd, args, { cwd, stdio: 'inherit' }) +} + +function isPublished(name: string, version: string): boolean { + try { + run('npm', ['view', `${name}@${version}`, 'version'], rootDir) + return true + } catch { + return false + } +} + +interface ParsedArgs { + tag: string + dryRun: boolean + force: boolean + platforms: Set | null + skipUmbrella: boolean +} + +function parseArgs(): ParsedArgs { + const args = process.argv.slice(2) + let tag = 'latest' + let platforms: Set | null = null + for (let i = 0; i < args.length; i += 1) { + const a = args[i]! + if (a.startsWith('--tag=')) { + tag = a.slice('--tag='.length) + } else if (a === '--tag' && i + 1 < args.length) { + tag = args[i + 1]! + i += 1 + } else if (a.startsWith('--platforms=')) { + platforms = new Set(a.slice('--platforms='.length).split(',')) + } else if (a === '--platforms' && i + 1 < args.length) { + platforms = new Set(args[i + 1]!.split(',')) + i += 1 + } + } + return { + tag, + dryRun: args.includes('--dry-run'), + force: args.includes('--force'), + skipUmbrella: args.includes('--skip-umbrella'), + platforms, + } +} + +/** + * Read the workspace's catalog block from pnpm-workspace.yaml. + * Returns a flat name -> version map. + */ +function readWorkspaceCatalog(workspaceRoot: string): Record { + const wsPath = path.join(workspaceRoot, 'pnpm-workspace.yaml') + const yaml = readFileSync(wsPath, 'utf8') + const lines = yaml.split('\n') + const catalog: Record = { __proto__: null as never } + let inCatalog = false + for (const line of lines) { + if (line === 'catalog:') { + inCatalog = true + continue + } + if (inCatalog) { + if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t')) { + break + } + const m = line.match(/^\s+'?([^':\s]+(?:\/[^':\s]+)?)'?:\s*(.+?)\s*$/) + if (m) { + catalog[m[1]!] = m[2]!.replace(/^['"]|['"]$/g, '') + } + } + } + return catalog +} + +/** + * Rewrite `workspace:*` -> exact version and `catalog:` -> exact + * version in the staged manifest's deps blocks. + */ +function rewriteDeps( + pj: Record, + workspaceVersions: Map, + catalog: Record, +): void { + const blocks = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', + ] as const + for (const block of blocks) { + const deps = pj[block] as Record | undefined + if (!deps) { + continue + } + for (const [name, spec] of Object.entries(deps)) { + if ( + spec === 'workspace:*' || + spec === 'workspace:^' || + spec === 'workspace:~' + ) { + const resolved = workspaceVersions.get(name) + if (!resolved) { + throw new Error( + `${block}.${name} = ${spec} but no workspace package matches`, + ) + } + deps[name] = resolved + } else if (spec === 'catalog:' || spec.startsWith('catalog:')) { + const resolved = catalog[name] + if (!resolved) { + throw new Error( + `${block}.${name} = ${spec} but no catalog entry matches`, + ) + } + deps[name] = resolved + } + } + } +} + +/** + * Stage a package's source dir into a fresh os.tmpdir() subdir with + * deps rewrites applied. Working tree is untouched. + */ +async function stagePackage( + pkgDir: string, + slug: string, + workspaceVersions: Map, + catalog: Record, +): Promise { + const stage = await fs.mkdtemp( + path.join(os.tmpdir(), `socket-addon-publish-${slug}-${process.pid}-`), + ) + await fs.cp(pkgDir, stage, { + recursive: true, + dereference: true, + filter: src => { + const base = path.basename(src) + return ( + base !== 'node_modules' && + base !== '.git' && + base !== '.gitignore' && + base !== '.gitkeep' && + !base.startsWith('.pnpm') && + base !== 'pnpm-lock.yaml' + ) + }, + }) + + const stagedPj = path.join(stage, 'package.json') + const pj = JSON.parse(await fs.readFile(stagedPj, 'utf8')) as Record< + string, + unknown + > + rewriteDeps(pj, workspaceVersions, catalog) + await fs.writeFile(stagedPj, JSON.stringify(pj, null, 2) + '\n', 'utf8') + + return stage +} + +/** + * Download the per-platform `.node` from btm's iocraft Release, verify + * its SHA-256 against the embedded checksum, and drop it into the + * staged dir as `iocraft.node`. Throws on download failure or checksum + * mismatch — never publishes a package whose binary we couldn't verify. + */ +async function downloadAndVerifyNode( + stage: string, + releaseSuffix: string, + iocraftAssets: NonNullable, +): Promise { + const { tag } = iocraftAssets + const assetName = `${tag}-${releaseSuffix}.node` + const downloadPath = path.join(stage, 'iocraft.node') + + logger.info(` downloading ${assetName} from socket-btm@${tag}`) + await downloadReleaseAsset( + tag, + assetName, + downloadPath, + SOCKET_BTM_REPO, + { quiet: true }, + ) + + const result = await verifyReleaseChecksum({ + assetName, + filePath: downloadPath, + tool: 'iocraft', + quiet: true, + }) + + if (!result.valid) { + throw new Error( + `Checksum verification failed for ${assetName}\n` + + ` expected: ${result.expected ?? ''}\n` + + ` actual: ${result.actual ?? ''}\n` + + ` Bump the iocraft tag + checksums in packages/build-infra/release-assets.json`, + ) + } + + // Sanity: refuse to publish an empty file even if checksums align. + const stat = await fs.stat(downloadPath) + if (stat.size < 1024) { + throw new Error( + `Downloaded ${assetName} is suspiciously small (${stat.size} bytes)`, + ) + } + + logger.info(` verified ${assetName} (${stat.size} bytes, sha-256 ok)`) +} + +interface PublishContext { + catalog: Record + dryRun: boolean + force: boolean + iocraftAssets: NonNullable + stagingDirs: string[] + tag: string + workspaceVersions: Map +} + +async function publishPackage( + pkg: PackageInfo, + ctx: PublishContext, +): Promise { + if (!ctx.force && isPublished(pkg.name, pkg.version)) { + logger.info(` ${pkg.name}@${pkg.version} — already published, skipping`) + return + } + + const pkgDir = path.join(rootDir, 'packages', pkg.dir) + const stage = await stagePackage( + pkgDir, + pkg.dir, + ctx.workspaceVersions, + ctx.catalog, + ) + ctx.stagingDirs.push(stage) + + if (pkg.releaseSuffix) { + await downloadAndVerifyNode(stage, pkg.releaseSuffix, ctx.iocraftAssets) + } + + const publishArgs = [ + 'publish', + '--access', + 'public', + '--tag', + ctx.tag, + '--no-git-checks', + '--ignore-scripts', + ] + if (process.env['GITHUB_ACTIONS'] === 'true') { + publishArgs.push('--provenance') + } + if (ctx.dryRun) { + publishArgs.push('--dry-run') + } + + logger.info(` ${pkg.name}@${pkg.version} — publishing`) + runInteractive('pnpm', publishArgs, stage) + logger.success(` ${pkg.name}@${pkg.version} — done`) +} + +async function main(): Promise { + const { tag, dryRun, force, platforms, skipUmbrella } = parseArgs() + + logger.info( + `Publish mode: tag=${tag}` + + `${dryRun ? ' dry-run' : ''}` + + `${force ? ' force' : ''}` + + `${platforms ? ` platforms=${[...platforms].join(',')}` : ''}` + + `${skipUmbrella ? ' skip-umbrella' : ''}`, + ) + + const releaseAssets = readReleaseAssets() + if (!releaseAssets.iocraft?.tag) { + throw new Error( + 'release-assets.json missing iocraft.tag — cannot resolve binaries', + ) + } + logger.info(`Using iocraft release: ${releaseAssets.iocraft.tag}`) + + const filteredPerPlatform = platforms + ? PER_PLATFORM_PACKAGES.filter(p => platforms.has(p.releaseSuffix) || + platforms.has(p.dir.replace(/^iocraft-/, ''))) + : PER_PLATFORM_PACKAGES + + if (platforms && filteredPerPlatform.length === 0) { + throw new Error( + `--platforms=${[...platforms].join(',')} matched no known platform`, + ) + } + + const allPackages = [ + ...filteredPerPlatform.map(p => readPackage(p.dir, p.releaseSuffix)), + ...(skipUmbrella || platforms ? [] : [readPackage(UMBRELLA_DIR)]), + ] + + const workspaceVersions = new Map( + allPackages.map(p => [p.name, p.version]), + ) + // Include all per-platform packages in the version map so the umbrella's + // optionalDependencies can resolve even if --platforms filters the publish set. + for (const { dir } of PER_PLATFORM_PACKAGES) { + if (!filteredPerPlatform.some(p => p.dir === dir)) { + const info = readPackage(dir) + workspaceVersions.set(info.name, info.version) + } + } + const catalog = readWorkspaceCatalog(rootDir) + + const stagingDirs: string[] = [] + const cleanup = (): void => { + for (const d of stagingDirs) { + try { + safeDeleteSync(d) + } catch { + /* swallow during teardown */ + } + } + } + process.once('SIGINT', () => { + logger.warn('SIGINT — cleaning up staging dirs') + cleanup() + process.exit(130) + }) + process.once('SIGTERM', () => { + logger.warn('SIGTERM — cleaning up staging dirs') + cleanup() + process.exit(143) + }) + + const ctx: PublishContext = { + catalog, + dryRun, + force, + iocraftAssets: releaseAssets.iocraft, + stagingDirs, + tag, + workspaceVersions, + } + + try { + // Per-platform first. + for (const pkg of allPackages.filter(p => p.releaseSuffix)) { + await publishPackage(pkg, ctx) + } + // Umbrella last so its optionalDependencies can resolve. + const umbrella = allPackages.find(p => !p.releaseSuffix) + if (umbrella) { + await publishPackage(umbrella, ctx) + } + } finally { + for (const d of stagingDirs) { + await safeDelete(d) + } + } +} + +main().catch((e: unknown) => { + logger.error(`publish: ${errorMessage(e)}`) + process.exitCode = 1 +})