Skip to content

Phase 1C: server distribution polish (CLI + install + supervision + upgrade) - #170

Merged
aterrylu merged 10 commits into
mainfrom
terry/phase-1c-server-distribution
May 14, 2026
Merged

Phase 1C: server distribution polish (CLI + install + supervision + upgrade)#170
aterrylu merged 10 commits into
mainfrom
terry/phase-1c-server-distribution

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Summary

Phase 1C of the desktop-app initiative. Polishes the autonomOS Server distribution into something shippable to external users: a public curl install.sh | bash install path, OS-native daemon supervision (launchd/systemd-user, no pm2), an atomic upgrade flow, and a hard-cutover migration for existing pm2 users.

Built end-to-end as 5 self-contained commits, each individually reviewable.

What ships

Subcommand Behavior
autonomos start Run server in foreground (default when no subcommand). Forwards --port / --embedded flags.
autonomos stop Read PID file, SIGTERM, wait up to 10s, escalate to SIGKILL if needed.
autonomos status Daemon state — version, pid, port, hostname, uptime, url.
autonomos install-service Write launchd plist (mac) or systemd-user unit (linux). User-scope, no sudo.
autonomos uninstall-service Stop daemon, remove service file.
autonomos upgrade Fetch latest GitHub release, verify SHA256, atomic swap, restart daemon.
autonomos migrate-from-pm2 One-shot migration: stop pm2's autonomos + install OS-native supervisor.

install.sh at scripts/install.sh:

  • Cross-platform Bash (darwin-arm64/x64 + linux-x64/arm64)
  • Downloads from GitHub Releases
  • Verifies SHA256 before extraction
  • Installs to $HOME/.local by default (no sudo)
  • Auto-detects pm2-managed autonomos and migrates before installing the new supervisor
  • Auto-runs install-service post-install

HTTP API additions:

  • GET /api/system/version — version + platform + arch
  • POST /api/system/upgrade — server-side upgrade (callable from dashboard; UI button is a follow-up)

Release pipeline at .github/workflows/release.yml:

  • Triggered by v* tags
  • Matrix: macos-14 (arm64), macos-13 (x64), ubuntu-latest, ubuntu-24.04-arm
  • Builds dashboard → embeds → bundles → tarballs each platform
  • Publishes a GitHub Release with all tarballs + SHA256SUMS

Architecture changes

  • New package: packages/cli/ — autonomos CLI, depends on @autonomos/server
  • New module: packages/server/src/run.ts — exports runServer(argv); the CLI's start subcommand calls this directly (no subprocess hop). Old packages/server/src/index.ts becomes a thin entry that calls runServer for backward compat with Phase 1B's Electron-spawn contract.
  • New module: packages/server/src/pid-file.ts — JSON PID file at $configDir/autonomos.pid. Written on listen, removed on shutdown. Skipped in embedded mode.
  • New module: packages/server/src/version.ts — single source of truth for the running version.
  • New module: packages/server/src/upgrade.ts — shared upgrade logic used by CLI + HTTP endpoint.
  • packages/server/build/build-binary.ts — now bundles packages/cli/src/index.ts (the CLI is the top-level entry; it dispatches argv-only invocations to runServer for the Phase 1B contract). Also writes the bundle's own package.json for runtime version reading.

Backward compat for existing pm2 users

The migration is hard cutover, automatic on upgrade:

  1. curl install.sh | bash detects pm2-managed autonomos
  2. Auto-runs autonomos migrate-from-pm2:
    • pm2 stop autonomos
    • pm2 delete autonomos
    • pm2 save
    • install-service --force (writes plist/unit + activates)
  3. Daemon now under OS-native supervision; existing pm2 setup is gone

install-service itself refuses to overwrite an active pm2 install unless --force is passed — prevents accidental port collisions.

Test plan

  • Hermetic end-to-end (scripts/test-install.sh):
    • Build tarball + SHA256SUMS
    • install.sh with BUNDLE_URL=file:// + INSTALL_PREFIX=/tmp
    • Verify wrapper, --help, start, status, /api/host probe, stop
    • install-service --no-activate under test prefix → verify plist/unit written
    • uninstall-service → verify plist/unit removed
  • CI matrix (.github/workflows/test-install.yml):
    • macos-14 (darwin-arm64) + ubuntu-latest (linux-x64)
    • Runs test-install.sh on PR
  • tsc -b clean
  • npx biome check packages/ clean (only pre-existing useTerminal.ts warnings unrelated to 1C)
  • Manual verification on existing pm2 setup — Terry to run on his actual deployment before merging. The make prod/pm2 path is hard to fully exercise in CI.

What's NOT in this PR

  • Dashboard Settings → About UI for the upgrade button. The /api/system/upgrade endpoint exists; UI integration is a small follow-up.
  • Windows distribution (no Windows Service install yet; deferred per Phase 1C scope).

Commits (5)

  1. feat(cli): commit 1/5 — packages/cli/ scaffold + start/stop/status
  2. feat(cli): commit 2/5 — install-service + uninstall-service
  3. feat: commit 3/5 — install.sh + GitHub Releases workflow
  4. feat: commit 4/5 — autonomos upgrade + /api/system/* endpoints
  5. feat: commit 5/5 — pm2 migration + hermetic test + CI matrix

Existing make dev/make prod/pm2 flows: untouched until a user explicitly runs autonomos upgrade or migrate-from-pm2.

🤖 Generated with Claude Code

aterrylu and others added 6 commits May 13, 2026 01:23
…op/status

Introduces the `autonomos` CLI binary. Refactors packages/server/src/index.ts
to expose runServer() as a function so the CLI can invoke it without
duplicating logic — the CLI imports runServer from @autonomos/server/run.js.

Subcommands:
  autonomos start     Run the server in the foreground (default if no subcommand)
                      Forwards --port and --embedded to runServer.
  autonomos stop      Read PID file, send SIGTERM, wait up to 10s,
                      escalate to SIGKILL if needed.
  autonomos status    Read PID file, verify process alive, HTTP-probe /api/host,
                      print version/pid/port/hostname/uptime/url.

Behind the scenes:
  - packages/server/src/run.ts: runServer(argv) — the actual startup logic
    (parseCliArgs, provider validation, gemini settings, migration, app setup,
    serve(), shutdown handlers). Behavior identical to the pre-1C top-level
    startup, with one addition: writes a PID file in standalone mode.
  - packages/server/src/index.ts: thin entry that calls runServer.
  - packages/server/src/pid-file.ts: JSON PID file utilities (write on listen,
    remove on shutdown). Skipped in embedded mode (Electron tracks PID).
    Location: ${configDir()}/autonomos.pid — respects AUTONOMOS_CONFIG_DIR
    for test isolation.
  - packages/server/package.json: exposes ./run.js, ./pid-file.js, ./configDir.js
    via exports field so the CLI can import subpaths cleanly.

Smoke test results (isolated AUTONOMOS_CONFIG_DIR=/tmp/...):
  ✓ autonomos --help short-circuits before startup work
  ✓ autonomos status (no daemon) → "not running", exit 2
  ✓ autonomos stop (no daemon) → "not running", exit 0 (idempotent)
  ✓ autonomos start --port=7780 → listening, PID file written
  ✓ autonomos status (running) → full state (version/pid/port/uptime), exit 0
  ✓ autonomos stop → SIGTERM → daemon exits cleanly, PID file removed
  ✓ autonomos status (after stop) → "not running", exit 2

Existing make dev / make prod / pm2 paths: untouched (additive only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds OS-native supervisor install for the autonomos daemon. Embeds the plist
and unit templates as TypeScript string constants in the CLI binary (the
Tailscale `install_darwin.go` pattern — single source of truth, no separate
template files to package).

Subcommands:
  autonomos install-service [flags]
    Writes launchd plist (mac) or systemd-user unit (linux), then loads it.
    User-scope by default — no sudo required.
    --prefix=DIR    Write under DIR instead of $HOME (hermetic-test mode)
    --no-activate   Just write the file; skip launchctl/systemctl
    --bin=PATH      Override the auto-detected program path
    --force         Re-install even if the file already exists

  autonomos uninstall-service [flags]
    Stops the daemon (launchctl unload / systemctl --user disable --now),
    then removes the service file. Best-effort — file removal succeeds even
    if supervisor commands fail.
    --prefix=DIR    Same as install-service

Supervision details:
  macOS:   ~/Library/LaunchAgents/com.autonomos.daemon.plist
           RunAtLoad=true, KeepAlive=true (auto-restart on failure)
           Logs at ${configDir()}/logs/autonomos.{log,error.log}

  Linux:   ~/.config/systemd/user/autonomos.service
           Type=simple, Restart=always, RestartSec=5
           Plus `loginctl enable-linger` so the daemon survives logout

Files added:
  packages/cli/src/lib/service-templates.ts   plist + unit string templates
  packages/cli/src/lib/service-paths.ts       path computation + program-args
  packages/cli/src/lib/shell.ts               spawnSync wrapper (no bun deps)
  packages/cli/src/commands/install-service.ts
  packages/cli/src/commands/uninstall-service.ts

Smoke-test results (hermetic --prefix=/tmp/...):
  ✓ install-service writes a structurally-valid plist
  ✓ ProgramArguments contains the binary + "start"
  ✓ Re-install without --force fails with clear error
  ✓ --force allows overwrite
  ✓ uninstall-service removes the file

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the public install path: `curl -fsSL .../install.sh | bash`. Switches the
build to bundle the CLI entry point so the resulting binary supports both
server-startup invocations (Phase 1B's `--embedded --port=0` contract) AND
the new subcommands (status, stop, install-service, etc.).

What ships:

  scripts/install.sh
    - Cross-platform Bash (uname-based platform detection)
    - Supports darwin-arm64 / darwin-x64 / linux-x64 / linux-arm64
    - Downloads tarball + SHA256SUMS from GitHub Releases
    - Verifies checksum before extraction
    - Default install prefix: $HOME/.local (no sudo)
    - Writes wrapper at $PREFIX/bin/autonomos → node $PREFIX/share/autonomos/index.js
    - Warns if $PREFIX/bin is not on PATH
    - Auto-runs `autonomos install-service` post-install (SKIP_INSTALL_SERVICE=1 to skip)
    - Env-var-driven for hermetic testing: INSTALL_PREFIX, BUNDLE_URL, SKIP_NODE_CHECK

  .github/workflows/release.yml
    - Triggered by v* tags or workflow_dispatch
    - Matrix: macos-14 (arm64), macos-13 (x64), ubuntu-latest, ubuntu-24.04-arm
    - Builds dashboard → embeds it → bundles → tarballs each platform
    - Final release job: collects all tarballs, computes SHA256SUMS, publishes
      a GitHub Release with auto-generated notes

  packages/server/build/build-binary.ts
    - Entry point switched from packages/server/src/index.ts to
      packages/cli/src/index.ts. One bundle now contains BOTH the CLI tools
      AND the server runServer() they manage. Phase 1B's spawn contract
      (node index.js --embedded --port=0) preserved — CLI dispatches
      flag-only invocations to runStartCommand.
    - TARBALL=1 env var triggers .tar.gz creation alongside the bundle dir.
      (Bun's CLI eats unknown long-flags before passing to scripts, so an
      env var is more reliable than --tarball.)

Hermetic install.sh verification (BUNDLE_URL=file://...):
  ✓ Platform detection works
  ✓ Tarball downloaded via file:// URL
  ✓ SHA256SUMS verification passes
  ✓ Extraction to test prefix
  ✓ Wrapper script written, executable
  ✓ PATH warning printed when prefix not on PATH
  ✓ Installed binary executes (--help output correct)
  ✓ Bundle structure: index.js + .node natives + _embedded_dashboard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oints

Adds the upgrade flow. Two entry points sharing one implementation:

  CLI:    autonomos upgrade
          - Runs out-of-process, works even when daemon is stopped
          - Reads PID file; SIGTERMs running daemon after swap to trigger
            supervisor restart (no-op if not running under one)

  HTTP:   POST /api/system/upgrade
          - Dashboard "Upgrade" button hook (UI piece is a separate follow-up)
          - Daemon self-restarts ~500ms after responding, assuming supervisor

Plus:
  GET /api/system/version → { version, platform, arch }

Implementation shared via packages/server/src/upgrade.ts (performUpgrade):
  1. Fetch GitHub Releases API (https://api.github.com/repos/.../latest)
  2. Compare current vs latest version (skip if up-to-date)
  3. Locate platform-specific tarball + SHA256SUMS in the release assets
  4. Download to staging dir
  5. Verify SHA256 via node:crypto (no shasum dependency)
  6. Extract tarball to <bundleDir>.new
  7. Atomic swap: rename bundleDir → bundleDir.previous, .new → bundleDir
  8. Return result; caller restarts daemon

The .previous directory is retained for one cycle so users can manually roll
back: `mv share/autonomos.previous share/autonomos`.

Other refactors in this commit:
  - packages/server/src/version.ts NEW: single source of truth for the
    server version, used by PID file + /api/system/version + upgrade flow.
    Reads from the bundle's own package.json when bundled, falls back to
    ../package.json when running from source via tsx.
  - build-binary.ts: writes a minimal package.json next to the bundled
    index.js so the version reader works in compiled installs.
  - run.ts/system.ts/upgrade.ts: all import getServerVersion from one place.

Smoke-test results (against actual installed bundle):
  ✓ Dev-checkout `autonomos upgrade` rejects cleanly with layout error
  ✓ Installed binary reports correct version: "0.0.1" (not "unknown")
  ✓ GitHub API call works (returns 404 today since aterrylu/autonomOS has
    no v* releases yet — will succeed once Phase 1C is tagged)
  ✓ Type-check clean, all package exports resolve via @autonomos/server/*

Deferred to follow-up: dashboard Settings → About UI for the upgrade button.
The /api/system/upgrade endpoint is callable; UI integration can land later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The final piece of Phase 1C. Closes the loop on backward compatibility for
existing pm2-managed users and adds the test infrastructure that catches
regressions on every PR.

What lands:

  packages/cli/src/lib/pm2.ts NEW
    detectPm2Install()   parses `pm2 jlist` JSON for autonomos processes
    migrateFromPm2()     pm2 stop + delete + save  (best-effort, fails loudly)

  packages/cli/src/commands/migrate-from-pm2.ts NEW
    `autonomos migrate-from-pm2` — one-shot migration command:
      1. Detect pm2-managed autonomos (no-op if absent)
      2. Stop + deregister
      3. install-service --force (sets up launchd/systemd-user)
      4. Done — daemon is now under OS-native supervision

  packages/cli/src/commands/install-service.ts MODIFIED
    Refuses to install on top of a pm2-managed autonomos (would race on
    port 3100). Directs the user to `autonomos migrate-from-pm2`. --force
    bypasses for advanced users who know what they're doing.

  scripts/install.sh MODIFIED
    Pre-install-service check: if pm2-managed autonomos is detected, the
    installer auto-runs `migrate-from-pm2` instead of `install-service`.
    Existing users running `curl install.sh | bash` get a clean transition.

  scripts/test-install.sh NEW
    Hermetic end-to-end test of the entire install + CLI lifecycle:
      1. Build dashboard + bundle + tarball
      2. SHA256SUMS
      3. install.sh with BUNDLE_URL=file:// + INSTALL_PREFIX=/tmp/...
      4. Verify wrapper, --help, start, status, /api/host probe, stop
      5. install-service --no-activate under test prefix
      6. Verify plist/unit written
      7. uninstall-service
      8. Verify plist/unit removed
    Doesn't touch ~/.autonomos/, real /Library/LaunchAgents/, or real systemd.

  .github/workflows/test-install.yml NEW
    Matrix: macos-14 (darwin-arm64) + ubuntu-latest (linux-x64)
    Runs scripts/test-install.sh on PR. Catches breakage on a clean OS.

Other cleanups:
  - upgrade.ts: switched node:crypto to top-level import (was require()
    masquerading as ES import; biome flagged it).
  - migrate-from-pm2.ts internally passes --force to install-service so the
    just-migrated user doesn't get caught by the "pm2 detected" check that
    sees stale entries pre-pm2-save.

Verification:
  ✓ Hermetic test passes locally (darwin-arm64)
  ✓ tsc -b clean
  ✓ biome check clean (only pre-existing useTerminal.ts warnings unrelated to 1C)
  ✓ All 5 Phase 1C subcommands work end-to-end:
    autonomos start | stop | status | install-service | uninstall-service |
    upgrade | migrate-from-pm2

Phase 1C as a whole now ships:
  - install.sh — public install path (curl ... | bash)
  - Five subcommands replacing the make-based interface
  - launchd/systemd-user supervision (no pm2, no sudo)
  - GitHub Releases CI pipeline (release.yml, ready for first v* tag)
  - Atomic upgrade flow with .previous rollback dir
  - Hard-cutover pm2 migration for existing users
  - Hermetic install test in CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI matrix runs failed because GitHub Actions runners don't have the
Claude Code CLI installed, and run.ts's provider validation exits the
process when `claude-code` can't be resolved. Test daemon never started,
script timed out waiting for it.

Fixes:

1. When `claude` is missing from PATH, write a tiny shell stub at a temp
   directory and prepend that to PATH. Provider validation finds it and
   the daemon starts. Agent spawning would fail at runtime if invoked,
   but the install/lifecycle tests here don't spawn agents.

2. On failure, dump the last 50 lines of /tmp/autonomos-test-server.log
   in the EXIT trap. Saves a round-trip when diagnosing failures.

3. Cleanup the stub dir in the existing cleanup trap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/release.yml Outdated

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes — the release workflow as written will fail on the first v* tag push because bun build-binary.ts --tarball produces no tarballs (the script only honors TARBALL=1 env, per its own embedded warning comment), so the Upload tarball step will then 404. That breaks the entire distribution path this PR is built around.

Everything else looks solid: CLI subcommand layout is clean, PID file + status state machine is well thought out, the atomic-swap upgrade flow with .previous rollback is sensible, the pm2 migration cleanly hands off to install-service, and the hermetic test (scripts/test-install.sh) actually exercises a real install→start→status→stop→install-service→uninstall-service round-trip on both macOS and Linux.

Minor follow-ups (not blockers, easy to land later):

  • scripts/test-install.sh lines 89–94 — the "Verifying /api/system/version" section never actually calls the endpoint. It parses the token preview from the server log and stops. Since one of Phase 1C's selling points is the new /api/system/* surface, exercising it in CI would be cheap (fetch via the auth-cookie endpoint or via Authorization header — token is in the log).
  • packages/server/src/upgrade.ts performUpgrade — version comparison is equality-only (latestVersion === currentVersion). If the user is on a newer version than latest (manual install / dev build), autonomos upgrade will silently downgrade them. Pre-1.0 this is fine, but a < check would catch it cheaply later.
  • packages/cli/package.json"bin": { "autonomos": "./src/index.ts" } doesn't work via npm install (npm can't exec TS). Phase 1C ships via install.sh + bundle, so this isn't load-bearing today, but it's a footgun if someone tries npm i -g @autonomos/cli later.

Fix the release.yml tarball flag and this is good to ship.

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed Phase 1C end-to-end. Confirming the existing unresolved thread on release.yml:48 is a real, still-present blocker — the workflow passes --tarball as argv, but build-binary.ts:51 only honors TARBALL=1 env var (scripts/test-install.sh:51 does it correctly). On a real v* tag push, no .tar.gz is produced and the Upload tarball step fails on a missing path, leaving the release empty and install.sh with nothing to fetch. The hermetic CI test does not exercise release.yml and so does not catch this.

One-line fix: run: TARBALL=1 bun packages/server/build/build-binary.ts (matching test-install.sh).

Otherwise the design is solid:

  • runServer(argv) extraction + pid-file.ts JSON contract is clean; embedded mode correctly skips the PID file.
  • Atomic upgrade flow (live → .previous, .new → live, .previous retained for manual rollback) is well-thought-out.
  • install-service correctly refuses to overwrite a pm2 install without --force; migration path is auto-triggered from install.sh.
  • detectProgramArgs() handles node-runtime layouts correctly, and the supervisor file's reference to $INSTALL_DIR/index.js continues to resolve after autonomos upgrade swaps the bundle.
  • systemd ExecStart uses POSIX single-quote shell escaping for args; plist uses XML escaping. Good attention to detail.

Submitting as COMMENT rather than APPROVE specifically because the release-pipeline bug, if not caught before the first tag, will block the public install path that is the headline deliverable of this PR. Once that one-line fix lands the rest looks ready to merge.

aterrylu and others added 2 commits May 13, 2026 02:58
The pm2 setup runs autonomos with PORT=3100, but install-service was writing
plists/units without a PORT, so the new daemon defaulted to 3000 — silently
breaking any client pointed at the old port post-migration.

Fix:
  - install-service gets a --port=N flag that appends to the ExecStart
    (becomes "autonomos start --port=3100" in the unit)
  - detectPm2Install() now extracts PORT from pm2's env block
  - migrate-from-pm2 passes the detected port through to install-service

Existing users are now port-preserved automatically across the cutover.
New installs (no pm2 to migrate from) get the 3000 default as before.

Caught while preparing to test on a real pm2-managed install — the failure
mode would have been invisible to CI since the hermetic test doesn't have
a real pre-existing pm2 entry to read PORT from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caught when testing on a real pm2 install: the `make prod` flow does
`bun add -g pm2`, which puts pm2 at $HOME/.bun/bin/pm2 — not on the default
PATH that install.sh inherits when invoked via `curl ... | bash`. Result:
install.sh skipped pm2 detection, install-service skipped it too (same
issue inside the bundled CLI), and two daemons came up side-by-side.

Fix: explicit fallback locations checked in order:
  PATH (command -v) → $HOME/.bun/bin → $HOME/.local/bin → $HOME/.volta/bin
  → /usr/local/bin → /opt/homebrew/bin

Both the bash install.sh and the TS detectPm2Install() share this list.
migrateFromPm2() also re-resolves so all pm2 subcommands (stop, delete, save)
use the same binary.

Real-world test on forge (linux, bun-installed pm2):
  - Pre-fix: pm2-managed autonomos was undetected, both daemons ran
  - Post-fix: TBD (about to retest)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes — the prior critical thread on release.yml:48 (--tarball flag isn't read by build-binary.ts) is still unaddressed, so the first real v* tag push will fail at Upload tarball and the GitHub Release will have no artifacts for install.sh to fetch.

build-binary.ts:51 only consults process.env.TARBALL === "1", and the comment right above it explicitly warns "Bun's CLI eats unknown long-flags, so we use an env var instead." The hermetic test-install.sh:51 correctly uses TARBALL=1 bun …, but the release workflow still passes --tarball as argv. Since test-install.yml doesn't exercise release.yml, this only surfaces on the first tagged release.

Trivial fix — pick one:

  • release.yml:48run: TARBALL=1 bun packages/server/build/build-binary.ts (matches the test script), or
  • teach build-binary.ts to also accept process.argv.includes("--tarball").

Everything else looks good: PID-file + status/stop lifecycle is clean, atomic swap in upgrade.ts is well-structured (keeps .previous for manual rollback), pm2 migration preserves PORT, install-service refuses to overwrite an active pm2 install without --force, hermetic test exercises the end-to-end install flow. Ship it once release.yml is fixed.

Caught on real-world pm2 install on forge: after `pm2 stop autonomos` +
`pm2 delete autonomos`, the migration ran `pm2 save` — which silently
refused with "PM2 is not managing any process, skipping save". pm2's
default save behavior won't overwrite the dump when the current process
list is empty, as a safety measure against accidental wipe.

But that's exactly our case: we just deleted autonomos, so the in-memory
list is empty, and we WANT to persist that emptiness so `pm2 resurrect`
on next boot doesn't bring autonomos back and conflict with the
systemd-user supervisor.

Fix: add --force to the save. Doc comment notes that --force correctly
preserves any OTHER pm2-managed apps in the dump (it dumps current state;
empty current state → empty dump; non-empty current state → only the
remaining apps).

Real-world verification on forge:
  - Pre-fix: dump.pm2 still had autonomos entry; `pm2 resurrect` would
    have re-launched it on boot, racing systemd-user
  - Post-fix: dump.pm2 empty after migration; `pm2 resurrect` is a no-op

CI couldn't catch this because the hermetic test doesn't have pm2 at all,
so there's no pre-existing dump to leak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes — the release workflow as-currently-written won't produce tarballs, so the first v* tag push will publish a release with no install artifacts.

The existing unresolved critical thread on .github/workflows/release.yml:48 (from nox-0x) is still load-bearing: that step runs bun packages/server/build/build-binary.ts --tarball, but build-binary.ts:51 only consults process.env.TARBALL === "1". Bun's CLI eats unknown long flags before forwarding argv, so --tarball is silently dropped, no .tar.gz is produced, and the Upload tarball step fails on a missing file. The hermetic test correctly uses TARBALL=1 and so doesn't exercise this path — it's release-only and won't surface until tag time.

Fix is one line — either swap the workflow step to TARBALL=1 bun packages/server/build/build-binary.ts, or teach build-binary.ts to also accept --tarball via process.argv.includes. Resolve the existing thread once done.

Everything else looks solid:

  • CLI dispatcher / start/stop/status lifecycle is clean, PID-file source-of-truth + isPidAlive fallback is the right pattern.
  • install-service correctly refuses to overlay an active pm2 install absent --force, and migrate-from-pm2 preserves the pm2 PORT into the new supervisor.
  • upgrade.ts SHA256-verifies before atomic swap and keeps a .previous directory for manual rollback — sensible.
  • Hermetic test-install.sh exercises the full install → start → status → /api/host → stop → install-service → uninstall-service loop on the CI matrix, including a stub claude so the provider check doesn't kill the daemon.

No new critical findings beyond the pre-existing thread.

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes — the existing critical unresolved thread on .github/workflows/release.yml:48 is still valid after the four follow-up fixes, and it blocks the entire distribution path on the first real tag push.

release.yml step "Build bundle + tarball" runs bun packages/server/build/build-binary.ts --tarball, but build-binary.ts:51 only honors process.env.TARBALL === "1". The hermetic test (scripts/test-install.sh:51) and package.json's build:binary:all are written consistent with that (the latter uses --all, which the script does check via process.argv.includes("--all") at line 81 — so there's also an internal inconsistency: --all is read from argv but --tarball is intentionally not). On a v* tag push, the build step succeeds, no .tar.gz is produced, and the Upload tarball step fails with "no files found." No release artifacts → install.sh has nothing to fetch.

Fix is one-line — either:

  • run: TARBALL=1 bun packages/server/build/build-binary.ts in release.yml, or
  • add process.argv.includes("--tarball") to wantTarball in build-binary.ts.

Everything else in this PR looks solid:

  • The pm2 migration path (detect → stop → delete → save --force → install-service with preserved PORT) is careful and the four fixup commits address the right edge cases (PATH-less pm2, empty-dump persistence, PORT preservation).
  • PID-file flow, atomic upgrade swap (with .previous for manual rollback), and the launchd/systemd templates (XML-escaped and shell-quoted properly) all look correct.
  • The hermetic test exercises start/stop/status/install-service/uninstall-service end-to-end, including a claude stub for CI environments without the real CLI.
  • runServer's PID-file write is correctly skipped in embedded mode, with cleanup on SIGTERM/SIGINT.

The release-workflow bug is the only blocker. Happy to flip to approve once it's a one-line patch.

Critical (blocker):
  .github/workflows/release.yml — the "Build bundle + tarball" step ran
  `bun packages/server/build/build-binary.ts --tarball`, but build-binary.ts
  only honors TARBALL=1 env var (the comment in that file even says "Bun's
  CLI eats unknown long-flags so we use an env var instead"). On a real v*
  tag push, the build succeeded but produced no .tar.gz, the Upload step
  would have failed on a missing path, and the published release would have
  had no install artifacts for install.sh to fetch. Fix: matches the working
  pattern in test-install.sh — `TARBALL=1 bun packages/server/build/build-binary.ts`.

Non-blocker follow-ups:
  • scripts/test-install.sh — the /api/system/version probe was parsing the
    log preview and stopping. Now actually fetches the endpoint with the
    auth token and validates the version field. Made hermetic from the
    user's real ~/.autonomos/token by setting AUTONOMOS_TOKEN to a per-run
    test value.
  • packages/server/src/upgrade.ts — replace equality check with a tiny
    semver compare (currentVersion >= latestVersion returns up-to-date).
    Prevents `autonomos upgrade` from silently downgrading users on a
    locally-built / ahead-of-release version.
  • packages/cli/package.json — remove the `bin` field pointing at .ts
    (would silently fail under `npm i -g`). Documented why with an inline
    comment so the next person doesn't re-add it without thinking. Phase 1C
    ships exclusively via install.sh; npm-publish path is intentionally not
    supported.

Verification:
  ✓ Local hermetic test passes end-to-end including the new
    /api/system/version assertion
  ✓ tsc -b clean, biome clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aterrylu

Copy link
Copy Markdown
Owner Author

Addressed in commit 93f247e:

Critical (the unresolved blocker):

  • .github/workflows/release.yml:48 — flipped to TARBALL=1 bun packages/server/build/build-binary.ts. You're right that --tarball was getting eaten by bun's CLI before reaching argv — the matching working pattern is in scripts/test-install.sh:51 which uses TARBALL=1, now mirrored in the workflow.

Non-blocker follow-ups (all three):

  • scripts/test-install.sh/api/system/version is now actually fetched and validated, with AUTONOMOS_TOKEN overridden to a per-run test value so the test stays hermetic from the host's ~/.autonomos/token.
  • packages/server/src/upgrade.ts — replaced equality with a tiny compareSemver() helper. currentVersion >= latestVersion now returns up-to-date; downgrade-by-misclick is prevented.
  • packages/cli/package.json — removed the bin field. Phase 1C ships via install.sh + bundle + node wrapper script; pointing bin at .ts would have silently failed under npm i -g. Left an inline _comment_no_bin so the next person doesn't re-add it without reading the trade-off.

Verification:

  • ✓ Local hermetic test passes including the new /api/system/version assertion ({"version":"0.0.1","platform":"darwin","arch":"arm64"})
  • ✓ tsc -b + biome both clean

Ready for re-review.

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — solid end-to-end shipping infrastructure with the prior critical (release.yml TARBALL env) cleanly addressed in 93f247e; no new blocking issues.

Reviewed the full diff:

  • release.yml now uses TARBALL=1 matching test-install.sh. currentTarget() correctly maps each matrix runner to its tarball name, the upload paths line up, and SHA256SUMS is computed in the publish job.
  • upgrade.ts download → SHA256 verify → atomic rename → keep .previous for rollback is correct. The compareSemver downgrade-guard is the right behavior (and the pre-release-stripping limitation is explicitly documented). Cached ESM modules survive the rename, so the post-swap shutdown path won't break from missing files.
  • PID file lifecycle is clean: written on listen in standalone-only, removed on shutdown, defensive cleanup in stop. isPidAlive via kill(pid, 0) is the standard pattern.
  • Service templates — plist XML-escaping + systemd shell-quoting both look correct. process.argv[0] resolves to absolute node path so the supervisor doesn't need PATH lookup for the entry. Auto-detected programArgs is overridable via --bin.
  • pm2 migrationpm2 save --force is correctly used (the comment captures why --force is mandatory after delete). findPm2Binary walks the bun/volta/homebrew dirs that non-interactive curl | bash would otherwise miss. PORT is preserved through to install-service.
  • Hermetic test — the v1.0 critique (parsing log output instead of fetching /api/system/version) is fixed; now actually does an authenticated GET with AUTONOMOS_TOKEN. Service-file written-then-removed assertion runs under --no-activate so it doesn't touch the user's real launchd/systemd.
  • /api/system/upgrade is behind requireAuth. The 500ms setTimeoutprocess.exit(0) flush window is acknowledged in the comment.

Minor follow-up nits (NOT blockers — fine in a later PR):

  • runOrThrow error message includes stderr but not stdout — if launchctl emits diagnostic to stdout we lose it. Cosmetic.
  • During the brief post-rename window before the supervisor restarts, dashboard serveStatic requests will 404 because the captured dashboardDist path no longer resolves. Acceptable transient.
  • release.yml isn't gated by test-install.yml passing — a malformed tag push could publish broken artifacts. The author manually verifies before tagging today; fine for the scale.

PR is mergeable. The "manual verification on existing pm2 setup" checkbox in the description is the right gate before pushing the install endpoint live, but doesn't block the merge itself.

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