Skip to content

feat(cli): Check npm for updates and install them from the CLI and the gateway console - #1246

Merged
kotevcode merged 13 commits into
mainfrom
feat/cli-console-updates
Oct 17, 2026
Merged

kotevcode merged 13 commits into
mainfrom
feat/cli-console-updates

Conversation

@kotevcode

@kotevcode kotevcode commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds update checks and updates to the CLI and the gateway console, the way the desktop app does them: check every 30 minutes, say a new version is out, and install only when someone asks. Nothing auto-installs. Checks are a small npm dist-tags request, cached in the data dir.

CLI

  • antseed update detects how the CLI was installed and runs the matching command (npm install -g, pnpm add -g, yarn global add, bun add -g, volta install). For a root-owned npm prefix, Nix, npx, a project dependency or a source checkout it prints the command instead. --check only reports; --json prints a versioned report.
  • One-line update notice on stderr for interactive commands, refreshed in a detached background process at most every 30 minutes. Quiet for --json, non-TTY stderr, CI, NO_UPDATE_NOTIFIER and ANTSEED_NO_UPDATE_CHECK. ANTSEED_NPM_REGISTRY / npm_config_registry select a mirror.
  • A running seller start / buyer start checks every 30 minutes and logs one line per new version, also under systemd or Docker: Antseed 0.1.195 is available (this seller runs 0.1.193). Run antseed update, then restart the seller. A buyer run by the gateway leaves this to the gateway.

Gateway console

  • The gateway checks every 30 minutes while it runs (gateway and tunnel start) and logs each new version once.
  • Two steps, as on the desktop app's banner: Download fetches the new version while the gateway keeps serving, then Restart & update (with a confirm dialog) switches to it. Elsewhere the panel shows the command to run on the host.
  • Settings → General → Updates: running version, npm latest, last check, Check now, and the two buttons. One calm line above every page for owners and admins (available → Download, downloaded → Restart & update; dismissible per version). The page reloads by itself once the gateway is back on the new version.
  • Console API: GET /update, POST /update/check, POST /update with {"action":"download"|"install"} (default install; org admins / admin-scope tokens; 409 update_unsupported / up_to_date; only the install is audited, as gateway.update).

Gateway installer

  • The services run as the unprivileged antseed user and can't write /opt/antseed, so an update only creates /var/lib/antseed/update-request. A new antseed-update.path unit starts a root antseed-update.service that installs @antseed/cli@latest (the request can only pick the step, not a version). "action":"download" stops there (status ready); otherwise it also switches to it and restarts the buyer and gateway. It writes /var/lib/antseed-update/status.json, which the console polls.
  • --uninstall removes the new units. Existing servers get the updater by re-running the installer once; until then the console says the request wasn't picked up.

Docs (commands, install, gateway server, management API) and CHANGELOG are updated.

No-downtime updates

  • The updater installs each version in its own /opt/antseed/releases/<version> next to the running one and swaps /opt/antseed/current (replacing /opt/antseed/cli). It restarts the buyer first, then the gateway, each only after the previous one answers, and rolls both back to the previous version if either does not start. Nothing is restarted when npm's latest is already installed, and a downloaded release is not installed again.
  • The gateway holds requests while the buyer refuses connections (up to 2 min, refused connections only, so the buyer never sees a request twice) instead of answering 502 buyer_unavailable; capped keys wait for the spend feed the same way. A request the buyer never received releases its budget hold at once.
  • Caddy holds requests while the gateway restarts (lb_try_duration 75s). The gateway and buyer give requests in flight up to a minute to finish on shutdown (TimeoutStopSec=90).

Desktop app

  • The desktop app's public tunnel now gets ANTSEED_UPDATER_REQUEST/STATUS plus ANTSEED_UPDATER_KIND=desktop. The console then shows the app's own version and newest release (product: 'desktop' on GET /update), Download has the app's electron-updater download the release, and Restart & update restarts the app into it; the run is finished (or reported failed) on the next launch. A download started from the app's own banner shows as ready in the console too.

Testing

  • New tests: CLI (version compare, registry/cache, install detection, root updater, notifier, console API routes) and console (updatePhase, notice visibility).
  • CLI typecheck + npm test (1236/1237; the one failure is deposit-watch-hub.test.js, unrelated and passing on rerun in isolation), console typecheck + vitest (192/192), full workspace build.
  • Root update script run with fake npm/systemctl for success and failure; antseed update end to end against a simulated update service; console flow clicked through in mock mode (?update=available|command|current).
  • Updater scenarios in a Debian container with stub npm/systemctl/curl: update, already current (no restart), a version that does not start (rolled back and removed), npm failure (nothing restarted), pruning to current + previous.
  • New tests for the gateway hold (buyer back within the hold, hold expiry, client leaving while held, capped key waiting for the spend feed) and the bounded shutdown drain; desktop console-updates tests. CLI suite 1241/1242 (0 failures), desktop main 372/372, console 192/192.
  • Two-step flow: CLI tests for the watcher (once per version, cache, off when disabled or in the desktop app), download/ready parsing and the console route (download not audited, repeated download is a no-op, bad action → 400); console updatePhase/readyVersion; desktop download → ready → install, banner download, relaunch handling (7/7); updater container run for download → ready → install without re-running npm (34/34). CLI 1244/1245 (0 failures, 1 skipped), console 194/194.
  • Not yet run on a real systemd host, so the path-unit trigger itself is untested.

@kotevcode

Copy link
Copy Markdown
Contributor Author

@claude review the code

@claude

claude Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Claude finished @kotevcode's task in 48s —— View job


Review of #1246: update checks and updates from the CLI and gateway console

I read the changed code, mainly the installer script, the root-updater protocol, the gateway hold logic, the console API handler and the watcher/notifier. I did not run the build or tests, and I did not read the console UI (UpdatePanel) or the desktop console-updates code.

The design is solid. The request file can only pick a step, never a version. The status directory is root-owned. The release-directory plus atomic symlink swap, the rollback, and the ECONNREFUSED-only retry (so a request is never sent twice) are all well reasoned.

Issues worth fixing

  1. The 30-minute watcher can skip a tick and effectively check hourly (apps/cli/src/update/watch.ts, check()).

    • The cache-hit test is now() - cache.checkedAt < intervalMs.
    • The interval is armed before the first fetch finishes, so the cache is stamped a little after the timer started.
    • When the timer fires 30 minutes later, the cache is slightly under 30 minutes old, so the check reuses it and skips the fetch.
    • The next real fetch then happens about 60 minutes in.
    • A fix is to let the timer tick bypass the cache (a force flag), or to allow slack, e.g. < intervalMs - 5_000.
    • The gateway version (gateway/updates.ts) calls runCheck() directly from the timer, so it is not affected.
  2. An install request during an in-flight download is silently dropped (gateway/updates.ts apply() and console-api/handlers/update.ts).

    • apply() writes nothing when an update is already requested or running, but the route still returns 202 with the unchanged progress.
    • The audit entry is skipped, which is correct, but the caller can't tell its "install" was ignored.
    • Please confirm the UI disables Restart & update while the phase is requested or running.
    • Otherwise return 409 update_in_progress for a different action than the one running.
  3. Updater runs npm install as root. install_release runs inside the root antseed-update.service, so the package's lifecycle scripts, and those of its native dependencies such as better-sqlite3 and node-datachannel, also run as root.

    • That is a larger trust boundary than the old manual install.
    • It is defensible because the version is pinned to latest and only admins can trigger it, but it deserves a line in docs/gateway/server.md.
    • Consider whether the install can run as the antseed user into a staging dir, with root only doing the symlink swap and restarts.
  4. Rollback with no previous release (installer rollback()).

    • If readlink -f "$CLI_HOME" fails, PREVIOUS is empty and activate_release "" points current at nothing. It can't be an empty path in normal flow, because FROM is read earlier, but the updater has no guard.
    • Add [ -n "$PREVIOUS" ] || fail … before activating.

Smaller notes

  • Hold loop in _spendFeedReporting (gateway/server.ts).
    • Every capped-key request calls refreshSpendFeed() every 500ms for up to 2 minutes while the buyer is down.
    • With many concurrent requests this multiplies the load. Share one in-flight refresh or a single poller.
  • Accounting release on client abort while held (gateway/server.ts).
    • closedByClient() calls finish(499) and then accounting.release(tag) separately.
    • Please verify finish doesn't also release, so the hold isn't released twice. The new test "client leaving while held" might already cover this.
  • Timeout mismatch.
    • Caddy holds for 75s (lb_try_duration), the gateway holds for 120s, and the updater waits 120s per service.
    • That is fine in practice. A comment tying the numbers together would help whoever tunes them.
  • Downloaded release vs. a newer latest.
    • If a newer version is published after a download, Restart & update installs the newer one. The console compares progress.to with latest for the download no-op, but the install path can differ from what the user saw as "ready".
    • Consider showing the version to be installed in the confirm dialog.
  • Real systemd test. As the PR says, the .path unit trigger is untested on a real host. One run on a VM, covering update, rollback and uninstall, should happen before merge.
  • Changelog. CHANGELOG.md has +4 lines, which satisfies the PR policy.

Verdict

Nothing blocking beyond a real-host smoke test. I'd fix items 1 and 2 and either address or document item 3 before merge.

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab could you review this?

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab

@reviewant reviewant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review requested by @kotevcode (comment).

2 issues should be fixed before merge.

  • Adds npm update checks, CLI installation, two-step console updates, desktop integration, and restart-time request buffering.
  • Core files read fully: apps/cli/src/gateway/updates.ts, apps/cli/src/gateway/console.ts, apps/cli/src/gateway/console-api/deps.ts, apps/cli/src/gateway/console-api/handlers/index.ts, and apps/cli/src/gateway/console-api/handlers/update.ts. Also inspected updater modules, installer lifecycle, desktop integration, and gateway/buyer shutdown and retry paths. Full-file coverage of the larger core files remains incomplete.
  • Checked authorization, update handoff, rollback, cancellation, version compatibility, and changelog coverage. Findings concern unintended installation and loss of the downloaded-release action. No verified decentralization or payment-protocol issue was identified in the inspected changes; this is not a complete funds-safety assessment.
  • Validation: installer passes bash syntax checking. Automated tests were not run because checkout dependencies are unavailable.

Comment thread apps/cli/src/update/root-updater.ts Outdated

/** Asks the root updater for `@antseed/cli@latest`. Writing the file is all it takes. */
export function requestRootUpdate(paths: RootUpdaterPaths, requestedBy: string, now = Date.now(), action: RootUpdateAction = 'install'): void {
writeFileSync(paths.request, `${JSON.stringify({ requestedAt: now, requestedBy, action })}\n`, { mode: 0o644 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Publish update requests atomically. writeFileSync creates the watched request path before writing its JSON. The systemd PathExists watcher can therefore start the updater while the file is empty; the updater treats anything without the download marker as install. A Download click can consequently restart the buyer and gateway without the separate restart confirmation. The desktop poller likewise defaults unreadable JSON to installation. Write the complete request to a temporary sibling and rename it onto the watched path, and reject incomplete requests rather than defaulting them to an install.

Comment thread apps/gateway-console/src/lib/update.ts Outdated
/** The downloaded version waiting for Restart & update, if any. */
export function readyVersion(update: GatewayUpdate): string | null {
const progress = update.progress
return progress?.state === 'ready' && progress.to && progress.to === update.latest && update.available ? progress.to : null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep a downloaded release installable when npm publishes a newer release. After downloading version B, if a periodic check discovers version C before the operator restarts, progress.to !== update.latest makes readyVersion() return null. Both console surfaces then replace Restart & update with Download, so the operator cannot install the already-staged B through the intended two-step UI. This contradicts the updater's explicit behavior of installing the staged version even after npm moves on. Derive readiness from a valid ready progress record whose version is newer than the running version, independently of the latest registry answer; display that staged version in the badge and confirmation. Add coverage for a registry change between download and installation.

@reviewant

Copy link
Copy Markdown
Contributor

@kotevcode I already reviewed this commit (e602e05): #1246 (review)
Push new commits, or tag me with "re-review" to run a fresh pass.

@reviewant

reviewant commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1246 (comment) (previously reviewed f6d7b23911)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab re-review

Both P2s are addressed in f6d7b23:

  • Publish update requests atomically: requestRootUpdate() writes to a temp sibling and renames it onto the watched path. The root updater restarts nothing when a request file exists but isn't a complete JSON object or a regular file (a missing file is still root starting the service by hand). The desktop bridge drops an unparseable request instead of installing.
  • Keep staged releases installable: GET /update now reports downloaded: a ready run newer than the running version, whatever npm's latest is. The console keeps Restart & update for it, shows that version in the badge, confirm dialog and audit, and mentions the newer npm release separately.

Tests: CLI 1249/1250 (0 failures, 1 skipped), desktop main 375/375, console 194/194; the updater script in a Linux container 47/47, including empty and half-written requests.

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab re-review

Fixed in 590d288: the root updater now parses the request with the installed Node (JSON.parse of at most 1 KiB of a regular, non-symlink file). It accepts only a non-array object whose action is missing (install, as root starting the service by hand or older writers), download or install; anything else, including {not-json}, {"requestedBy":{"x":1}, unknown actions and arrays, fails with nothing installed or restarted. { "action": "download" } with whitespace is a download. A missing file is still a manual install. The desktop bridge applies the same rules.

Tests: the parser against those inputs with real Node; the updater script in a Linux container 51/51 (truncated nested object, brace text, unknown action, array, whitespace download); desktop main 375/375.

@reviewant

reviewant commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1246 (comment) (previously reviewed 590d28835b)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab re-review

Both P2s are addressed in f0a72ba:

  • Reject oversized requests instead of parsing their prefix: the root updater reads at most 1025 bytes and its parser rejects anything over 1024 bytes before JSON.parse, so {} + 1022 spaces + not-json fails with nothing installed or restarted. A 1024-byte download request is still a download.
  • Reject explicit null actions in the desktop bridge: the bridge defaults the action only when it is undefined (destructuring default), so {"action":null} is rejected like the root updater does.

Tests: desktop console-updates 8/8 (the incomplete-request test now includes {"action":null}); the updater script in a Linux container 53/53, adding the oversized-with-trailing-junk and null-action cases; the embedded parser checked with real Node at 1024 and 1025 bytes; bash -n + shellcheck clean.

@reviewant

reviewant commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1246 (comment) (previously reviewed f0a72ba98d)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab re-review

@kotevcode

Copy link
Copy Markdown
Contributor Author

Addressed review 4: the gateway supervisor now waits BUYER_SHUTDOWN_DEADLINE_MS + 15s (90 s) before SIGKILL, with the deadline shared between buyer start and GatewayBuyer, plus a fake-timer test that a draining buyer is not killed before its own deadline.

@reviewant

reviewant commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Superseded by a newer review: #1246 (comment) (previously reviewed 40d577bea8)

@kotevcode

Copy link
Copy Markdown
Contributor Author

@kc-zero-lab re-review

Addressed review 5: Check now on a desktop-hosted gateway writes <request>.check (a separate file, so a check is never read as an update request). The desktop bridge picks it up, calls autoUpdater.checkForUpdates(), and the CLI waits up to 20 s for the app's checkedAt to move before answering. Tests on both sides.

@reviewant

Copy link
Copy Markdown
Contributor

Antseed Review Summary

This PR adds npm update discovery and an update command to the CLI, plus download and install controls in the gateway console. Installer-managed gateways use a privileged update service with staged releases and rollback, while desktop-hosted gateways communicate with Electron's updater through request and status files. Gateway requests now wait through refused buyer connections, and the desktop Check now action uses a separate check-request file.

  • P2 — Preserve the downloaded version as the installation target: A desktop update check after downloading a release can cause a successful installation to be reported as failed.

Confidence Score: 4/5

Safe to merge after preserving the downloaded desktop version as the installation target.

The separate desktop check-request path correctly avoids interpreting a check as an install, but checking after a download can make the bridge record the wrong installation target. The full CLI suite passed with 1,250 tests passing and one skipped; the desktop suites passed with 12 script tests, 376 main-process tests, and 502 renderer tests. An initial CLI invocation incorrectly appended source TypeScript test paths and failed module resolution; rerunning the package suite without those arguments passed. Actual systemd upgrades, rollback, and packaged Electron installation were assessed through code inspection rather than end-to-end execution.

Files that need attention:

Important Files Changed

Filename Overview
apps/cli/src/cli/commands/update.ts Adds explicit update checking and installation through package managers or the installer-managed update service.
apps/cli/src/update/install-method.ts Classifies installation layouts to select an executable update command or operator guidance.
apps/cli/src/update/root-updater.ts Defines file-based update requests, progress parsing, stale-run handling, and a separate desktop check request.
apps/cli/src/gateway/updates.ts Combines npm checks and updater progress, and waits for desktop check results without issuing an install request.
apps/cli/src/gateway/console-api/handlers/update.ts Adds administrator-only update endpoints, action validation, and installation audit records.
apps/cli/src/gateway/server.ts Adds bounded waiting for buyer connection refusals and unavailable spend reporting, with cancellation and shutdown handling.
apps/cli/src/gateway/buyer-process.ts Allows intentional buyer restarts without counting them as rapid startup failures.
apps/cli/src/proxy/buyer-proxy.ts Adds the buyer restart contract used by supervised gateway processes.
apps/desktop/src/main/public-tunnel/console-updates.ts Bridges console requests to the desktop updater; installation target tracking needs the correction described above.
apps/desktop/src/main/main.ts Connects the bridge to updater checks, downloads, errors, and installation of the downloaded artifact.
apps/gateway-console/src/components/UpdatePanel.tsx Adds update status, explicit checks, download controls, and restart confirmation.
apps/website/static/install-gateway.sh Introduces staged CLI releases and a systemd updater with readiness probes and rollback; live upgrade behavior remains unverified.
P2 Preserve the downloaded version as the installation target

P2 — apps/desktop/src/main/public-tunnel/console-updates.ts:178

At apps/desktop/src/main/public-tunnel/console-updates.ts:178, startRun always takes its target from app.latest, overwriting the previous ready run's target. For example, download version B while running A, then use Check now after C is published: checked(C) updates app.latest while the downloaded artifact remains B. The install callback in apps/desktop/src/main/main.ts:620-622 explicitly installs downloadedVersion B, but the persisted install run now targets C. On relaunch into B, the bridge's startup reconciliation compares B against C and marks the successful update as failed. This also contradicts the console confirmation and audit entry, which select the downloaded version.

Suggested fix

For an install action, preserve the ready run's to version before replacing run; use app.latest only when no downloaded release is being installed. Add a regression test covering download B, check C, install B, and relaunch into B, asserting that the run ends as succeeded.

Diagram

sequenceDiagram
    participant Console
    participant Gateway
    participant Files
    participant Desktop
    participant Updater
    Console->>Gateway: Check now
    Gateway->>Files: Write separate check request
    Desktop->>Files: Consume check request
    Desktop->>Updater: Check for updates
    Updater-->>Desktop: Release or error
    Desktop->>Files: Write versions and checkedAt
    Gateway->>Files: Wait for checkedAt change
    Gateway-->>Console: Updated status
    Console->>Gateway: Download or install
    Gateway->>Files: Write action request
    Desktop->>Files: Consume action request
    Desktop->>Updater: Download or install
    Desktop->>Files: Persist run progress
Loading

Core files read in full: apps/cli/src/gateway/buyer-process.ts, apps/cli/src/gateway/console.ts, apps/cli/src/gateway/console-api/handlers/update.ts, apps/cli/src/gateway/server.ts, apps/cli/src/gateway/updates.ts, apps/cli/src/proxy/buyer-proxy.ts

Reviews (6): Last reviewed commit: feat(cli): Check npm for updates and install them from the C (38ae4bde6a) | requested by @kotevcode | Tag @kc-zero-lab with "re-review" to run again

@kotevcode

Copy link
Copy Markdown
Contributor Author

Addressed review 6: installing a finished download now targets that downloaded release even when a later check found a newer one, so the restart on it reports success. Test added.

@kotevcode
kotevcode merged commit 06f5375 into main Oct 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants