From f5cead7e8071522c945e03c99c5f2f84411fc627 Mon Sep 17 00:00:00 2001 From: dipto0321 Date: Thu, 2 Jul 2026 01:49:36 +0600 Subject: [PATCH] chore(release): ship npm wrapper and install-method guidance Adds nodeup-npm/, the pinned-version npm distribution channel that's the remaining piece of Phase 7 (issue #17). The wrapper is a thin downloader: scripts/check.js validates the OS/arch matrix at preinstall time, scripts/install.js downloads the static Go binary matching package.json's binaryVersion field at postinstall time. Pinned-version design (rather than fetching "latest") so the wrapper and the Go binary move together - bumped in the same PR, tested together, shipped together. Also reworks the Installation section of the README and docs/installation.md with a who-should-use-which matrix. Each channel now states the persona it fits (Homebrew user, Scoop user, npm-globals user, locked-down env, contributor) and the tradeoffs (especially the npm wrapper's Node-coupling and one-release lag). Co-Authored-By: Sonnet 4.6 --- README.md | 53 +++++++-- docs/installation.md | 125 +++++++++++++++++++- nodeup-npm/.npmignore | 42 +++++++ nodeup-npm/LICENSE | 21 ++++ nodeup-npm/README.md | 94 +++++++++++++++ nodeup-npm/package.json | 43 +++++++ nodeup-npm/scripts/check.js | 61 ++++++++++ nodeup-npm/scripts/install.js | 209 ++++++++++++++++++++++++++++++++++ 8 files changed, 635 insertions(+), 13 deletions(-) create mode 100644 nodeup-npm/.npmignore create mode 100644 nodeup-npm/LICENSE create mode 100644 nodeup-npm/README.md create mode 100644 nodeup-npm/package.json create mode 100644 nodeup-npm/scripts/check.js create mode 100644 nodeup-npm/scripts/install.js diff --git a/README.md b/README.md index 88cdb21..cf99bfc 100644 --- a/README.md +++ b/README.md @@ -63,30 +63,62 @@ A few things worth knowing before you run `nodeup`: ## Installation -### Homebrew (macOS, Linux) +Pick the channel that matches how you manage other tools. Use the +matrix below as the entry point — the per-channel sections below it +spell out exactly what to run. + +### Pick by who you are + +| If you… | Use | Why | +|---|---|---| +| Already use Homebrew for dev tooling on macOS / Linux | **Homebrew** | One command, auto-updates with `brew upgrade`, lives outside any Node install, uninstalls cleanly with `brew uninstall`. | +| Already use Scoop on Windows | **Scoop** | Same shape as Homebrew on the Windows side — `scoop update nodeup` keeps it current, no admin shell needed. | +| Already manage dev tools via `npm install -g` and want a one-liner | **npm wrapper** | No new package manager to set up; the install travels with the Node version it's installed against. Slight catch: a `nodeup` upgrade ships when the wrapper version bumps. | +| Are blocked from system package installs but can `npm i -g` | **npm wrapper** | Sandboxed / corp-locked-down machines often allow npm globals where they block system installers. | +| Maintain `nodeup` itself, or want a version pinned to a specific tag without any postinstall network step | **Direct binary download** | You choose the exact release; no installer, no auto-update, no Node coupling. Best for reproducible CI installs. | +| Are a Go developer hacking on `nodeup` and want the latest commit | **`go install`** | Pulls the current source and builds it locally. Fastest iteration loop for contributors. | + +### Channels + +#### Homebrew (macOS, Linux) ```bash brew install dipto0321/tap/nodeup ``` -### Scoop (Windows) +Best for: macOS / Linux users who already use Homebrew for tools. The +formula is auto-managed by GoReleaser from `brew: {}` in +`.goreleaser.yaml`, so a `v*.*.*` tag updates the tap. + +#### Scoop (Windows) ```powershell scoop bucket add dipto0321 https://github.com/dipto0321/scoop-bucket scoop install nodeup ``` -### npm wrapper (any platform) +Best for: Windows users who already use Scoop. The manifest is +auto-managed by GoReleaser from `scoop: {}` in `.goreleaser.yaml`. + +#### npm wrapper (any platform with Node ≥ 14) ```bash npm install -g nodeup ``` -The npm wrapper downloads the right binary for your platform at install time. +Best for: anyone who treats their global npm install as the source of +truth for CLI tools — typical in JavaScript / TypeScript projects. +The wrapper is a thin downloader (`scripts/install.js`) that fetches +the matching static Go binary from the GitHub release tagged by this +package's `binaryVersion` field. See +[`nodeup-npm/README.md`](./nodeup-npm/README.md) for the install +mechanics, updating behavior, and uninstall behavior. -### Direct binary download +#### Direct binary download -Grab the archive for your OS/arch from the [Releases page](https://github.com/dipto0321/nodeup/releases): +Best for: CI pipelines, lock-down environments, and anyone who wants +zero install ceremony. Grab the archive for your OS/arch from the +[Releases page](https://github.com/dipto0321/nodeup/releases): ```bash # macOS Apple Silicon @@ -96,14 +128,21 @@ sudo mv nodeup /usr/local/bin/ # Linux x86_64 curl -L https://github.com/dipto0321/nodeup/releases/latest/download/nodeup_*_linux_amd64.tar.gz | tar xz sudo mv nodeup /usr/local/bin/ + +# Windows (PowerShell) +curl -L https://github.com/dipto0321/nodeup/releases/latest/download/nodeup_$(curl -s https://api.github.com/repos/dipto0321/nodeup/releases/latest | grep tag_name | cut -d'"' -f4 | tr -d v)_windows_amd64.zip -OutFile nodeup.zip +Expand-Archive nodeup.zip -DestinationPath . ``` -### From source +#### From source ```bash go install github.com/dipto0321/nodeup/cmd/nodeup@latest ``` +Requires Go 1.22+. Best for: `nodeup` contributors and anyone who +wants to track `main` between releases. + ## Quickstart ```bash diff --git a/docs/installation.md b/docs/installation.md index 2fff138..b7fce2d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -13,6 +13,22 @@ automatically on every `v*.*.*` tag (see `.github/workflows/release.yml`). | Linux | arm64, amd64 | Most distros; glibc-based | | Windows | amd64 | nvm-windows supported | +## Pick a channel + +Each channel is a deliberate trade between **convenience**, **how +strongly you trust the package manager you already use**, and **how +much you want `nodeup` coupled to anything else on your system**. Use +the table below to pick — the per-channel sections below it show +exactly what to run. + +| Channel | One-line install | Auto-updates? | Lives in… | Best for… | Avoid if… | +|---|---|---|---|---|---| +| **Homebrew** | `brew install dipto0321/tap/nodeup` | Yes — `brew upgrade` | `/opt/homebrew/bin/` (macOS) or `/home/linuxbrew/.linuxbrew/bin/` (Linux) | macOS / Linux devs already using Homebrew for tooling | You don't have Homebrew and don't want to set it up just for one tool | +| **Scoop** | `scoop install nodeup` (after adding the bucket) | Yes — `scoop update` | `%USERPROFILE%\scoop\apps\nodeup\` | Windows devs already using Scoop | You prefer winget / Chocolatey — neither is wired up today | +| **npm wrapper** | `npm install -g nodeup` | Semi — `npm update -g nodeup` only when a wrapper version bumps | Inside a Node install, on your global `node_modules/` path | JS devs who already treat `npm i -g` as the source of truth for CLIs; locked-down machines that block system installers but allow npm globals | You want a CLI that lives completely outside any Node install, or you want to track Go-binary releases the moment they tag (the wrapper lags by one publish) | +| **Direct binary** | `curl … \| tar xz` | No — you re-run to update | Wherever you put the extracted binary | CI pipelines, reproducible installs, lock-down environments without npm or brew | You want auto-updates; you'll forget to re-run | +| **From source** | `go install ./cmd/nodeup@latest` | No — re-run against a new tag | `$GOBIN` or `$GOPATH/bin` | `nodeup` contributors and Go developers who want HEAD | Anyone who doesn't have Go installed and isn't trying to hack on the tool | + ## Channels ### Homebrew (macOS, Linux) @@ -21,6 +37,19 @@ automatically on every `v*.*.*` tag (see `.github/workflows/release.yml`). brew install dipto0321/tap/nodeup ``` +**Who this is for:** developers on macOS or Linux who already use +Homebrew for dev tooling (`brew` is the package manager for +command-line apps, distinct from the App Store). The tap +[`dipto0321/homebrew-tap`](https://github.com/dipto0321/homebrew-tap) +holds the formula and is auto-pushed by GoReleaser on every +`v*.*.*` tag. + +**Tradeoffs:** upgrades happen via `brew upgrade`, which all Homebrew +users already run on a schedule. The install lives in Homebrew's +prefix (typically `/opt/homebrew/bin` on Apple Silicon), completely +independent of any Node install — safe for `nvm`/`fnm`/`Volta` users +who don't want a Node coupling. + ### Scoop (Windows) ```powershell @@ -28,28 +57,112 @@ scoop bucket add dipto0321 https://github.com/dipto0321/scoop-bucket scoop install nodeup ``` -### npm wrapper +**Who this is for:** Windows developers already using Scoop as their +package manager. The bucket +[`dipto0321/scoop-bucket`](https://github.com/dipto0321/scoop-bucket) +holds the manifest and is auto-pushed by GoReleaser. + +**Tradeoffs:** user-level install (no admin shell), upgrades via +`scoop update nodeup`. Same Node-decoupled shape as Homebrew. + +### npm wrapper (any platform with Node ≥ 14) ```bash npm install -g nodeup ``` -### Direct binary +**Who this is for:** developers who already treat `npm install -g` as +the canonical way to install CLIs. Common in JavaScript-heavy +projects where the team's onboarding script already runs `npm i -g + …`. Also a good fit when system package installs are +blocked but npm globals are allowed. + +**Tradeoffs:** + +- **Slight version lag.** The wrapper pins to the Go-binary version + in its `binaryVersion` field. A new Go release needs a new wrapper + publish — `npm update -g nodeup` only gets you a new Go binary + after the wrapper version that pins to it ships. To jump to a + brand-new release ahead of that, use a direct-binary install. +- **Coupled to a Node install.** The wrapper and binary live inside + a Node install (the one you ran `npm i -g` against). For `nvm` / + `fnm` / `Volta` users this means re-installing the wrapper after + every Node bump — exactly what `nodeup` is supposed to remove. + For those users, prefer Homebrew / Scoop / direct-binary. +- **No Node runtime needed at runtime.** The wrapper's + `postinstall` script downloads a static Go binary; the `node` + runtime is used only during install (for the script itself) and + is never invoked by `nodeup` once installed. + +See [`nodeup-npm/README.md`](../nodeup-npm/README.md) for the full +install / update / uninstall flow. + +### Direct binary download + +Best for CI, locked-down environments, and anyone who wants zero +install ceremony. Grab the archive for your OS/arch from the +[Releases page](https://github.com/dipto0321/nodeup/releases): -See the [Releases page](https://github.com/dipto0321/nodeup/releases). +```bash +# macOS Apple Silicon +curl -L https://github.com/dipto0321/nodeup/releases/latest/download/nodeup_$(curl -s https://api.github.com/repos/dipto0321/nodeup/releases/latest | grep tag_name | cut -d'"' -f4 | tr -d v)_darwin_arm64.tar.gz | tar xz +sudo mv nodeup /usr/local/bin/ + +# Linux x86_64 +curl -L https://github.com/dipto0321/nodeup/releases/latest/download/nodeup_*_linux_amd64.tar.gz | tar xz +sudo mv nodeup /usr/local/bin/ + +# Windows (PowerShell) +curl -L https://github.com/dipto0321/nodeup/releases/latest/download/nodeup_$(curl -s https://api.github.com/repos/dipto0321/nodeup/releases/latest | grep tag_name | cut -d'"' -f4 | tr -d v)_windows_amd64.zip -OutFile nodeup.zip +Expand-Archive nodeup.zip -DestinationPath . +# Move nodeup.exe somewhere on $PATH +``` -### From source +**Who this is for:** anyone who wants a deterministic install — no +package manager metadata, no postinstall hook, no auto-update. Pin +the URL to a specific tag (not `latest`) to make the install +reproducible across CI runs. -Requires Go 1.22+. +**Tradeoffs:** you own the upgrade lifecycle. Re-run the command (or +script it) when you want a new version. The binary is fully +self-contained — no runtime, no Node, no manager. + +### From source ```bash go install github.com/dipto0321/nodeup/cmd/nodeup@latest ``` +Requires Go 1.22+. + +**Who this is for:** `nodeup` contributors and Go developers who want +to track `main` between tagged releases. The binary lands in +`$GOBIN` (or `$GOPATH/bin`), which Go is configured to put on +`$PATH` for you. + +**Tradeoffs:** the slowest to upgrade (you recompile), but the +closest to the bleeding edge. Not appropriate for end users — they +should pick one of the channels above. + ## Verifying ```bash nodeup version ``` -Should print a version, git commit, build date, and Go runtime info. \ No newline at end of file +Should print a version, git commit, build date, and Go runtime info. + +## Picking once and sticking + +You can install via multiple channels at once if you want — they'll +write to different paths and the first one on `$PATH` wins. To +avoid the version-skew that causes, pick one and use it for upgrades +and uninstalls consistently: + +- **Homebrew** owns upgrades → use `brew upgrade nodeup`. +- **Scoop** owns upgrades → use `scoop update nodeup`. +- **npm** owns upgrades → use `npm update -g nodeup`. +- **Direct binary** owns upgrades → re-run the curl one-liner (or + pin a specific tag in CI). +- **From source** owns upgrades → `go install -u + github.com/dipto0321/nodeup/cmd/nodeup@latest`. \ No newline at end of file diff --git a/nodeup-npm/.npmignore b/nodeup-npm/.npmignore new file mode 100644 index 0000000..bd832eb --- /dev/null +++ b/nodeup-npm/.npmignore @@ -0,0 +1,42 @@ +# Node-up wrapper published artefacts — what `npm publish` ships: +# scripts/install.js — postinstall downloader +# scripts/check.js — preinstall platform check +# README.md — install / usage notes +# LICENSE — MIT +# +# Everything else in nodeup-npm/ is local dev scaffolding and is excluded +# from the published tarball. + +# Local artefacts — populated at install time by scripts/install.js +# (the postinstall downloads the right binary from the GitHub release +# into ./bin/). Anything already in bin/ before publish would be wrong +# for every user except the one who built it. +/bin/ + +# Wrapper dev / CI scratch +/.github/ +/scripts/__tests__/ +*.test.js +.coverage +.nyc_output + +# Editor / OS noise +.DS_Store +Thumbs.db +*.swp +*.swo +.idea/ +.vscode/ + +# Node / npm noise (we're shipped, not installed) +node_modules/ +package-lock.json +yarn.lock +.npm/ +.npmrc +*.log + +# VCS +.git/ +.gitignore +.gitattributes \ No newline at end of file diff --git a/nodeup-npm/LICENSE b/nodeup-npm/LICENSE new file mode 100644 index 0000000..3369d29 --- /dev/null +++ b/nodeup-npm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 dipto0321 + +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/nodeup-npm/README.md b/nodeup-npm/README.md new file mode 100644 index 0000000..36f59bd --- /dev/null +++ b/nodeup-npm/README.md @@ -0,0 +1,94 @@ +# nodeup (npm wrapper) + +> Thin npm wrapper around the [`nodeup`](https://github.com/dipto0321/nodeup) +> Go binary. Installs the right static binary for your OS/arch on +> `npm install -g` and exposes `nodeup` on your `$PATH`. + +This directory is the **npm distribution channel** for `nodeup`. It does +**not** contain the Go tool itself — it downloads the matching binary +from the [GitHub release](https://github.com/dipto0321/nodeup/releases) +that this package's `binaryVersion` field points at. + +## Install + +```bash +npm install -g nodeup +nodeup version +``` + +The `postinstall` script downloads a ~6 MB static Go binary into +`node_modules/nodeup/bin/` and symlinks it onto your global `$PATH` +as `nodeup`. No Go toolchain, no system services. + +## Who should use this wrapper + +**Pick the npm wrapper if you already live inside Node and want a +one-line install you trust from the npm registry.** It's especially +good when: + +- You manage dev tooling via `npm` / `pnpm` / `yarn` and your team's + onboarding doc already says `npm i -g `. +- You're on a machine where installing system packages (Homebrew, + Scoop, apt) is restricted, but npm global installs are allowed. +- You want the install to **travel with your Node version** — when you + upgrade Node and reinstall globals, `nodeup` reinstalls at the + matching binary. + +**Don't pick the npm wrapper if:** + +- You want zero Node runtime coupling. Use **Homebrew** (macOS/Linux) + or **Scoop** (Windows) — those install `nodeup` outside any Node + install and update it independently. +- You maintain `nodeup` itself. Use `go install ./cmd/nodeup@latest` + or **direct binary download** so you're not bound to whichever + release the wrapper is pinned to. +- You're scripting the install in CI and want a deterministic binary + with no postinstall network step. Use **direct binary download** + against a pinned release tag. + +## What it does on install + +1. **`preinstall` (`scripts/check.js`)** — verifies your + `process.platform` × `process.arch` is one of the matrix + `nodeup` ships for (`darwin`/`linux`/`windows` × `amd64`/`arm64`). + Exits non-zero so `npm` aborts if you ask for the wrapper on a + platform we don't build for. +2. **`postinstall` (`scripts/install.js`)** — downloads + `nodeup___.{tar.gz,zip}` from the + matching `v` GitHub release, extracts the binary + into `node_modules/nodeup/bin/`, and chmods it executable. The + `bin` field in `package.json` wires `nodeup` to that file. + +`binaryVersion` is **pinned** in `package.json`. The wrapper does +**not** fetch "latest" — it fetches the exact release this package +version was tested against. A wrapper bump and the matching Go +release bump ship in the same commit. + +## Updating + +```bash +npm update -g nodeup +``` + +You get a new wrapper version. If that wrapper pins a newer +`binaryVersion`, `npm` re-runs `postinstall` and downloads the new +binary. Old binaries are replaced; no leftover. + +To jump to a brand-new release ahead of the next wrapper publish, +install the binary directly (see the main +[README](https://github.com/dipto0321/nodeup#installation)). + +## Uninstall + +```bash +npm uninstall -g nodeup +``` + +Removes the wrapper, the downloaded binary, and the `nodeup` symlink +from your global `$PATH`. `nodeup`'s runtime state at `~/.nodeup/` +is **not** touched — uninstalling the CLI keeps your snapshots and +config in place. + +## License + +[MIT](../LICENSE) \ No newline at end of file diff --git a/nodeup-npm/package.json b/nodeup-npm/package.json new file mode 100644 index 0000000..c301045 --- /dev/null +++ b/nodeup-npm/package.json @@ -0,0 +1,43 @@ +{ + "name": "nodeup", + "version": "1.0.0", + "description": "Automated Node.js version upgrade + global package migration CLI. Static Go binary wrapped for npm distribution.", + "license": "MIT", + "author": "Dipto Karmakar ", + "homepage": "https://github.com/dipto0321/nodeup", + "repository": { + "type": "git", + "url": "https://github.com/dipto0321/nodeup.git", + "directory": "nodeup-npm" + }, + "bugs": { + "url": "https://github.com/dipto0321/nodeup/issues" + }, + "keywords": [ + "node", + "nodejs", + "version-manager", + "fnm", + "nvm", + "volta", + "upgrade", + "cli" + ], + "engines": { + "node": ">=14" + }, + "bin": { + "nodeup": "bin/nodeup" + }, + "scripts": { + "postinstall": "node scripts/install.js", + "preinstall": "node scripts/check.js" + }, + "files": [ + "scripts/install.js", + "scripts/check.js", + "README.md", + "LICENSE" + ], + "binaryVersion": "1.0.0" +} \ No newline at end of file diff --git a/nodeup-npm/scripts/check.js b/nodeup-npm/scripts/check.js new file mode 100644 index 0000000..6626262 --- /dev/null +++ b/nodeup-npm/scripts/check.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +// +// preinstall.js — runs BEFORE `npm install` downloads anything. +// +// We check that the user's environment can actually run the Go binary +// we're about to fetch. The npm wrapper is a thin downloader; if the +// binary can't run on this OS/arch, we fail fast with a clear error +// instead of letting `npm install` succeed and `nodeup upgrade` +// failing later with a cryptic exec error. +// +// This script exits 0 on success (so npm proceeds) and 1 on failure +// (so npm aborts with the message visible in the install log). +// +// Mapping rules mirror the GoReleaser archive names in +// .goreleaser.yaml — see the `archives[].name_template` there: +// {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + +'use strict'; + +const PLATFORM_TO_OS = { + darwin: 'darwin', + linux: 'linux', + win32: 'windows', + freebsd: null, // not built; intentional + openbsd: null, // not built; intentional + sunos: null, // not built; intentional +}; + +const ARCH_TO_GOARCH = { + x64: 'amd64', + arm64: 'arm64', + ia32: null, // not built + arm: null, // not built + ppc64: null, // not built + s390x: null, // not built +}; + +const platform = process.platform; +const arch = process.arch; + +const os = PLATFORM_TO_OS[platform]; +const goarch = ARCH_TO_GOARCH[arch]; + +if (!os || !goarch) { + console.error(''); + console.error(' nodeup: unsupported platform/architecture'); + console.error(` platform=${platform}, arch=${arch}`); + console.error(''); + console.error(' Built binaries are available for:'); + console.error(' OS: darwin, linux, windows'); + console.error(' arch: amd64, arm64'); + console.error(''); + console.error(' See https://github.com/dipto0321/nodeup/releases'); + console.error(' for direct binary downloads, or open an issue if'); + console.error(' you need support for this platform.'); + console.error(''); + process.exit(1); +} + +// All checks passed — let npm continue. +process.exit(0); \ No newline at end of file diff --git a/nodeup-npm/scripts/install.js b/nodeup-npm/scripts/install.js new file mode 100644 index 0000000..a221537 --- /dev/null +++ b/nodeup-npm/scripts/install.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node +// +// install.js — runs on `npm install` (postinstall) AND `npm install -g` +// (postinstall). Downloads the nodeup Go binary that matches this +// package's `binaryVersion` field for the user's OS/arch, extracts +// it to ./bin/, and chmods it executable on POSIX. +// +// We deliberately pin to a specific version (the one in package.json's +// `binaryVersion` field) rather than fetching "latest" so that: +// - npm install is reproducible: same package version always pulls +// the same binary version. +// - users don't get a surprise major-version upgrade by running +// `npm update -g` and getting the latest tag. +// - the Go binary and the wrapper move together — they bump in the +// same PR, get tested together, ship together. +// +// Bumping the binary: edit package.json's `binaryVersion`, commit, +// `npm publish`. The GoRelease pipeline still has to push the matching +// v tag first (the wrapper download will 404 otherwise). +// +// Mapping rules mirror the GoReleaser archive names in +// .goreleaser.yaml — see the `archives[].name_template` there: +// {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} +// +// Format overrides (line 53-55 of .goreleaser.yaml): +// - format_overrides: { goos: windows, format: zip } +// So unix archives are .tar.gz, windows is .zip. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execFileSync } = require('child_process'); +const zlib = require('zlib'); +const tar = require('tar'); // npm bundles tar; no extra dep + +// ---- helpers -------------------------------------------------------------- + +function die(msg, code = 1) { + console.error(''); + console.error(' nodeup: ' + msg); + console.error(''); + process.exit(code); +} + +function info(msg) { + console.log(' nodeup: ' + msg); +} + +function loadPackage() { + const pkgPath = path.join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + if (!pkg.binaryVersion) { + die('package.json is missing the binaryVersion field'); + } + return pkg; +} + +function platformOsArch() { + // mirrors preinstall.js — kept in sync by convention. We re-derive + // here rather than reading a shared file so that each script is + // self-contained and can be inspected in isolation. + const platform = process.platform; + const arch = process.arch; + + const PLATFORM_TO_OS = { + darwin: 'darwin', + linux: 'linux', + win32: 'windows', + }; + const ARCH_TO_GOARCH = { + x64: 'amd64', + arm64: 'arm64', + }; + + const os = PLATFORM_TO_OS[platform]; + const goarch = ARCH_TO_GOARCH[arch]; + if (!os || !goarch) { + die(`unsupported platform/architecture: ${platform}/${arch}`); + } + return { os, goarch }; +} + +function archiveName(os, arch, version) { + const ext = os === 'windows' ? 'zip' : 'tar.gz'; + return `nodeup_${version}_${os}_${arch}.${ext}`; +} + +function downloadTo(url, destPath) { + return new Promise((resolve, reject) => { + info(`downloading ${url}`); + const request = (targetUrl) => { + https + .get(targetUrl, { headers: { 'user-agent': 'nodeup-npm-wrapper' } }, (res) => { + // GitHub releases redirect to S3. Follow one redirect (the + // pattern is consistent enough that two-hop handling would + // be overkill for this use case). + if (res.statusCode === 302 || res.statusCode === 301) { + const redirect = res.headers.location; + if (!redirect) { + reject(new Error(`redirect with no Location header from ${targetUrl}`)); + return; + } + request(redirect); + return; + } + if (res.statusCode !== 200) { + reject( + new Error( + `download failed: HTTP ${res.statusCode} from ${targetUrl}` + ) + ); + return; + } + const out = fs.createWriteStream(destPath); + res.pipe(out); + out.on('finish', () => out.close(() => resolve(destPath))); + out.on('error', reject); + }) + .on('error', reject); + }; + request(url); + }); +} + +function extractTarGz(archivePath, outDir) { + return tar.x({ + file: archivePath, + cwd: outDir, + strip: 0, // GoReleaser archives don't have a top-level wrapper dir + }); +} + +function extractZip(archivePath, outDir) { + // No native zip support in Node — shell out to `unzip`. Available + // by default on Windows 10+ (PowerShell Expand-Archive) and via the + // `unzip` package on most POSIX distros. If neither is present the + // user gets a clear error from the spawned process. + const isWindows = process.platform === 'win32'; + if (isWindows) { + execFileSync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Expand-Archive -LiteralPath "${archivePath}" -DestinationPath "${outDir}" -Force`, + ], + { stdio: 'inherit' } + ); + } else { + execFileSync('unzip', ['-o', archivePath, '-d', outDir], { stdio: 'inherit' }); + } +} + +function chmodExec(filePath) { + if (process.platform === 'win32') return; + fs.chmodSync(filePath, 0o755); +} + +// ---- main ----------------------------------------------------------------- + +(async function main() { + const pkg = loadPackage(); + const { os, goarch } = platformOsArch(); + const version = pkg.binaryVersion; + const archive = archiveName(os, goarch, version); + + const repo = 'dipto0321/nodeup'; + const tag = `v${version}`; + const downloadUrl = `https://github.com/${repo}/releases/download/${tag}/${archive}`; + + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'nodeup-')); + const archivePath = path.join(tmpDir, archive); + const binDir = path.join(__dirname, '..', 'bin'); + const binaryDest = path.join(binDir, process.platform === 'win32' ? 'nodeup.exe' : 'nodeup'); + + fs.mkdirSync(binDir, { recursive: true }); + + try { + await downloadTo(downloadUrl, archivePath); + + if (os === 'windows') { + extractZip(archivePath, tmpDir); + } else { + await extractTarGz(archivePath, tmpDir); + } + + // GoReleaser archives have `nodeup` (or `nodeup.exe`) at the root. + const extractedName = process.platform === 'win32' ? 'nodeup.exe' : 'nodeup'; + const extractedPath = path.join(tmpDir, extractedName); + if (!fs.existsSync(extractedPath)) { + die(`archive did not contain ${extractedName} — unexpected layout`); + } + fs.renameSync(extractedPath, binaryDest); + chmodExec(binaryDest); + + info(`installed nodeup v${version} (${os}/${goarch}) -> ${binaryDest}`); + } finally { + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch (_) { + // best-effort cleanup; tmp will be reaped by the OS + } + } +})().catch((err) => { + die(err && err.message ? err.message : String(err)); +}); \ No newline at end of file