diff --git a/.github/workflows/install-script-tests.yml b/.github/workflows/install-script-tests.yml index f4efeeb16..e8a6a1b9f 100644 --- a/.github/workflows/install-script-tests.yml +++ b/.github/workflows/install-script-tests.yml @@ -7,13 +7,21 @@ on: - '.github/workflows/install-script-tests.yml' - 'bin/test_install_script.sh' - 'bin/test_install_script_over_homebrew.sh' + - 'npm/**' + - '.goreleaser.yml' + - 'scripts/npm-publish.sh' pull_request: paths: - 'install-cli.sh' - '.github/workflows/install-script-tests.yml' - 'bin/test_install_script.sh' - 'bin/test_install_script_over_homebrew.sh' + - 'npm/**' + - '.goreleaser.yml' + - 'scripts/npm-publish.sh' workflow_dispatch: + release: + types: [published] jobs: test-script: @@ -63,4 +71,34 @@ jobs: shell: bash run: | chmod +x install-cli.sh - bash bin/test_install_script_over_homebrew.sh --token ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + bash bin/test_install_script_over_homebrew.sh --token ${{ secrets.GITHUB_TOKEN }} + + # Note: this job installs from the public npm registry, so on push/PR + # it tests the currently published version — not the code being changed. + # That still catches regressions. The release trigger is what tests new releases. + test-npm: + name: Test npm install on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest # linux/x64 + - ubuntu-24.04-arm # linux/arm64 + - macos-latest # darwin/arm64 + - windows-latest # win32/x64 + - windows-11-arm # win32/arm64 + + steps: + - name: Install @kosli/cli via npm + shell: bash + run: | + TAG="${{ github.event.release.tag_name }}" + if [[ "${{ github.event_name }}" == "release" && "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + npm install -g @kosli/cli@latest + else + npm install -g @kosli/cli@snapshot + fi + + - name: Verify kosli binary works + shell: bash + run: kosli version \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c2aba652..150f4dd85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,11 +149,17 @@ jobs: - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: + distribution: goreleaser-pro version: '~> v2' # latest args: release --clean ${{ steps.get-tag-notes.outputs.args }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FURY_TOKEN: ${{ secrets.FURY_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GORELEASER_KEY: ${{ secrets.KOSLI_GORELEASERPRO }} + + - name: Copy npm packages into dist for provenance + run: find npm -name "*.tgz" -exec cp {} dist/ \; - uses: actions/upload-artifact@v7 with: @@ -164,7 +170,7 @@ jobs: - name: Prepare artifacts list id: prepare-artifacts-list run: | - ARTIFACTS=$(jq '[reduce .[] as $item ( + GORELEASER_ARTIFACTS=$(jq '[reduce .[] as $item ( []; if ($item.type == "Archive") then . + [{ template_name: ($item.goos + "-" + $item.goarch), path: $item.path }] @@ -175,6 +181,18 @@ jobs: end )][]' dist/artifacts.json) + NPM_ARTIFACTS=$(find dist -maxdepth 1 -name "*.tgz" -printf '%f\n' \ + | jq -R '{ + template_name: ("npm-" + sub("-[0-9]+\\.[0-9]+\\.[0-9]+.*\\.tgz$"; "")), + path: ("dist/" + .) + }' \ + | jq -s '.') + + ARTIFACTS=$(jq -n \ + --argjson g "$GORELEASER_ARTIFACTS" \ + --argjson n "$NPM_ARTIFACTS" \ + '$g + $n') + echo "artifacts<> $GITHUB_OUTPUT echo "${ARTIFACTS}" >> $GITHUB_OUTPUT echo "nEOFn" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index acc7c1a17..3bcee113c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ docs.kosli.com/resources/_gen/* docs.kosli.com/content/client_reference/kosli* docs.kosli.com/public/ docs.kosli.com/.netlify +npm/cli*/bin/* +npm/*/kosli*.tgz *.tar.gz *~ /.idea diff --git a/.goreleaser.yml b/.goreleaser.yml index b212574cc..53609aef8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,8 @@ project_name: kosli before: hooks: - go mod tidy + - rm -rf npm/cli-*/bin + - find npm -name "*.tgz" -delete builds: - id: kosli binary: kosli @@ -27,6 +29,19 @@ builds: - goos: windows goarch: arm main: ./cmd/kosli/ + hooks: + post: + - cmd: >- + bash -c ' + OS="{{ .Os }}"; + ARCH="{{ .Arch }}"; + [ "$OS" = "windows" ] && OS="win32"; + [ "$ARCH" = "amd64" ] && ARCH="x64"; + EXT=""; + [ "{{ .Os }}" = "windows" ] && EXT=".exe"; + mkdir -p npm/cli-${OS}-${ARCH}/bin && + cp "{{ .Path }}" npm/cli-${OS}-${ARCH}/bin/kosli${EXT} && + chmod +x npm/cli-${OS}-${ARCH}/bin/kosli${EXT}' archives: - @@ -37,11 +52,9 @@ archives: - goos: windows formats: [zip] - # docs for nfpm can be found here: https://goreleaser.com/customization/nfpm/ nfpms: - id: kosli - # You can change the file name of the package. # # Default:`{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}` @@ -83,6 +96,12 @@ nfpms: - src: dist/{{ .ProjectName }}_{{ .Os }}_{{ if .Amd64 }}{{ .Arch }}_v1{{ else if .Arm }}{{ .Arch }}_6{{ else if eq .Arch "arm64" }}{{ .Arch }}_v8.0{{ else }}{{ .Arch }}{{ end }}/kosli dst: /usr/local/bin/kosli +after: + hooks: + - cmd: bash scripts/npm-publish.sh "{{ .Version }}"{{ if or .IsSnapshot (not .IsRelease) }} --dry-run{{ end }} + # after hooks suppresses output by default. You need to add output: true to the hook to see the script's messages. + output: true + publishers: - name: fury.io # by specifying `packages` id here goreleaser will only use this publisher diff --git a/docs.kosli.com/content/getting_started/install.md b/docs.kosli.com/content/getting_started/install.md index d07e53400..4541896c7 100644 --- a/docs.kosli.com/content/getting_started/install.md +++ b/docs.kosli.com/content/getting_started/install.md @@ -87,6 +87,18 @@ curl -L https://github.com/kosli-dev/cli/releases/download/v{{< cli-version >}}/ sudo mv kosli /usr/local/bin/kosli ``` +{{< /tab >}} + +{{< tab "NPM" >}} +You can install Kosli CLI system-wide with `npm` from the default registry + +```shell {.command} +npm install -g @kosli/cli +``` + +Using `npx` is currently not supported + + {{< /tab >}} {{< tab "From source" >}} @@ -100,7 +112,6 @@ make build {{< /tabs >}} - ## Verifying the installation worked Run this command: diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 000000000..b7f24cdcc --- /dev/null +++ b/npm/README.md @@ -0,0 +1,164 @@ +# NPM Packaging + +This directory contains the npm package structure for distributing the Kosli CLI via npm, following the same pattern used by [esbuild](https://github.com/evanw/esbuild). + +## Structure + +``` +npm/ +├── wrapper/ # @kosli/cli — the package users install +│ ├── bin/kosli # JS shim that detects the platform and runs the binary +│ ├── install.js # postinstall script that validates the binary +│ └── package.json # declares optionalDependencies for all platforms +├── cli-darwin-arm64/ # @kosli/cli-darwin-arm64 +│ ├── bin/kosli # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +├── cli-darwin-x64/ # @kosli/cli-darwin-x64 +│ ├── bin/kosli # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +├── cli-linux-arm/ # @kosli/cli-linux-arm +│ ├── bin/kosli # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +├── cli-linux-arm64/ # @kosli/cli-linux-arm64 +│ ├── bin/kosli # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +├── cli-linux-x64/ # @kosli/cli-linux-x64 +│ ├── bin/kosli # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +├── cli-win32-arm64/ # @kosli/cli-win32-arm64 +│ ├── bin/kosli.exe # the native binary — see below +│ └── package.json # declares os/cpu fields for platform filtering +└── cli-win32-x64/ # @kosli/cli-win32-x64 + ├── bin/kosli.exe # the native binary — see below + └── package.json # declares os/cpu fields for platform filtering +``` + +## How it works + +Users install a single package: + +```sh +npm install @kosli/cli +``` + +or if using in continuous integration you can install globally: + +```sh +npm install -g @kosli/cli +``` + +npm resolves the `optionalDependencies` declared in the wrapper's `package.json` and installs only the platform-specific package that matches the current OS and CPU architecture — all non-matching packages are silently skipped. The wrapper's `bin/kosli` JS shim then locates the binary inside the installed platform package and executes it. + +> **`npx` is not supported.** `npx @kosli/cli` does not install optional dependencies, so the platform binary is never fetched and the command fails. Always install the package before running it. + +## The `bin/` directories are populated by goreleaser + +The platform package `bin/` directories are **not committed to git**. They are populated automatically during the release process by a post-build hook in [`.goreleaser.yml`](../.goreleaser.yml): + +```yaml +hooks: + post: + - cmd: >- + bash -c ' + OS="{{ .Os }}"; + ARCH="{{ .Arch }}"; + [ "$OS" = "windows" ] && OS="win32"; + [ "$ARCH" = "amd64" ] && ARCH="x64"; + EXT=""; + [ "{{ .Os }}" = "windows" ] && EXT=".exe"; + mkdir -p npm/cli-${OS}-${ARCH}/bin && + cp "{{ .Path }}" npm/cli-${OS}-${ARCH}/bin/kosli${EXT} && + chmod +x npm/cli-${OS}-${ARCH}/bin/kosli${EXT}' +``` + +This hook runs once per build target immediately after goreleaser compiles the binary. It applies the following naming conventions: + +| goreleaser | npm package dir | +|------------|-----------------| +| `linux` | `linux` | +| `darwin` | `darwin` | +| `windows` | `win32` | +| `amd64` | `x64` | +| `arm64` | `arm64` | +| `arm` | `arm` | + +Windows binaries are copied as `kosli.exe`; all others as `kosli`. The `windows/arm` combination is excluded from builds. + +The `before` hooks in `.goreleaser.yml` clean up stale artifacts before each build run: + +```yaml +before: + hooks: + - rm -rf npm/cli-*/bin + - find npm -name "*.tgz" -delete +``` + +## Publishing + +Packages are published to the [npm public registry](https://registry.npmjs.org). Platform packages must be published before the wrapper, since the wrapper's `optionalDependencies` references them by version. After a goreleaser build has populated the `bin/` directories: + +```sh +# Publish platform packages first +(cd npm/cli-linux-x64 && npm publish) +(cd npm/cli-linux-arm64 && npm publish) +(cd npm/cli-linux-arm && npm publish) +(cd npm/cli-darwin-x64 && npm publish) +(cd npm/cli-darwin-arm64 && npm publish) +(cd npm/cli-win32-x64 && npm publish) +(cd npm/cli-win32-arm64 && npm publish) + +# Then publish the wrapper +(cd npm/wrapper && npm publish) +``` + +Each package directory contains an `.npmrc` that sets the auth token: + +```text +//registry.npmjs.org/:_authToken=${NPM_TOKEN} +``` + +## Automated Publishing with npm-publish.sh + +The `scripts/npm-publish.sh` script automates the npm packaging and publishing process. It injects the version into all `package.json` files, packs each package into a `.tgz`, and optionally publishes them. + +### Usage + +```bash +scripts/npm-publish.sh [--dry-run] +``` + +### Arguments + +- ``: Required. A SemVer string — either `X.Y.Z` (stable) or `X.Y.Z-TAG` (pre-release). +- `--dry-run` (optional second argument): Pack packages but skip publishing. + +### Behavior + +1. Injects `` into the `version` field of all `package.json` files. +2. Updates the `optionalDependencies` version references in `npm/wrapper/package.json` to match. +3. Runs `npm pack` on each platform package, then on the wrapper. +4. Unless `--dry-run` is set, runs `npm publish --tag ` on each package. + +The dist-tag is determined by the version format: + +| Version format | npm dist-tag | +|----------------|--------------| +| `X.Y.Z` | `latest` | +| `X.Y.Z-*` | `snapshot` | + +### Integration with GoReleaser + +GoReleaser calls this script automatically via the `after` hook once all platform binaries have been built and copied into the `bin/` directories: + +```yaml +after: + hooks: + - cmd: bash scripts/npm-publish.sh "{{ .Version }}" ... + output: true +``` + +The script output is surfaced in the goreleaser log (`output: true`). + +## Versioning + +All packages share the same version number. When releasing, `npm-publish.sh` updates it automatically in all eight `package.json` files — the seven platform packages and the wrapper — as well as the `optionalDependencies` version pins in `npm/wrapper/package.json`. There is no need to edit these files manually. diff --git a/npm/cli-darwin-arm64/.npmrc b/npm/cli-darwin-arm64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-darwin-arm64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-darwin-arm64/README.md b/npm/cli-darwin-arm64/README.md new file mode 100644 index 000000000..04f0e822f --- /dev/null +++ b/npm/cli-darwin-arm64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-darwin-arm64 + +This is the macOS ARM64 platform binary for the Kosli CLI (Apple Silicon). **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-darwin-arm64/package.json b/npm/cli-darwin-arm64/package.json new file mode 100644 index 000000000..d09eb0547 --- /dev/null +++ b/npm/cli-darwin-arm64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-darwin-arm64", + "version": "0.0.0", + "description": "macOS arm64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-darwin-arm64" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-darwin-x64/.npmrc b/npm/cli-darwin-x64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-darwin-x64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-darwin-x64/README.md b/npm/cli-darwin-x64/README.md new file mode 100644 index 000000000..a101a35d8 --- /dev/null +++ b/npm/cli-darwin-x64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-darwin-x64 + +This is the macOS x64 platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-darwin-x64/package.json b/npm/cli-darwin-x64/package.json new file mode 100644 index 000000000..a84ffbed4 --- /dev/null +++ b/npm/cli-darwin-x64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-darwin-x64", + "version": "0.0.0", + "description": "macOS x64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-darwin-x64" + }, + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-linux-arm/.npmrc b/npm/cli-linux-arm/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-linux-arm/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-linux-arm/README.md b/npm/cli-linux-arm/README.md new file mode 100644 index 000000000..ef4e8a981 --- /dev/null +++ b/npm/cli-linux-arm/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-linux-arm + +This is the Linux ARM 32-bit platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-linux-arm/package.json b/npm/cli-linux-arm/package.json new file mode 100644 index 000000000..862fa0392 --- /dev/null +++ b/npm/cli-linux-arm/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-linux-arm", + "version": "0.0.0", + "description": "Linux arm binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-linux-arm" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm" + ], + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-linux-arm64/.npmrc b/npm/cli-linux-arm64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-linux-arm64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-linux-arm64/README.md b/npm/cli-linux-arm64/README.md new file mode 100644 index 000000000..00408f7f1 --- /dev/null +++ b/npm/cli-linux-arm64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-linux-arm64 + +This is the Linux ARM64 platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-linux-arm64/package.json b/npm/cli-linux-arm64/package.json new file mode 100644 index 000000000..8c7378818 --- /dev/null +++ b/npm/cli-linux-arm64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-linux-arm64", + "version": "0.0.0", + "description": "Linux arm64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-linux-arm64" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-linux-x64/.npmrc b/npm/cli-linux-x64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-linux-x64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-linux-x64/README.md b/npm/cli-linux-x64/README.md new file mode 100644 index 000000000..e10cc2501 --- /dev/null +++ b/npm/cli-linux-x64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-linux-x64 + +This is the Linux x64 platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-linux-x64/package.json b/npm/cli-linux-x64/package.json new file mode 100644 index 000000000..1a870bb4c --- /dev/null +++ b/npm/cli-linux-x64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-linux-x64", + "version": "0.0.0", + "description": "Linux x64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-linux-x64" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-win32-arm64/.npmrc b/npm/cli-win32-arm64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-win32-arm64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-win32-arm64/README.md b/npm/cli-win32-arm64/README.md new file mode 100644 index 000000000..80031d8a1 --- /dev/null +++ b/npm/cli-win32-arm64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-win32-arm64 + +This is the Windows ARM64 platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-win32-arm64/package.json b/npm/cli-win32-arm64/package.json new file mode 100644 index 000000000..4fcece13d --- /dev/null +++ b/npm/cli-win32-arm64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-win32-arm64", + "version": "0.0.0", + "description": "Windows arm64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-win32-arm64" + }, + "os": [ + "win32" + ], + "cpu": [ + "arm64" + ], + "bin": { + "kosli": "bin/kosli.exe" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli.exe || (echo 'ERROR: bin/kosli.exe is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/cli-win32-x64/.npmrc b/npm/cli-win32-x64/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/cli-win32-x64/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/cli-win32-x64/README.md b/npm/cli-win32-x64/README.md new file mode 100644 index 000000000..f8347a151 --- /dev/null +++ b/npm/cli-win32-x64/README.md @@ -0,0 +1,11 @@ +# @kosli/cli-win32-x64 + +This is the Windows x64 platform binary for the Kosli CLI. **Do not install this package directly.** + +Install the main package instead, which selects the right binary for your platform automatically: + +```sh +npm install -g @kosli/cli +``` + +See the [Kosli CLI repository](https://github.com/kosli-dev/cli) for documentation and source code. diff --git a/npm/cli-win32-x64/package.json b/npm/cli-win32-x64/package.json new file mode 100644 index 000000000..0c9fa4c41 --- /dev/null +++ b/npm/cli-win32-x64/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kosli/cli-win32-x64", + "version": "0.0.0", + "description": "Windows x64 binary for @kosli/cli", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/cli-win32-x64" + }, + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "bin": { + "kosli": "bin/kosli.exe" + }, + "files": [ + "bin/" + ], + "scripts": { + "prepack": "test -f bin/kosli.exe || (echo 'ERROR: bin/kosli.exe is missing' && exit 1)" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/npm/wrapper/.npmrc b/npm/wrapper/.npmrc new file mode 100644 index 000000000..ae643592e --- /dev/null +++ b/npm/wrapper/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/npm/wrapper/bin/kosli b/npm/wrapper/bin/kosli new file mode 100755 index 000000000..fb2ca85de --- /dev/null +++ b/npm/wrapper/bin/kosli @@ -0,0 +1,46 @@ +#!/usr/bin/env node +"use strict"; + +const { spawnSync } = require("child_process"); +const path = require("path"); + +const platform = process.platform; +const arch = process.arch; + +const SUPPORTED = { + linux: { x64: true, arm64: true, arm: true }, + darwin: { x64: true, arm64: true }, + win32: { x64: true, arm64: true }, +}; + +if (!SUPPORTED[platform] || !SUPPORTED[platform][arch]) { + process.stderr.write( + `[kosli] Error: ${platform}/${arch} is not a supported platform.\n` + + `[kosli] See https://github.com/kosli-dev/cli for supported platforms.\n` + ); + process.exit(1); +} + +const packageName = `@kosli/cli-${platform}-${arch}`; + +let binaryPath; +try { + const packageDir = path.dirname( + require.resolve(`${packageName}/package.json`) + ); + const binaryName = platform === "win32" ? "kosli.exe" : "kosli"; + binaryPath = path.join(packageDir, "bin", binaryName); +} catch (e) { + process.stderr.write( + `[kosli] Error: platform package ${packageName} is not installed.\n` + + `[kosli] Try reinstalling: npm install -g @kosli/cli\n` + ); + process.exit(1); +} + +const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: "inherit" }); +if (result.error) { + process.stderr.write(`[kosli] Error: failed to run binary: ${result.error.message}\n`); + process.exit(1); +} +process.exit(result.status ?? 1); diff --git a/npm/wrapper/install.js b/npm/wrapper/install.js new file mode 100644 index 000000000..2d73b865d --- /dev/null +++ b/npm/wrapper/install.js @@ -0,0 +1,62 @@ +"use strict"; + +// Postinstall script: validates that the platform binary was installed correctly. +// Runs after `npm install @kosli/cli`. + +const path = require("path"); +const fs = require("fs"); + +const SUPPORTED = { + linux: { x64: true, arm64: true, arm: true }, + darwin: { x64: true, arm64: true }, + win32: { x64: true, arm64: true }, +}; + +const platform = process.platform; +const arch = process.arch; + +if (!SUPPORTED[platform] || !SUPPORTED[platform][arch]) { + process.stderr.write( + `[kosli] Error: ${platform}/${arch} is not a supported platform.\n` + + `[kosli] See https://github.com/kosli-dev/cli for supported platforms.\n` + + `[kosli] Use --ignore-scripts to skip this check.\n` + ); + process.exit(1); +} + +const packageName = `@kosli/cli-${platform}-${arch}`; + +let binaryPath; +try { + const packageDir = path.dirname( + require.resolve(`${packageName}/package.json`) + ); + const binaryName = platform === "win32" ? "kosli.exe" : "kosli"; + binaryPath = path.join(packageDir, "bin", binaryName); +} catch (e) { + // Optional dependency was skipped (e.g. --no-optional). Warn but don't fail. + process.stderr.write( + `[kosli] Warning: ${packageName} is not installed.\n` + + `[kosli] The kosli binary will not be available.\n` + + `[kosli] Re-run without --no-optional to fix this.\n` + ); + process.exit(0); +} + +if (!fs.existsSync(binaryPath)) { + process.stderr.write( + `[kosli] Error: binary not found at ${binaryPath}\n` + + `[kosli] Try reinstalling: npm install -g @kosli/cli\n` + ); + process.exit(1); +} + +try { + fs.accessSync(binaryPath, fs.constants.X_OK); +} catch (e) { + process.stderr.write( + `[kosli] Error: binary is not executable: ${e.message}\n` + + `[kosli] Try reinstalling: npm install -g @kosli/cli\n` + ); + process.exit(1); +} diff --git a/npm/wrapper/package.json b/npm/wrapper/package.json new file mode 100644 index 000000000..b4a18bae4 --- /dev/null +++ b/npm/wrapper/package.json @@ -0,0 +1,50 @@ +{ + "name": "@kosli/cli", + "version": "0.0.0", + "description": "CLI client for reporting compliance events to https://kosli.com", + "license": "MIT", + "author": "Kosli Inc. ", + "homepage": "https://kosli.com", + "repository": { + "type": "git", + "url": "git+https://github.com/kosli-dev/cli.git", + "directory": "npm/wrapper" + }, + "bugs": { + "url": "https://github.com/kosli-dev/cli/issues" + }, + "keywords": [ + "cli", + "kosli", + "compliance", + "supply-chain", + "devops" + ], + "engines": { + "node": ">=18" + }, + "bin": { + "kosli": "bin/kosli" + }, + "files": [ + "bin/", + "install.js" + ], + "scripts": { + "prepack": "test -f bin/kosli || (echo 'ERROR: bin/kosli is missing' && exit 1) && test -f install.js || (echo 'ERROR: install.js is missing' && exit 1)", + "postinstall": "node install.js" + }, + "optionalDependencies": { + "@kosli/cli-darwin-arm64": "0.0.0", + "@kosli/cli-darwin-x64": "0.0.0", + "@kosli/cli-linux-arm": "0.0.0", + "@kosli/cli-linux-arm64": "0.0.0", + "@kosli/cli-linux-x64": "0.0.0", + "@kosli/cli-win32-arm64": "0.0.0", + "@kosli/cli-win32-x64": "0.0.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + } +} diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh new file mode 100755 index 000000000..4f6d932e0 --- /dev/null +++ b/scripts/npm-publish.sh @@ -0,0 +1,88 @@ +#!/bin/bash +set -e + +VERSION="$1" +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + +# When called from goreleaser, $2 is "--dry-run" if snapshot build +DRY_RUN=false +if [ "$2" = "--dry-run" ]; then + echo "Running in DRY-RUN mode. Packages will be created but not published." + DRY_RUN=true +fi + +# Regex for stable: X.Y.Z (where X, Y, Z are numbers) +STABLE_REGEX="^[0-9]+\.[0-9]+\.[0-9]+$" + +# Regex for pre-release: X.Y.Z-TAG (where TAG starts with a hyphen) +PRE_REGEX="^[0-9]+\.[0-9]+\.[0-9]+-.*$" + +# Determine npm dist-tag: pre-release versions must not go to "latest" +if [[ $VERSION =~ $STABLE_REGEX ]]; then + echo "✅ '$VERSION' is a STABLE release." + NPM_TAG="latest" +elif [[ $VERSION =~ $PRE_REGEX ]]; then + echo "🧪 '$VERSION' is a PRE-RELEASE version." + NPM_TAG="snapshot" +else + echo "❌ '$VERSION' is not a valid SemVer format." + exit 1 +fi + +# Inject version into all platform package.json files +while IFS= read -r f; do + tmp="$(mktemp)" + jq --arg v "$VERSION" '.version = $v' "$f" > "$tmp" && mv "$tmp" "$f" || { rm -f "$tmp"; exit 1; } +done < <(find npm -name package.json ! -path '*/node_modules/*') + +# Also update the optionalDependencies version references in the wrapper +tmp="$(mktemp)" +jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | with_entries(.value = $v))' \ + npm/wrapper/package.json > "$tmp" && mv "$tmp" npm/wrapper/package.json || { rm -f "$tmp"; exit 1; } + +# Build ordered package list: platform packages first, wrapper last +PACKAGES=() +while IFS= read -r f; do + PACKAGES+=("$(dirname "$f")") +done < <(find npm -name package.json ! -path "npm/wrapper/*" | sort) +PACKAGES+=("npm/wrapper") + +# Phase 1: pack all packages — exit immediately on any failure +for PKG_DIR in "${PACKAGES[@]}"; do + PKG_NAME="$(basename "$PKG_DIR")" + echo "Packing ${PKG_NAME}..." + (cd "$PKG_DIR" && npm pack) || { echo "❌ Failed to pack ${PKG_NAME}"; exit 1; } +done + +# Phase 2: publish all packages if not a dry run — exit immediately on any failure +npm_publish_with_retry() { + local pkg_dir="$1" + local tag="$2" + local max_attempts=3 + local delay=5 + + for attempt in $(seq 1 "$max_attempts"); do + local provenance_flag="" + [ "${GITHUB_ACTIONS:-false}" = "true" ] && provenance_flag="--provenance" + if (cd "$pkg_dir" && npm publish --tag "$tag" $provenance_flag); then + return 0 + fi + if [ "$attempt" -lt "$max_attempts" ]; then + echo "⚠️ Attempt ${attempt}/${max_attempts} failed. Retrying in ${delay}s..." + sleep "$delay" + delay=$(( delay * 2 )) + fi + done + return 1 +} + +if [ "$DRY_RUN" = false ]; then + for PKG_DIR in "${PACKAGES[@]}"; do + PKG_NAME="$(basename "$PKG_DIR")" + echo "Publishing ${PKG_NAME}..." + npm_publish_with_retry "$PKG_DIR" "$NPM_TAG" || { echo "❌ Failed to publish ${PKG_NAME} after retries"; exit 1; } + done +fi