OTA Auto-Update for Pinggy CLI
Context
Pinggy CLI has no update mechanism. Users discover new versions only by checking npm or GitHub manually. The CLI ships two ways: npm global install (npm i -g pinggy) and standalone binaries via GitHub Releases (pkg-built, code-signed on macOS). An OTA system must handle both distribution paths across Linux, macOS, and Windows.
Design
Two-Mode Strategy
Detect how Pinggy was installed and behave accordingly:
- npm/pnpm/yarn install -> notify only, print generic message suggesting
npm install -g pinggy@latest
- Standalone binary (
process.pkg defined) -> full self-update via pinggy update
Architecture
src/updater/
updateChecker.ts # Version check + notification
selfUpdater.ts # Binary download + replace (standalone only)
platform.ts # OS/arch detection, install type, paths
Component 1: Update Checker
When: Every CLI launch, throttled to once per 24 hours.
Skip entirely on: --version, --help, no args (fast-exit commands).
How:
- Read last check timestamp from
{configDir}/update-check.json (uses existing getConfigDir() from src/utils/configDir.ts)
- If <24h since last check, use cached result. If cached result says update available, show banner.
- If >=24h, fire-and-forget
https.get in the main process (non-blocking):
- Fetches
https://api.github.com/repos/Pinggy-io/cli-js/releases/latest
- Strips
v prefix from release tag, compares against local getVersion()
- Writes result to cache file
- If CLI exits before response arrives, check silently fails — retries next launch
- If cache says update available, show banner:
- Non-TUI paths (subcommands,
--notui): Print to stderr at end of output
- TUI mode: Small text in the bottom bar area
Banner text (binary users):
Update available: 0.4.7 -> 0.5.0. Run: pinggy update
Banner text (npm users):
Update available: 0.4.7 -> 0.5.0
Pinggy was not installed as a standalone binary.
Update with your package manager, e.g.: npm install -g pinggy@latest
Opt-out controls:
--no-update-check flag
PINGGY_NO_UPDATE_CHECK=1 env var
{configDir}/config.json with { "updateCheck": false }
- Auto-skip when
CI=true
No external dependencies. Uses Node's built-in https module.
Component 2: Self-Updater (pinggy update)
Standalone binary path (process.pkg defined):
- Detect current platform + arch (
darwin-arm64, linux-x64, win-x64, etc.)
- Fetch latest release metadata from GitHub API:
GET https://api.github.com/repos/Pinggy-io/cli-js/releases/latest
- Strip
v prefix from tag. Compare with local version. If already up to date, print and exit.
- Find matching asset name (e.g.,
pinggy-linux-x64)
- Download binary to a temp directory (always writable, no root needed)
- Download matching
.sha256 file from the same release
- Verify SHA256 checksum
- If target directory is writable:
- Unix: Backup current binary (
pinggy -> pinggy.bak), move new binary in, chmod +x, verify with pinggy --version, delete backup on success, restore on failure
- Windows: Rename current to
pinggy-old.exe, rename new to pinggy.exe. Cleanup pinggy-old.exe on next launch.
- If target directory is NOT writable (EACCES):
- Print explicit commands the user needs to run:
Permission denied. Run these commands to complete the update:
sudo mv /tmp/pinggy-new /usr/local/bin/pinggy
sudo chmod +x /usr/local/bin/pinggy
- Do not suggest
sudo pinggy update. Keep the CLI out of root execution.
- Print "Updated to vX.Y.Z" on success.
Non-binary (npm/pnpm/yarn) path:
Pinggy was not installed as a standalone binary.
Update with your package manager, e.g.: npm install -g pinggy@latest
Old binary cleanup (all platforms):
On every CLI launch, attempt to delete pinggy-old.exe / pinggy.bak next to the current executable. Wrapped in try-catch, silently ignores ENOENT and EBUSY.
Component 3: Per-Binary Checksums (CI change)
Each CI job uploads a .sha256 file alongside its binary:
pinggy-linux-x64 + pinggy-linux-x64.sha256
pinggy-linux-arm64 + pinggy-linux-arm64.sha256
pinggy-macos-x64 + pinggy-macos-x64.sha256
pinggy-macos-arm64 + pinggy-macos-arm64.sha256
pinggy-win-x64.exe + pinggy-win-x64.exe.sha256
pinggy-win-arm64.exe + pinggy-win-arm64.exe.sha256
Each .sha256 file contains: <hex-digest> <filename>
Generated in each CI job after binary build, before upload. No cross-job coordination needed.
Component 4: CLI Subcommand
Two forms:
pinggy update — check and apply (or print instructions for npm users)
pinggy update --check — check only, print result, don't apply
Wire into existing subcommand routing in src/cli/subcommands.ts.
Add "update" to the reserved names list in configStore.ts name validation to prevent collision with saved tunnel configs.
Integration Points
| File |
Change |
src/main.ts |
Call checkForUpdate() after arg parsing (skip on fast-exit commands), add old binary cleanup |
src/cli/subcommands.ts |
Add "update" to subcommand set, route to updater |
src/cli/options.ts |
Add --no-update-check flag |
src/cli/help.ts |
Document update subcommand and --no-update-check |
src/cli/configStore.ts |
Add "update" to reserved config names |
src/utils/util.ts |
Add getInstallType(): returns `"npm" |
src/tui/blessed/*.ts |
Add small update banner text in bottom bar area |
.github/workflows/publish-binaries.yml |
Add SHA256 checksum generation + upload step to each job |
package.json |
No new runtime dependencies |
Limitations and Edge Cases
- Permissions: Download to temp dir (always writable). Print explicit
sudo mv + chmod commands if target directory requires root. Never auto-escalate.
- Network failures: 5s timeout for version check, 30s for binary download. Silent failure for background checks. Clear error with manual download URL for
pinggy update.
- Concurrent updates: Use a lockfile (
{configDir}/update.lock) during self-update to prevent races.
- Rate limiting: GitHub API allows 60 req/hour unauthenticated. 24h throttle means ~1 req/day per user.
- Pre-releases:
/releases/latest endpoint automatically excludes pre-releases and drafts.
- Version tag format: Tags are
v-prefixed (v0.4.7). Strip v before comparing with getVersion().
Excluded from v1
- No rollback command. Automatic restore-on-failed-verification covers broken binaries. Manual rollback via GitHub Releases download.
- No
--force flag on pinggy update. Avoids conflict with existing -f, --force tunnel flag.
- No proxy support. If the user can reach Pinggy servers for tunneling, they can likely reach GitHub.
- No Homebrew/apt/winget integration. Users on those channels update through their package manager.
- No third-party update libraries. Total updater code is ~300 lines.
Implementation Order
src/updater/platform.ts — install type detection, OS/arch helpers
src/updater/updateChecker.ts — fire-and-forget check, cache read/write, banner display
- Wire checker into
main.ts (skip on fast-exit), add old binary cleanup
src/updater/selfUpdater.ts — binary download, checksum verify, replace with platform-specific swap
- Add
update subcommand to subcommands.ts, add "update" to reserved names
- Add
--no-update-check to options, update help text
- Add TUI banner in blessed bottom bar
- Update CI workflows to generate + upload
.sha256 files
- Tests for each component
Verification
- npm user path: Install via npm, run any command. Verify banner appears on stderr when a newer version exists. Verify
pinggy update prints generic package manager message.
- Binary path: Build a binary with an old version number. Run
pinggy update. Verify download, checksum verification, binary replacement. Run pinggy --version to confirm.
- Throttling: Run twice within 24h. Second run uses cached result (no network call).
- Opt-out: Set
PINGGY_NO_UPDATE_CHECK=1, verify no check or banner.
- Windows: Test rename dance. Run again, verify
pinggy-old.exe cleaned up.
- Permission denied: Place binary in root-owned directory. Run
pinggy update. Verify it prints sudo mv instructions instead of crashing.
- Offline: Disconnect network, verify CLI starts normally with no errors or delays.
- TUI mode: Start a tunnel, verify small update banner in bottom bar.
- Fast-exit: Run
pinggy --version, verify no network call made.
OTA Auto-Update for Pinggy CLI
Context
Pinggy CLI has no update mechanism. Users discover new versions only by checking npm or GitHub manually. The CLI ships two ways: npm global install (
npm i -g pinggy) and standalone binaries via GitHub Releases (pkg-built, code-signed on macOS). An OTA system must handle both distribution paths across Linux, macOS, and Windows.Design
Two-Mode Strategy
Detect how Pinggy was installed and behave accordingly:
npm install -g pinggy@latestprocess.pkgdefined) -> full self-update viapinggy updateArchitecture
Component 1: Update Checker
When: Every CLI launch, throttled to once per 24 hours.
Skip entirely on:
--version,--help, no args (fast-exit commands).How:
{configDir}/update-check.json(uses existinggetConfigDir()fromsrc/utils/configDir.ts)https.getin the main process (non-blocking):https://api.github.com/repos/Pinggy-io/cli-js/releases/latestvprefix from release tag, compares against localgetVersion()--notui): Print to stderr at end of outputBanner text (binary users):
Banner text (npm users):
Opt-out controls:
--no-update-checkflagPINGGY_NO_UPDATE_CHECK=1env var{configDir}/config.jsonwith{ "updateCheck": false }CI=trueNo external dependencies. Uses Node's built-in
httpsmodule.Component 2: Self-Updater (
pinggy update)Standalone binary path (
process.pkgdefined):darwin-arm64,linux-x64,win-x64, etc.)GET https://api.github.com/repos/Pinggy-io/cli-js/releases/latestvprefix from tag. Compare with local version. If already up to date, print and exit.pinggy-linux-x64).sha256file from the same releasepinggy->pinggy.bak), move new binary in,chmod +x, verify withpinggy --version, delete backup on success, restore on failurepinggy-old.exe, rename new topinggy.exe. Cleanuppinggy-old.exeon next launch.sudo pinggy update. Keep the CLI out of root execution.Non-binary (npm/pnpm/yarn) path:
Old binary cleanup (all platforms):
On every CLI launch, attempt to delete
pinggy-old.exe/pinggy.baknext to the current executable. Wrapped in try-catch, silently ignoresENOENTandEBUSY.Component 3: Per-Binary Checksums (CI change)
Each CI job uploads a
.sha256file alongside its binary:Each
.sha256file contains:<hex-digest> <filename>Generated in each CI job after binary build, before upload. No cross-job coordination needed.
Component 4: CLI Subcommand
Two forms:
pinggy update— check and apply (or print instructions for npm users)pinggy update --check— check only, print result, don't applyWire into existing subcommand routing in
src/cli/subcommands.ts.Add
"update"to the reserved names list inconfigStore.tsname validation to prevent collision with saved tunnel configs.Integration Points
src/main.tscheckForUpdate()after arg parsing (skip on fast-exit commands), add old binary cleanupsrc/cli/subcommands.ts"update"to subcommand set, route to updatersrc/cli/options.ts--no-update-checkflagsrc/cli/help.tsupdatesubcommand and--no-update-checksrc/cli/configStore.ts"update"to reserved config namessrc/utils/util.tsgetInstallType(): returns `"npm"src/tui/blessed/*.ts.github/workflows/publish-binaries.ymlpackage.jsonLimitations and Edge Cases
sudo mv+chmodcommands if target directory requires root. Never auto-escalate.pinggy update.{configDir}/update.lock) during self-update to prevent races./releases/latestendpoint automatically excludes pre-releases and drafts.v-prefixed (v0.4.7). Stripvbefore comparing withgetVersion().Excluded from v1
--forceflag onpinggy update. Avoids conflict with existing-f, --forcetunnel flag.Implementation Order
src/updater/platform.ts— install type detection, OS/arch helperssrc/updater/updateChecker.ts— fire-and-forget check, cache read/write, banner displaymain.ts(skip on fast-exit), add old binary cleanupsrc/updater/selfUpdater.ts— binary download, checksum verify, replace with platform-specific swapupdatesubcommand tosubcommands.ts, add"update"to reserved names--no-update-checkto options, update help text.sha256filesVerification
pinggy updateprints generic package manager message.pinggy update. Verify download, checksum verification, binary replacement. Runpinggy --versionto confirm.PINGGY_NO_UPDATE_CHECK=1, verify no check or banner.pinggy-old.execleaned up.pinggy update. Verify it printssudo mvinstructions instead of crashing.pinggy --version, verify no network call made.