diff --git a/.contributors.yml b/.contributors.yml new file mode 100644 index 0000000..1a229a8 --- /dev/null +++ b/.contributors.yml @@ -0,0 +1,36 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/smorinlabs/contributors-please/main/schemas/config.schema.json + +classifier: path +output_file: CONTRIBUTORS.md +state_file: .contributors.jsonl +in_place: true +in_place_marker_start: "" +in_place_marker_end: "" +entry_template: "- [{{name}}]({{profile}}) - {{title}} ({{commits}} commits)" +columns_per_row: 1 +sort: contributions +min_contributions: 1 +ignore: + - Copilot + - claude + - github-actions[bot] + - dependabot[bot] + - smorin-release-please[bot] + +classification: + categories: + - id: docs + label: Documentation Contributor + paths: + - "docs/**" + - "prds/**" + - "*.md" + - ".github/**" + - id: code + label: Code Contributor + paths: + - "crates/**" + - "tests/**" + - "benches/**" + - "nix/**" + default_category: code diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 3e56437..2e6463c 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -55,3 +55,13 @@ paths: - >- missing input "app-id" which is required by action "actions/create-github-app-token@v3" + .github/workflows/release.yml: + ignore: + # Same two false positives — release.yml's update-homebrew job mints a + # tap-push App token with the same action + client-id input. + - >- + input "client-id" is not defined in action + "actions/create-github-app-token@v3" + - >- + missing input "app-id" which is required by action + "actions/create-github-app-token@v3" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac12274..eebb46a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,17 @@ +# Release pipeline. Fires on the v* tag release-please pushes; builds binaries +# once, then fans out to every publish destination (GitHub Release assets, +# crates.io, PyPI, npm, Homebrew tap). +# +# This is the most security-sensitive workflow in the repo: +# - permissions are deny-all at the top; every job re-grants only what it needs +# - the guards job stops tags that don't point at main or don't match the +# workspace version — a v* tag can otherwise point at ANY commit +# - every registry that supports OIDC trusted publishing uses it (crates.io, +# TestPyPI, PyPI, npm); the sole long-lived secret is HOMEBREW_TAP_TOKEN +# (a cross-repo git push has no OIDC path) +# - production-publish jobs run in GitHub environments so a required reviewer +# is the final human gate + name: Release on: @@ -6,14 +20,63 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+*' workflow_dispatch: -permissions: - contents: write +permissions: {} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false # never cancel an in-flight publish jobs: + # Tag provenance guards. Everything else `needs:` this job. + guards: + name: Verify tag provenance + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Validate tag ref + if: github.ref_type != 'tag' + run: | + echo "::error::Release workflow must be triggered by a tag push, not a branch." + exit 1 + + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 # full history for the ancestry check + + - name: Verify tag is reachable from main + # A v* tag can point at any commit, including never-merged branch + # work. Without this, anyone with tag-push rights releases off-main + # code. The environment reviewers are the final gate; this check + # stops the workflow before it even builds. + run: | + set -euo pipefail + git fetch origin main + if ! git merge-base --is-ancestor "${GITHUB_SHA}" origin/main; then + echo "::error::Tag ${GITHUB_REF_NAME} (${GITHUB_SHA}) is not reachable from origin/main." + exit 1 + fi + echo "PASS: tag on main" + + - name: Verify tag matches workspace version + run: | + set -euo pipefail + TAG_VERSION="${GITHUB_REF_NAME#v}" + PKG_VERSION="$(sed -n '/^\[workspace\.package\]/,/^\[/p' Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -1)" + if [ "${TAG_VERSION}" != "${PKG_VERSION}" ]; then + echo "::error::Tag ${TAG_VERSION} != workspace version ${PKG_VERSION}" + exit 1 + fi + echo "PASS: tag matches workspace version (${TAG_VERSION})" + build-release: name: Build (${{ matrix.target }}) + needs: guards runs-on: ${{ matrix.os }} timeout-minutes: 30 + permissions: + contents: write # attach artifacts to the GitHub Release strategy: fail-fast: false matrix: @@ -29,12 +92,6 @@ jobs: - os: windows-latest target: x86_64-pc-windows-msvc steps: - - name: Validate tag ref - if: github.ref_type != 'tag' - run: | - echo "::error::Release workflow must be triggered by a tag push, not a branch." - exit 1 - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable @@ -47,10 +104,13 @@ jobs: - uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2 - - name: Upload binary to release + - name: Upload binaries to release uses: taiki-e/upload-rust-binary-action@57510bf386b3945b57963e73201cea60ca18dff4 # v1.9.1 with: - bin: toggle + # Both CLI entry points ship in one archive per target; the npm + # platform packages and the Homebrew formula unpack these. + bin: toggle,togl + archive: togl-$target target: ${{ matrix.target }} tar: unix zip: windows @@ -68,6 +128,7 @@ jobs: # short-lived token via `crates-io-auth-action`; no secret is required. publish-crates: name: Publish to crates.io + needs: guards runs-on: ubuntu-latest timeout-minutes: 30 environment: crates @@ -75,12 +136,6 @@ jobs: id-token: write # required to mint the OIDC token for trusted publishing contents: read steps: - - name: Validate tag ref - if: github.ref_type != 'tag' - run: | - echo "::error::Release workflow must be triggered by a tag push, not a branch." - exit 1 - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable @@ -108,3 +163,267 @@ jobs: run: cargo publish -p togl --allow-dirty env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + + # Build platform wheels for PyPI. maturin `bindings = "bin"` (pyproject.toml) + # packages the toggle/togl binaries as wheel entry points — one wheel per + # platform, no sdist (a source install would require a Rust toolchain). + build-wheels: + name: Wheel (${{ matrix.target }}) + needs: guards + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + manylinux: auto + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + manylinux: musllinux_1_2 + - os: macos-latest + target: x86_64-apple-darwin + - os: macos-latest + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Build wheel + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + with: + target: ${{ matrix.target }} + args: --release --locked --out dist + manylinux: ${{ matrix.manylinux }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-${{ matrix.target }} + path: dist/*.whl + if-no-files-found: error + + # TestPyPI publishes automatically as the smoke test; PyPI (below) sits + # behind the `pypi` environment's required reviewer. + publish-testpypi: + name: Publish to TestPyPI + needs: build-wheels + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: testpypi + permissions: + id-token: write # OIDC trusted publishing needs nothing else + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: wheels-* + merge-multiple: true + path: dist/ + + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + ignore-empty-workdir: true + + - name: Publish to TestPyPI + run: | + uv publish --trusted-publishing always \ + --publish-url https://test.pypi.org/legacy/ \ + --check-url https://test.pypi.org/simple/ \ + dist/* + + publish-pypi: + name: Publish to PyPI + needs: publish-testpypi + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: pypi # required reviewer = the human gate + permissions: + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: wheels-* + merge-multiple: true + path: dist/ + + - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + ignore-empty-workdir: true + + - name: Publish to PyPI + run: | + uv publish --trusted-publishing always \ + --check-url https://pypi.org/simple/ \ + dist/* + + # Publish the wrapper + platform packages (npm/README.md documents the + # layout). Binaries come from the release tarballs build-release attached; + # versions are stamped from the tag. All five packages use npm trusted + # publishing (OIDC; npm >= 11.5.1 auto-emits provenance). + publish-npm: + name: Publish to npm + needs: build-release + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: npm + permissions: + contents: read # gh release download + id-token: write # trusted publishing + auto-provenance + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: Ensure npm >= 11.5.1 (trusted publishing floor) + run: npm install -g npm@latest + + - name: Download release tarballs + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ + --pattern 'togl-*' --dir tarballs + + - name: Unpack binaries into platform packages + run: | + set -euo pipefail + declare -A MAP=( + [linux-x64]=x86_64-unknown-linux-musl + [darwin-x64]=x86_64-apple-darwin + [darwin-arm64]=aarch64-apple-darwin + [win32-x64]=x86_64-pc-windows-msvc + ) + for plat in "${!MAP[@]}"; do + target="${MAP[$plat]}" + bindir="npm/platform/${plat}/bin" + mkdir -p "${bindir}" + if [ "${plat}" = "win32-x64" ]; then + unzip -o "tarballs/togl-${target}.zip" -d "${bindir}" + test -f "${bindir}/togl.exe" && test -f "${bindir}/toggle.exe" + else + tar -xzf "tarballs/togl-${target}.tar.gz" -C "${bindir}" + test -x "${bindir}/togl" && test -x "${bindir}/toggle" + fi + done + + - name: Stamp package versions from the tag + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do + (cd "npm/platform/${plat}" && npm pkg set version="${VERSION}") + done + (cd npm/togl-cli && npm pkg set version="${VERSION}" \ + "optionalDependencies.@smorinlabs/togl-linux-x64=${VERSION}" \ + "optionalDependencies.@smorinlabs/togl-darwin-x64=${VERSION}" \ + "optionalDependencies.@smorinlabs/togl-darwin-arm64=${VERSION}" \ + "optionalDependencies.@smorinlabs/togl-win32-x64=${VERSION}") + + # Platform packages first — the wrapper's optionalDependencies must be + # resolvable the moment it lands. + - name: Publish platform packages + run: | + set -euo pipefail + for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do + (cd "npm/platform/${plat}" && npm publish --access public) + done + + - name: Publish togl-cli + run: | + set -euo pipefail + cd npm/togl-cli && npm publish + + # Bump the prebuilt-binary formula in smorinlabs/homebrew-tap. The push + # authenticates with a short-lived App installation token minted in-job + # (same technique as release-please.yml), scoped to only the tap repo — + # no long-lived credential. One-time setup: the smorinlabs App installed + # on smorinlabs/homebrew-tap; TAP_PUSH_APP_* secrets via + # `repo-secrets --app tap-push`. + update-homebrew: + name: Update Homebrew tap + needs: build-release + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read # gh release download + steps: + - name: Download darwin tarballs and compute checksums + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ + --pattern '*darwin*' --dir tarballs + shasum -a 256 tarballs/* + - name: Render Formula/togl.rb + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + ARM_SHA="$(shasum -a 256 tarballs/togl-aarch64-apple-darwin.tar.gz | awk '{print $1}')" + X64_SHA="$(shasum -a 256 tarballs/togl-x86_64-apple-darwin.tar.gz | awk '{print $1}')" + cat > togl.rb < + diff --git a/PROJECTS.md b/PROJECTS.md index d886f06..a2bf555 100644 --- a/PROJECTS.md +++ b/PROJECTS.md @@ -302,3 +302,58 @@ the CLI and auto-publish the library + CLI on each release tag. with a manual `0.0.0` publish since crates.io TP needs the crate to exist first.) - First real publish fires on the next release tag (e.g. v0.6.0); crates.io versions jump 0.2.3 → that release (allowed — versions only need to increase). + +--- + +## [-] Project P12: Distribution — PyPI, npm, Homebrew (v0.6.0) +**Goal**: Ship the togl CLI to PyPI (`togl`), npm (`togl-cli` + +`@smorinlabs/togl-*` platform packages), and Homebrew +(`smorinlabs/tap/togl`) from the existing tag-triggered `release.yml`, +with OIDC trusted publishing everywhere a registry supports it. Also +hardens `release.yml` (guards job: tag-on-main ancestry + tag==version, +deny-all top-level permissions, no-cancel concurrency) and installs +contributors-please. + +**Decisions** (locked 2026-06-07) +- PyPI: maturin `bindings=bin` wheels (no sdist), package `togl`, both + `toggle`+`togl` commands; TestPyPI (env `testpypi`, auto) → PyPI (env + `pypi`, required reviewer); OIDC on both registries. +- npm: esbuild-style — unscoped wrapper `togl-cli` + 4 platform packages + `@smorinlabs/togl-{linux-x64,darwin-x64,darwin-arm64,win32-x64}` + (linux = musl build); strict OIDC on all 5. +- Homebrew: prebuilt-binary formula `Formula/togl.rb` in + smorinlabs/homebrew-tap, pushed with a short-lived App installation + token minted in-job (`TAP_PUSH_APP_*` secrets; smorinlabs App installed + on the tap) — revised 2026-07-15 from the original `HOMEBREW_TAP_TOKEN` + PAT plan, eliminating the pattern's last long-lived credential. +- Release archives renamed `toggle-` → `togl-` and now + contain both binaries. + +**Out of Scope** +- Publishing the library to PyPI/npm (togl-ffi is C-ABI; no PyO3/napi). +- cargo-dist (rejected — would own release.yml; axo.dev wound down). + +### Tests & Tasks +- [x] [P12-T01] pyproject.toml (maturin bindings=bin) + `build-wheels` + matrix, `publish-testpypi` → `publish-pypi` jobs +- [x] [P12-T02] npm/ scaffolding (wrapper + 4 platform pkgs) + + `publish-npm` job (unpack tarballs, stamp versions, publish) +- [x] [P12-T03] `update-homebrew` job rendering Formula/togl.rb from + darwin tarball checksums +- [x] [P12-T04] Harden release.yml: `guards` job, `permissions: {}`, + no-cancel concurrency; ship both bins in `togl-` archives +- [x] [P12-T05] contributors-please: update-contributors.yml, + .contributors.yml, CONTRIBUTORS.md markers +- [ ] [P12-TS01] Pre-release tag (vX.Y.Z-rc.1) exercises the pipeline + end-to-end before the first real release + +### Manual Steps (maintainer) +- PyPI + TestPyPI: add trusted publisher per registry for `togl` + (owner `smorin`, repo `toggle`, workflow `release.yml`, env + `pypi`/`testpypi`). +- npm: one-time placeholder publish of the 4 platform packages with a + temp classic Automation token (npm has no pending publishers), attach + trusted publisher (`smorin`/`toggle`/`release.yml`/env `npm`) on all 5 + packages, revoke token. +- Homebrew: install the smorinlabs App on smorinlabs/homebrew-tap, then + `repo_secrets set --repo smorin/toggle --app tap-push`. diff --git a/RELEASE.md b/RELEASE.md index 58b66d1..97e8199 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,11 +12,33 @@ existing tag-triggered **`release.yml`** binary build. writing a `CHANGELOG.md` entry. The `sync-cargo-lock` job updates `Cargo.lock` on that PR branch so CI's `--locked` checks pass. 3. Merge the release PR. release-please pushes a `v*` tag. -4. The `release.yml` workflow fires on the tag and builds the multi-target - release binaries. +4. The `release.yml` workflow fires on the tag, verifies tag provenance + (tag on `main`, tag == workspace version), builds the multi-target release + binaries, and fans out to the publish destinations below. `feat:` → minor bump, `fix:`/`perf:` → patch, `feat!:`/`BREAKING CHANGE:` → major. +## Publish destinations + +Every registry that supports OIDC trusted publishing uses it, and the +Homebrew tap push mints a short-lived App installation token in-job — no +long-lived credentials anywhere. Production publishes run in GitHub +environments; the `pypi`, `npm`, and `crates` environments have a required +reviewer as the final human gate (TestPyPI publishes automatically as the +smoke test). + +| Destination | Job | Environment | Auth | +|---|---|---|---| +| GitHub Release binaries | `build-release` | — | `GITHUB_TOKEN` | +| crates.io (`togl-lib`, `togl`) | `publish-crates` | `crates` | OIDC trusted publishing | +| TestPyPI → PyPI (`togl`) | `publish-testpypi` → `publish-pypi` | `testpypi` → `pypi` | OIDC trusted publishing (wheels via maturin `bindings=bin`) | +| npm (`togl-cli` + 4 `@smorinlabs/togl-*` platform packages) | `publish-npm` | `npm` | OIDC trusted publishing | +| Homebrew (`smorinlabs/tap/togl`) | `update-homebrew` | — | Short-lived App token (`TAP_PUSH_APP_*` secrets; App installed on the tap) | + +Release archives are named `togl-.tar.gz` (`.zip` on Windows) and +contain both the `toggle` and `togl` binaries; the npm platform packages and +the Homebrew formula unpack them. + ## Disabling release-please If you'd rather not use PR-driven version proposals: @@ -44,7 +66,7 @@ of these two mechanisms instead: 1. **GitHub App (preferred).** Create a GitHub App with **Contents** and **Pull requests: write**, install it on this repo, then add two secrets: - - `RELEASE_PLEASE_APP_ID` — the App's numeric ID + - `RELEASE_PLEASE_CLIENT_ID` — the App's Client ID - `RELEASE_PLEASE_PRIVATE_KEY` — the App's private key (`.pem` contents) Both jobs mint a short-lived installation token from these. diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..428cbf7 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,20 @@ +# npm packaging for togl + +esbuild-style layout: the unscoped wrapper [`togl-cli`](togl-cli/) exposes the +`togl` and `toggle` commands via Node shims and declares the four platform +packages under [`platform/`](platform/) as `optionalDependencies` +(`@smorinlabs/togl-{linux-x64,darwin-x64,darwin-arm64,win32-x64}`). npm +installs only the package matching the host's `os`/`cpu`; the shim +`require.resolve`s the binary out of it. + +Nothing here is published from a dev machine. The `publish-npm` job in +[`.github/workflows/release.yml`](../.github/workflows/release.yml) runs on the +`v*` tag: it downloads the release tarballs, unpacks the binaries into each +platform package's `bin/`, stamps every `version` (and the wrapper's +`optionalDependencies` ranges) to the release version, then publishes the four +platform packages followed by the wrapper — all via npm trusted publishing +(OIDC, no stored token). The committed `0.0.0` versions and empty `bin/` +directories are placeholders by design. + +Linux x64 ships the musl (statically linked) build so it works on both glibc +and musl distros. diff --git a/npm/platform/darwin-arm64/package.json b/npm/platform/darwin-arm64/package.json new file mode 100644 index 0000000..a470766 --- /dev/null +++ b/npm/platform/darwin-arm64/package.json @@ -0,0 +1,22 @@ +{ + "name": "@smorinlabs/togl-darwin-arm64", + "version": "0.0.0", + "description": "Prebuilt togl/toggle binaries for macOS arm64 (Apple Silicon)", + "repository": { + "type": "git", + "url": "git+https://github.com/smorin/toggle.git" + }, + "license": "MIT", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "files": [ + "bin" + ], + "engines": { + "node": ">=18" + } +} diff --git a/npm/platform/darwin-x64/package.json b/npm/platform/darwin-x64/package.json new file mode 100644 index 0000000..a9e3175 --- /dev/null +++ b/npm/platform/darwin-x64/package.json @@ -0,0 +1,22 @@ +{ + "name": "@smorinlabs/togl-darwin-x64", + "version": "0.0.0", + "description": "Prebuilt togl/toggle binaries for macOS x64", + "repository": { + "type": "git", + "url": "git+https://github.com/smorin/toggle.git" + }, + "license": "MIT", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin" + ], + "engines": { + "node": ">=18" + } +} diff --git a/npm/platform/linux-x64/package.json b/npm/platform/linux-x64/package.json new file mode 100644 index 0000000..580e866 --- /dev/null +++ b/npm/platform/linux-x64/package.json @@ -0,0 +1,22 @@ +{ + "name": "@smorinlabs/togl-linux-x64", + "version": "0.0.0", + "description": "Prebuilt togl/toggle binaries for Linux x64 (musl, statically linked)", + "repository": { + "type": "git", + "url": "git+https://github.com/smorin/toggle.git" + }, + "license": "MIT", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin" + ], + "engines": { + "node": ">=18" + } +} diff --git a/npm/platform/win32-x64/package.json b/npm/platform/win32-x64/package.json new file mode 100644 index 0000000..045ae5e --- /dev/null +++ b/npm/platform/win32-x64/package.json @@ -0,0 +1,22 @@ +{ + "name": "@smorinlabs/togl-win32-x64", + "version": "0.0.0", + "description": "Prebuilt togl/toggle binaries for Windows x64", + "repository": { + "type": "git", + "url": "git+https://github.com/smorin/toggle.git" + }, + "license": "MIT", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "files": [ + "bin" + ], + "engines": { + "node": ">=18" + } +} diff --git a/npm/togl-cli/bin/toggle.js b/npm/togl-cli/bin/toggle.js new file mode 100644 index 0000000..5509447 --- /dev/null +++ b/npm/togl-cli/bin/toggle.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +'use strict'; +require('../lib/run').run('toggle'); diff --git a/npm/togl-cli/bin/togl.js b/npm/togl-cli/bin/togl.js new file mode 100644 index 0000000..959f12b --- /dev/null +++ b/npm/togl-cli/bin/togl.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +'use strict'; +require('../lib/run').run('togl'); diff --git a/npm/togl-cli/lib/run.js b/npm/togl-cli/lib/run.js new file mode 100644 index 0000000..38099a9 --- /dev/null +++ b/npm/togl-cli/lib/run.js @@ -0,0 +1,37 @@ +'use strict'; +const { spawnSync } = require('node:child_process'); + +const PLATFORM_PACKAGES = { + 'linux-x64': '@smorinlabs/togl-linux-x64', + 'darwin-x64': '@smorinlabs/togl-darwin-x64', + 'darwin-arm64': '@smorinlabs/togl-darwin-arm64', + 'win32-x64': '@smorinlabs/togl-win32-x64', +}; + +function run(binName) { + const key = `${process.platform}-${process.arch}`; + const pkg = PLATFORM_PACKAGES[key]; + if (!pkg) { + console.error(`togl-cli: unsupported platform: ${key}`); + process.exit(1); + } + const exe = process.platform === 'win32' ? `${binName}.exe` : binName; + let binPath; + try { + binPath = require.resolve(`${pkg}/bin/${exe}`); + } catch { + console.error( + `togl-cli: platform package ${pkg} is missing.\n` + + 'It is an optionalDependency — reinstall without --no-optional / --omit=optional.' + ); + process.exit(1); + } + const result = spawnSync(binPath, process.argv.slice(2), { stdio: 'inherit' }); + if (result.error) { + console.error(`togl-cli: failed to launch ${exe}: ${result.error.message}`); + process.exit(1); + } + process.exit(result.status === null ? 1 : result.status); +} + +module.exports = { run }; diff --git a/npm/togl-cli/package.json b/npm/togl-cli/package.json new file mode 100644 index 0000000..50ee7d8 --- /dev/null +++ b/npm/togl-cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "togl-cli", + "version": "0.0.0", + "description": "A CLI tool for toggling code comments across multiple languages (prebuilt togl/toggle binaries)", + "repository": { + "type": "git", + "url": "git+https://github.com/smorin/toggle.git" + }, + "license": "MIT", + "bin": { + "togl": "bin/togl.js", + "toggle": "bin/toggle.js" + }, + "files": [ + "bin", + "lib" + ], + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@smorinlabs/togl-linux-x64": "0.0.0", + "@smorinlabs/togl-darwin-x64": "0.0.0", + "@smorinlabs/togl-darwin-arm64": "0.0.0", + "@smorinlabs/togl-win32-x64": "0.0.0" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..101abcd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +# PyPI packaging for the togl CLI. maturin `bindings = "bin"` ships the +# compiled `toggle` + `togl` binaries as wheel entry points — no Python +# bindings, no PyO3. Version comes from crates/togl-cli/Cargo.toml (the +# workspace version release-please bumps). +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "togl" +description = "A CLI tool for toggling code comments across multiple languages" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Steve Morin" }] +requires-python = ">=3.9" +keywords = ["cli", "comments", "toggle", "code"] +classifiers = [ + "Environment :: Console", + "Programming Language :: Rust", + "Operating System :: OS Independent", +] +dynamic = ["version"] + +[project.urls] +Repository = "https://github.com/smorin/toggle" +Changelog = "https://github.com/smorin/toggle/blob/main/CHANGELOG.md" + +[tool.maturin] +bindings = "bin" +manifest-path = "crates/togl-cli/Cargo.toml"