From 33553667ec3b14f564ec59f606f90545dfc203a3 Mon Sep 17 00:00:00 2001 From: khooihongzhe Date: Mon, 9 Mar 2026 15:09:12 +0800 Subject: [PATCH 1/5] feat: add npm distribution with platform-specific packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Distribute the CoinGecko CLI via npm using the industry-standard platform-specific optional dependencies pattern (like esbuild/turbo). - @coingecko/cg umbrella package with JS bin wrapper - 6 platform packages: darwin/linux/win32 × arm64/x64 - Publish script with checksum verification, retry safety, and provenance - Smoke test runs npm pack + install + cg version before publishing - Release workflow updated with setup-node + npm publish step Users install with: npm install -g @coingecko/cg Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 18 ++++ .gitignore | 4 + npm/cg-darwin-arm64/package.json | 13 +++ npm/cg-darwin-x64/package.json | 13 +++ npm/cg-linux-arm64/package.json | 13 +++ npm/cg-linux-x64/package.json | 13 +++ npm/cg-win32-arm64/package.json | 13 +++ npm/cg-win32-x64/package.json | 13 +++ scripts/npm-publish.sh | 165 +++++++++++++++++++++++++++++++ scripts/npm-smoke-test.sh | 112 +++++++++++++++++++++ 10 files changed, 377 insertions(+) create mode 100644 npm/cg-darwin-arm64/package.json create mode 100644 npm/cg-darwin-x64/package.json create mode 100644 npm/cg-linux-arm64/package.json create mode 100644 npm/cg-linux-x64/package.json create mode 100644 npm/cg-win32-arm64/package.json create mode 100644 npm/cg-win32-x64/package.json create mode 100755 scripts/npm-publish.sh create mode 100755 scripts/npm-smoke-test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee4b9ad..7c60256 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: permissions: contents: write + id-token: write jobs: release: @@ -25,3 +26,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Smoke test npm packages + run: | + VERSION="${GITHUB_REF_NAME#v}" + VERSION="$VERSION" scripts/npm-smoke-test.sh + + - name: Publish to npm + run: | + VERSION="${GITHUB_REF_NAME#v}" + VERSION="$VERSION" scripts/npm-publish.sh + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5df1fe6..8360d26 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,10 @@ Thumbs.db # Build dist/ +# npm platform binaries (copied during CI publish) +npm/cg-*/cg +npm/cg-*/cg.exe + # Secrets .env .env.* diff --git a/npm/cg-darwin-arm64/package.json b/npm/cg-darwin-arm64/package.json new file mode 100644 index 0000000..a50fe14 --- /dev/null +++ b/npm/cg-darwin-arm64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-darwin-arm64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (macOS ARM64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["darwin"], + "cpu": ["arm64"], + "files": ["cg"] +} diff --git a/npm/cg-darwin-x64/package.json b/npm/cg-darwin-x64/package.json new file mode 100644 index 0000000..1732ac8 --- /dev/null +++ b/npm/cg-darwin-x64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-darwin-x64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (macOS x64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["darwin"], + "cpu": ["x64"], + "files": ["cg"] +} diff --git a/npm/cg-linux-arm64/package.json b/npm/cg-linux-arm64/package.json new file mode 100644 index 0000000..5c88ebb --- /dev/null +++ b/npm/cg-linux-arm64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-linux-arm64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (Linux ARM64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["linux"], + "cpu": ["arm64"], + "files": ["cg"] +} diff --git a/npm/cg-linux-x64/package.json b/npm/cg-linux-x64/package.json new file mode 100644 index 0000000..bd8e7a7 --- /dev/null +++ b/npm/cg-linux-x64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-linux-x64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (Linux x64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["linux"], + "cpu": ["x64"], + "files": ["cg"] +} diff --git a/npm/cg-win32-arm64/package.json b/npm/cg-win32-arm64/package.json new file mode 100644 index 0000000..74d0b13 --- /dev/null +++ b/npm/cg-win32-arm64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-win32-arm64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (Windows ARM64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["win32"], + "cpu": ["arm64"], + "files": ["cg.exe"] +} diff --git a/npm/cg-win32-x64/package.json b/npm/cg-win32-x64/package.json new file mode 100644 index 0000000..c998108 --- /dev/null +++ b/npm/cg-win32-x64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@coingecko/cg-win32-x64", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data (Windows x64)", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "os": ["win32"], + "cpu": ["x64"], + "files": ["cg.exe"] +} diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh new file mode 100755 index 0000000..e7c2b1a --- /dev/null +++ b/scripts/npm-publish.sh @@ -0,0 +1,165 @@ +#!/bin/sh +set -eu + +# Publish CoinGecko CLI to npm as platform-specific packages. +# Called from CI after goreleaser has built the archives. +# +# Usage: VERSION=1.2.3 scripts/npm-publish.sh +# +# Expects: +# - goreleaser dist/ directory with archives and checksums.txt +# - NPM_TOKEN environment variable (or .npmrc already configured) +# +# Safety: +# - Verifies archive checksums against goreleaser's checksums.txt before extracting +# - Aborts if any platform archive is missing (won't publish broken umbrella) +# - Skips already-published versions for retry safety after partial failures +# - Uses node to rewrite package.json versions (works regardless of current value) +# - Publishes with --provenance for supply-chain verification (requires id-token: write) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +NPM_DIR="$ROOT_DIR/npm" +DIST_DIR="$ROOT_DIR/dist" + +if [ -z "${VERSION:-}" ]; then + echo "Error: VERSION environment variable is required" + exit 1 +fi + +echo "Publishing @coingecko/cg v${VERSION} to npm" + +# Mapping: goreleaser os_arch -> npm package directory + binary name +# darwin_arm64 -> cg-darwin-arm64/cg +# darwin_amd64 -> cg-darwin-x64/cg +# linux_arm64 -> cg-linux-arm64/cg +# linux_amd64 -> cg-linux-x64/cg +# windows_arm64 -> cg-win32-arm64/cg.exe +# windows_amd64 -> cg-win32-x64/cg.exe + +# set_version [dependency_version] +# Rewrites version (and optionalDependencies versions) in a package.json. +set_version() { + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('$1', 'utf8')); + pkg.version = '$2'; + if ('$3' !== '' && pkg.optionalDependencies) { + for (const k of Object.keys(pkg.optionalDependencies)) { + pkg.optionalDependencies[k] = '$3'; + } + } + fs.writeFileSync('$1', JSON.stringify(pkg, null, 2) + '\n'); + " +} + +# is_published +# Returns 0 if the exact version is already on the registry. +is_published() { + npm view "$1@$2" version >/dev/null 2>&1 +} + +# verify_checksum +# Verifies a file against goreleaser's checksums.txt. Aborts on mismatch. +verify_checksum() { + local file="$1" + local filename + filename=$(basename "$file") + local expected + expected=$(grep " ${filename}$" "${DIST_DIR}/checksums.txt" | awk '{print $1}') + if [ -z "$expected" ]; then + echo "Error: no checksum entry for ${filename} in checksums.txt" + exit 1 + fi + local actual + if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$file" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$file" | awk '{print $1}') + else + echo "Error: no sha256sum or shasum found" + exit 1 + fi + if [ "$expected" != "$actual" ]; then + echo "Error: checksum mismatch for ${filename}" + echo " Expected: ${expected}" + echo " Actual: ${actual}" + exit 1 + fi +} + +# Phase 1: Verify all platform archives exist and match checksums +echo "Verifying platform archives..." +CHECKSUMS_FILE="${DIST_DIR}/checksums.txt" +if [ ! -f "$CHECKSUMS_FILE" ]; then + echo "Error: ${CHECKSUMS_FILE} not found. Cannot verify archive integrity." + exit 1 +fi +PLATFORMS="darwin:arm64:cg-darwin-arm64:cg:tar.gz +darwin:amd64:cg-darwin-x64:cg:tar.gz +linux:arm64:cg-linux-arm64:cg:tar.gz +linux:amd64:cg-linux-x64:cg:tar.gz +windows:arm64:cg-win32-arm64:cg.exe:zip +windows:amd64:cg-win32-x64:cg.exe:zip" + +MISSING="" +echo "$PLATFORMS" | while IFS=: read -r goos goarch npm_dir binary ext; do + archive="${DIST_DIR}/cg_${VERSION}_${goos}_${goarch}.${ext}" + if [ ! -f "$archive" ]; then + echo " MISSING: ${archive}" + touch "${DIST_DIR}/.npm-missing" + else + verify_checksum "$archive" + fi +done + +if [ -f "${DIST_DIR}/.npm-missing" ]; then + rm -f "${DIST_DIR}/.npm-missing" + echo "Error: one or more platform archives are missing. Aborting npm publish." + exit 1 +fi +echo " All archives present and checksums verified." + +# Phase 2: Extract, version-stamp, and publish each platform package +echo "$PLATFORMS" | while IFS=: read -r goos goarch npm_dir binary ext; do + pkg_name="@coingecko/${npm_dir}" + pkg_dir="${NPM_DIR}/${npm_dir}" + archive="cg_${VERSION}_${goos}_${goarch}.${ext}" + archive_path="${DIST_DIR}/${archive}" + + if is_published "$pkg_name" "$VERSION"; then + echo " ${pkg_name}@${VERSION} already published, skipping" + continue + fi + + echo " Extracting ${archive}..." + tmpdir=$(mktemp -d) + if [ "$ext" = "tar.gz" ]; then + tar -xzf "$archive_path" -C "$tmpdir" + else + unzip -q "$archive_path" -d "$tmpdir" + fi + + cp "${tmpdir}/${binary}" "${pkg_dir}/${binary}" + chmod +x "${pkg_dir}/${binary}" + rm -rf "$tmpdir" + + set_version "${pkg_dir}/package.json" "$VERSION" "" + + echo " Publishing ${pkg_name}@${VERSION}..." + npm publish "${pkg_dir}" --access public --provenance +done + +# Phase 3: Version-stamp and publish the umbrella package +UMBRELLA_DIR="${NPM_DIR}/cg" + +if is_published "@coingecko/cg" "$VERSION"; then + echo " @coingecko/cg@${VERSION} already published, skipping" +else + set_version "${UMBRELLA_DIR}/package.json" "$VERSION" "$VERSION" + + echo " Publishing @coingecko/cg@${VERSION}..." + npm publish "${UMBRELLA_DIR}" --access public --provenance +fi + +echo "Done! Published @coingecko/cg@${VERSION} and all platform packages." diff --git a/scripts/npm-smoke-test.sh b/scripts/npm-smoke-test.sh new file mode 100755 index 0000000..f748851 --- /dev/null +++ b/scripts/npm-smoke-test.sh @@ -0,0 +1,112 @@ +#!/bin/sh +set -eu + +# End-to-end smoke test for the npm wrapper package. +# Runs after goreleaser and before npm publish to catch packaging issues early. +# +# Usage: VERSION=1.2.3 scripts/npm-smoke-test.sh +# +# Tests: +# 1. Extracts the runner's platform binary into its npm package dir +# 2. npm pack both the platform package and the umbrella +# 3. npm install from the tarballs into a temp project +# 4. Invokes "cg version" via the wrapper and verifies output + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +NPM_DIR="$ROOT_DIR/npm" +DIST_DIR="$ROOT_DIR/dist" + +if [ -z "${VERSION:-}" ]; then + echo "Error: VERSION environment variable is required" + exit 1 +fi + +echo "=== npm smoke test v${VERSION} ===" + +# Detect the current runner's platform to pick the right package +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +case "$OS" in + linux) NPM_OS="linux" ;; + darwin) NPM_OS="darwin" ;; + *) echo "Unsupported smoke test OS: $OS"; exit 1 ;; +esac + +case "$ARCH" in + x86_64|amd64) NPM_CPU="x64"; GO_ARCH="amd64" ;; + arm64|aarch64) NPM_CPU="arm64"; GO_ARCH="arm64" ;; + *) echo "Unsupported smoke test arch: $ARCH"; exit 1 ;; +esac + +PLATFORM_PKG="cg-${NPM_OS}-${NPM_CPU}" +PLATFORM_DIR="${NPM_DIR}/${PLATFORM_PKG}" +ARCHIVE="cg_${VERSION}_${OS}_${GO_ARCH}.tar.gz" +ARCHIVE_PATH="${DIST_DIR}/${ARCHIVE}" + +if [ ! -f "$ARCHIVE_PATH" ]; then + echo "Error: archive not found: ${ARCHIVE_PATH}" + exit 1 +fi + +# Step 1: Extract binary into platform package +echo " Extracting ${ARCHIVE} into ${PLATFORM_PKG}/" +tmpdir=$(mktemp -d) +tar -xzf "$ARCHIVE_PATH" -C "$tmpdir" +cp "${tmpdir}/cg" "${PLATFORM_DIR}/cg" +chmod +x "${PLATFORM_DIR}/cg" +rm -rf "$tmpdir" + +# Stamp versions for packing +node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('${PLATFORM_DIR}/package.json', 'utf8')); + pkg.version = '${VERSION}'; + fs.writeFileSync('${PLATFORM_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\n'); +" +node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('${NPM_DIR}/cg/package.json', 'utf8')); + pkg.version = '${VERSION}'; + for (const k of Object.keys(pkg.optionalDependencies || {})) { + pkg.optionalDependencies[k] = '${VERSION}'; + } + fs.writeFileSync('${NPM_DIR}/cg/package.json', JSON.stringify(pkg, null, 2) + '\n'); +" + +# Step 2: npm pack both packages +PACK_DIR=$(mktemp -d) + +echo " Packing platform package..." +PLATFORM_TGZ=$(npm pack "${PLATFORM_DIR}" --pack-destination "$PACK_DIR" 2>/dev/null | tail -1) + +echo " Packing umbrella package..." +UMBRELLA_TGZ=$(npm pack "${NPM_DIR}/cg" --pack-destination "$PACK_DIR" 2>/dev/null | tail -1) + +# Step 3: Install from tarballs into an isolated temp project +echo " Installing from tarballs..." +TEST_DIR=$(mktemp -d) +cd "$TEST_DIR" +npm init -y >/dev/null 2>&1 +npm install "${PACK_DIR}/${PLATFORM_TGZ}" "${PACK_DIR}/${UMBRELLA_TGZ}" >/dev/null 2>&1 + +# Step 4: Invoke the wrapper and check output +echo " Running: npx cg version" +OUTPUT=$(npx cg version 2>&1) || true + +if echo "$OUTPUT" | grep -qi "coingecko\|cg\|${VERSION}"; then + echo " Smoke test passed: ${OUTPUT}" +else + echo " Error: unexpected output from cg version:" + echo " ${OUTPUT}" + rm -rf "$PACK_DIR" "$TEST_DIR" + exit 1 +fi + +# Cleanup +rm -rf "$PACK_DIR" "$TEST_DIR" +# Remove binary extracted for testing (CI will re-extract during publish) +rm -f "${PLATFORM_DIR}/cg" + +echo "=== smoke test passed ===" From 6b02243c797c23abb641576254caf1f3b636f429 Mon Sep 17 00:00:00 2001 From: khooihongzhe Date: Mon, 9 Mar 2026 15:17:18 +0800 Subject: [PATCH 2/5] fix: include umbrella npm package and fix gitignore pattern The npm/cg/ directory was excluded by the overly broad `cg` gitignore pattern. Changed to `/cg` to only match the root-level binary. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- npm/cg/bin/cg.js | 56 +++++++++++++++++++++++++++++++++++++++++++++ npm/cg/package.json | 34 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 npm/cg/bin/cg.js create mode 100644 npm/cg/package.json diff --git a/.gitignore b/.gitignore index 8360d26..d2e8af3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Binaries -cg +/cg *.exe *.exe~ *.dll diff --git a/npm/cg/bin/cg.js b/npm/cg/bin/cg.js new file mode 100644 index 0000000..242b1f3 --- /dev/null +++ b/npm/cg/bin/cg.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +"use strict"; + +const { execFileSync } = require("child_process"); +const path = require("path"); +const os = require("os"); + +// Map Node.js platform/arch to npm package names +const PLATFORMS = { + "darwin arm64": "@coingecko/cg-darwin-arm64", + "darwin x64": "@coingecko/cg-darwin-x64", + "linux arm64": "@coingecko/cg-linux-arm64", + "linux x64": "@coingecko/cg-linux-x64", + "win32 arm64": "@coingecko/cg-win32-arm64", + "win32 x64": "@coingecko/cg-win32-x64", +}; + +function getBinaryPath() { + const platform = os.platform(); + const arch = os.arch(); + const key = `${platform} ${arch}`; + const pkg = PLATFORMS[key]; + + if (!pkg) { + throw new Error( + `Unsupported platform: ${platform} ${arch}. ` + + `Supported: ${Object.keys(PLATFORMS).join(", ")}` + ); + } + + const binary = platform === "win32" ? "cg.exe" : "cg"; + + try { + // resolve the platform package from this package's location + const pkgDir = path.dirname(require.resolve(`${pkg}/package.json`)); + return path.join(pkgDir, binary); + } catch { + throw new Error( + `The platform package ${pkg} is not installed. ` + + `This usually means your package manager excluded optional dependencies. ` + + `Try reinstalling with: npm install @coingecko/cg` + ); + } +} + +const binary = getBinaryPath(); + +try { + execFileSync(binary, process.argv.slice(2), { stdio: "inherit" }); +} catch (err) { + if (err.status !== undefined) { + process.exit(err.status); + } + throw err; +} diff --git a/npm/cg/package.json b/npm/cg/package.json new file mode 100644 index 0000000..58e6346 --- /dev/null +++ b/npm/cg/package.json @@ -0,0 +1,34 @@ +{ + "name": "@coingecko/cg", + "version": "0.0.0", + "description": "CoinGecko CLI - Real Time & Historical Crypto Data", + "repository": { + "type": "git", + "url": "https://github.com/coingecko/coingecko-cli.git" + }, + "license": "MIT", + "bin": { + "cg": "bin/cg.js" + }, + "files": [ + "bin/cg.js" + ], + "optionalDependencies": { + "@coingecko/cg-darwin-arm64": "0.0.0", + "@coingecko/cg-darwin-x64": "0.0.0", + "@coingecko/cg-linux-arm64": "0.0.0", + "@coingecko/cg-linux-x64": "0.0.0", + "@coingecko/cg-win32-arm64": "0.0.0", + "@coingecko/cg-win32-x64": "0.0.0" + }, + "engines": { + "node": ">=16" + }, + "keywords": [ + "coingecko", + "crypto", + "cryptocurrency", + "bitcoin", + "cli" + ] +} From 94553c6696ae69f634065bc7ad9ad4ab5c9906fa Mon Sep 17 00:00:00 2001 From: khooihongzhe Date: Mon, 9 Mar 2026 17:44:56 +0800 Subject: [PATCH 3/5] fix: address PR review feedback for npm distribution - Bump node engine requirement from >=16 to >=20 (LTS) - Remove shasum fallback, require sha256sum only - Add comment that provenance requires GitHub-hosted public runners Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 2 ++ npm/cg/package.json | 2 +- scripts/npm-publish.sh | 9 +++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c60256..a547765 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,8 @@ permissions: jobs: release: + # MUST use GitHub-hosted public runners — npm --provenance requires OIDC id-token + # from a trusted CI environment (GitHub Actions on public runners). runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/npm/cg/package.json b/npm/cg/package.json index 58e6346..7997624 100644 --- a/npm/cg/package.json +++ b/npm/cg/package.json @@ -22,7 +22,7 @@ "@coingecko/cg-win32-x64": "0.0.0" }, "engines": { - "node": ">=16" + "node": ">=20" }, "keywords": [ "coingecko", diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh index e7c2b1a..5ccef46 100755 --- a/scripts/npm-publish.sh +++ b/scripts/npm-publish.sh @@ -72,14 +72,11 @@ verify_checksum() { exit 1 fi local actual - if command -v sha256sum >/dev/null 2>&1; then - actual=$(sha256sum "$file" | awk '{print $1}') - elif command -v shasum >/dev/null 2>&1; then - actual=$(shasum -a 256 "$file" | awk '{print $1}') - else - echo "Error: no sha256sum or shasum found" + if ! command -v sha256sum >/dev/null 2>&1; then + echo "Error: sha256sum not found" exit 1 fi + actual=$(sha256sum "$file" | awk '{print $1}') if [ "$expected" != "$actual" ]; then echo "Error: checksum mismatch for ${filename}" echo " Expected: ${expected}" From d99b2cd55390c5883906d7a9e276c8712c157793 Mon Sep 17 00:00:00 2001 From: khooihongzhe Date: Mon, 11 May 2026 14:52:41 +0800 Subject: [PATCH 4/5] chore: ship LICENSE, README, and metadata in npm packages npm tarballs are immutable once published, so missing files would persist in the first release forever. - Bundle LICENSE in umbrella and all 6 platform packages - Copy main README into umbrella at publish/smoke-test time so the npmjs.com page mirrors the GitHub README - Document the npm install path in the project README - Add homepage and bugs.url to all 7 package.json files --- .gitignore | 3 +++ README.md | 8 ++++++++ npm/cg-darwin-arm64/LICENSE | 21 +++++++++++++++++++++ npm/cg-darwin-arm64/package.json | 4 ++++ npm/cg-darwin-x64/LICENSE | 21 +++++++++++++++++++++ npm/cg-darwin-x64/package.json | 4 ++++ npm/cg-linux-arm64/LICENSE | 21 +++++++++++++++++++++ npm/cg-linux-arm64/package.json | 4 ++++ npm/cg-linux-x64/LICENSE | 21 +++++++++++++++++++++ npm/cg-linux-x64/package.json | 4 ++++ npm/cg-win32-arm64/LICENSE | 21 +++++++++++++++++++++ npm/cg-win32-arm64/package.json | 4 ++++ npm/cg-win32-x64/LICENSE | 21 +++++++++++++++++++++ npm/cg-win32-x64/package.json | 4 ++++ npm/cg/LICENSE | 21 +++++++++++++++++++++ npm/cg/package.json | 4 ++++ scripts/npm-publish.sh | 3 +++ scripts/npm-smoke-test.sh | 6 ++++++ 18 files changed, 195 insertions(+) create mode 100644 npm/cg-darwin-arm64/LICENSE create mode 100644 npm/cg-darwin-x64/LICENSE create mode 100644 npm/cg-linux-arm64/LICENSE create mode 100644 npm/cg-linux-x64/LICENSE create mode 100644 npm/cg-win32-arm64/LICENSE create mode 100644 npm/cg-win32-x64/LICENSE create mode 100644 npm/cg/LICENSE diff --git a/.gitignore b/.gitignore index d2e8af3..aaed867 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ dist/ npm/cg-*/cg npm/cg-*/cg.exe +# npm umbrella README (copied from root during CI publish) +npm/cg/README.md + # Secrets .env .env.* diff --git a/README.md b/README.md index b451e5d..463fc9b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,14 @@ brew tap coingecko/coingecko-cli brew install cg ``` +### npm + +```sh +npm install -g @coingecko/cg +``` + +Works on macOS, Linux, and Windows (x64 and arm64). Requires Node.js >= 20. The right native binary for your platform is fetched via npm `optionalDependencies`; no build step. + ### Shell script ```sh diff --git a/npm/cg-darwin-arm64/LICENSE b/npm/cg-darwin-arm64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-darwin-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-darwin-arm64/package.json b/npm/cg-darwin-arm64/package.json index a50fe14..4f24547 100644 --- a/npm/cg-darwin-arm64/package.json +++ b/npm/cg-darwin-arm64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["darwin"], "cpu": ["arm64"], diff --git a/npm/cg-darwin-x64/LICENSE b/npm/cg-darwin-x64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-darwin-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-darwin-x64/package.json b/npm/cg-darwin-x64/package.json index 1732ac8..aec1151 100644 --- a/npm/cg-darwin-x64/package.json +++ b/npm/cg-darwin-x64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["darwin"], "cpu": ["x64"], diff --git a/npm/cg-linux-arm64/LICENSE b/npm/cg-linux-arm64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-linux-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-linux-arm64/package.json b/npm/cg-linux-arm64/package.json index 5c88ebb..a3d3a21 100644 --- a/npm/cg-linux-arm64/package.json +++ b/npm/cg-linux-arm64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["linux"], "cpu": ["arm64"], diff --git a/npm/cg-linux-x64/LICENSE b/npm/cg-linux-x64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-linux-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-linux-x64/package.json b/npm/cg-linux-x64/package.json index bd8e7a7..27e7bee 100644 --- a/npm/cg-linux-x64/package.json +++ b/npm/cg-linux-x64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["linux"], "cpu": ["x64"], diff --git a/npm/cg-win32-arm64/LICENSE b/npm/cg-win32-arm64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-win32-arm64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-win32-arm64/package.json b/npm/cg-win32-arm64/package.json index 74d0b13..73c069f 100644 --- a/npm/cg-win32-arm64/package.json +++ b/npm/cg-win32-arm64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["win32"], "cpu": ["arm64"], diff --git a/npm/cg-win32-x64/LICENSE b/npm/cg-win32-x64/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg-win32-x64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg-win32-x64/package.json b/npm/cg-win32-x64/package.json index c998108..c3a5c31 100644 --- a/npm/cg-win32-x64/package.json +++ b/npm/cg-win32-x64/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "os": ["win32"], "cpu": ["x64"], diff --git a/npm/cg/LICENSE b/npm/cg/LICENSE new file mode 100644 index 0000000..bc3159c --- /dev/null +++ b/npm/cg/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 Gecko Labs Pte. Ltd. and/or its affiliates. All Rights Reserved. + +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. \ No newline at end of file diff --git a/npm/cg/package.json b/npm/cg/package.json index 7997624..9159c37 100644 --- a/npm/cg/package.json +++ b/npm/cg/package.json @@ -6,6 +6,10 @@ "type": "git", "url": "https://github.com/coingecko/coingecko-cli.git" }, + "homepage": "https://github.com/coingecko/coingecko-cli", + "bugs": { + "url": "https://github.com/coingecko/coingecko-cli/issues" + }, "license": "MIT", "bin": { "cg": "bin/cg.js" diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh index 5ccef46..1f4205d 100755 --- a/scripts/npm-publish.sh +++ b/scripts/npm-publish.sh @@ -155,6 +155,9 @@ if is_published "@coingecko/cg" "$VERSION"; then else set_version "${UMBRELLA_DIR}/package.json" "$VERSION" "$VERSION" + # Copy the project README so npmjs.com shows the same content as GitHub. + cp "${ROOT_DIR}/README.md" "${UMBRELLA_DIR}/README.md" + echo " Publishing @coingecko/cg@${VERSION}..." npm publish "${UMBRELLA_DIR}" --access public --provenance fi diff --git a/scripts/npm-smoke-test.sh b/scripts/npm-smoke-test.sh index f748851..1b96561 100755 --- a/scripts/npm-smoke-test.sh +++ b/scripts/npm-smoke-test.sh @@ -78,6 +78,10 @@ node -e " # Step 2: npm pack both packages PACK_DIR=$(mktemp -d) +# Copy the project README into the umbrella so the packed tarball matches +# what gets published. +cp "${ROOT_DIR}/README.md" "${NPM_DIR}/cg/README.md" + echo " Packing platform package..." PLATFORM_TGZ=$(npm pack "${PLATFORM_DIR}" --pack-destination "$PACK_DIR" 2>/dev/null | tail -1) @@ -108,5 +112,7 @@ fi rm -rf "$PACK_DIR" "$TEST_DIR" # Remove binary extracted for testing (CI will re-extract during publish) rm -f "${PLATFORM_DIR}/cg" +# Remove the copied README (regenerated on each run) +rm -f "${NPM_DIR}/cg/README.md" echo "=== smoke test passed ===" From 628c9b8e6f9c74001e3c804be88022137b174cbe Mon Sep 17 00:00:00 2001 From: khooihongzhe Date: Mon, 11 May 2026 14:55:56 +0800 Subject: [PATCH 5/5] refactor(scripts): consolidate npm script cleanup with trap Smoke test had three separate cleanup paths (happy-path rm, error-branch rm, and an unhandled abort case). Replace with a single trap on EXIT so the umbrella README, extracted binary, and temp dirs are always removed. Publish script: add a trap to remove the copied README after the umbrella publish completes, since local runs would otherwise leave it behind. --- scripts/npm-publish.sh | 1 + scripts/npm-smoke-test.sh | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh index 1f4205d..5fc5f7f 100755 --- a/scripts/npm-publish.sh +++ b/scripts/npm-publish.sh @@ -157,6 +157,7 @@ else # Copy the project README so npmjs.com shows the same content as GitHub. cp "${ROOT_DIR}/README.md" "${UMBRELLA_DIR}/README.md" + trap 'rm -f "${UMBRELLA_DIR}/README.md"' EXIT echo " Publishing @coingecko/cg@${VERSION}..." npm publish "${UMBRELLA_DIR}" --access public --provenance diff --git a/scripts/npm-smoke-test.sh b/scripts/npm-smoke-test.sh index 1b96561..aa42c8b 100755 --- a/scripts/npm-smoke-test.sh +++ b/scripts/npm-smoke-test.sh @@ -50,6 +50,14 @@ if [ ! -f "$ARCHIVE_PATH" ]; then exit 1 fi +# Clean up test artifacts on any exit path (success, failure, set -e abort). +cleanup() { + [ -n "${PACK_DIR:-}" ] && rm -rf "$PACK_DIR" + [ -n "${TEST_DIR:-}" ] && rm -rf "$TEST_DIR" + rm -f "${NPM_DIR}/cg/README.md" "${PLATFORM_DIR}/cg" +} +trap cleanup EXIT + # Step 1: Extract binary into platform package echo " Extracting ${ARCHIVE} into ${PLATFORM_PKG}/" tmpdir=$(mktemp -d) @@ -104,15 +112,7 @@ if echo "$OUTPUT" | grep -qi "coingecko\|cg\|${VERSION}"; then else echo " Error: unexpected output from cg version:" echo " ${OUTPUT}" - rm -rf "$PACK_DIR" "$TEST_DIR" exit 1 fi -# Cleanup -rm -rf "$PACK_DIR" "$TEST_DIR" -# Remove binary extracted for testing (CI will re-extract during publish) -rm -f "${PLATFORM_DIR}/cg" -# Remove the copied README (regenerated on each run) -rm -f "${NPM_DIR}/cg/README.md" - echo "=== smoke test passed ==="