Skip to content

OTA - PLAN #95

@Niladri2003

Description

@Niladri2003

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:

  1. npm/pnpm/yarn install -> notify only, print generic message suggesting npm install -g pinggy@latest
  2. 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:

  1. Read last check timestamp from {configDir}/update-check.json (uses existing getConfigDir() from src/utils/configDir.ts)
  2. If <24h since last check, use cached result. If cached result says update available, show banner.
  3. 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
  4. 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):

  1. Detect current platform + arch (darwin-arm64, linux-x64, win-x64, etc.)
  2. Fetch latest release metadata from GitHub API: GET https://api.github.com/repos/Pinggy-io/cli-js/releases/latest
  3. Strip v prefix from tag. Compare with local version. If already up to date, print and exit.
  4. Find matching asset name (e.g., pinggy-linux-x64)
  5. Download binary to a temp directory (always writable, no root needed)
  6. Download matching .sha256 file from the same release
  7. Verify SHA256 checksum
  8. 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.
  9. 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.
  10. 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

  1. Permissions: Download to temp dir (always writable). Print explicit sudo mv + chmod commands if target directory requires root. Never auto-escalate.
  2. 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.
  3. Concurrent updates: Use a lockfile ({configDir}/update.lock) during self-update to prevent races.
  4. Rate limiting: GitHub API allows 60 req/hour unauthenticated. 24h throttle means ~1 req/day per user.
  5. Pre-releases: /releases/latest endpoint automatically excludes pre-releases and drafts.
  6. 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

  1. src/updater/platform.ts — install type detection, OS/arch helpers
  2. src/updater/updateChecker.ts — fire-and-forget check, cache read/write, banner display
  3. Wire checker into main.ts (skip on fast-exit), add old binary cleanup
  4. src/updater/selfUpdater.ts — binary download, checksum verify, replace with platform-specific swap
  5. Add update subcommand to subcommands.ts, add "update" to reserved names
  6. Add --no-update-check to options, update help text
  7. Add TUI banner in blessed bottom bar
  8. Update CI workflows to generate + upload .sha256 files
  9. Tests for each component

Verification

  1. 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.
  2. Binary path: Build a binary with an old version number. Run pinggy update. Verify download, checksum verification, binary replacement. Run pinggy --version to confirm.
  3. Throttling: Run twice within 24h. Second run uses cached result (no network call).
  4. Opt-out: Set PINGGY_NO_UPDATE_CHECK=1, verify no check or banner.
  5. Windows: Test rename dance. Run again, verify pinggy-old.exe cleaned up.
  6. Permission denied: Place binary in root-owned directory. Run pinggy update. Verify it prints sudo mv instructions instead of crashing.
  7. Offline: Disconnect network, verify CLI starts normally with no errors or delays.
  8. TUI mode: Start a tunnel, verify small update banner in bottom bar.
  9. Fast-exit: Run pinggy --version, verify no network call made.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions