Phase 1C: server distribution polish (CLI + install + supervision + upgrade) - #170
Conversation
…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>
nox-0x
left a comment
There was a problem hiding this comment.
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.shlines 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.tsperformUpgrade — version comparison is equality-only (latestVersion === currentVersion). If the user is on a newer version thanlatest(manual install / dev build),autonomos upgradewill 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 vianpm 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 triesnpm i -g @autonomos/clilater.
Fix the release.yml tarball flag and this is good to ship.
nox-0x
left a comment
There was a problem hiding this comment.
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.tsJSON contract is clean; embedded mode correctly skips the PID file.- Atomic upgrade flow (live →
.previous,.new→ live,.previousretained for manual rollback) is well-thought-out. install-servicecorrectly refuses to overwrite a pm2 install without--force; migration path is auto-triggered frominstall.sh.detectProgramArgs()handles node-runtime layouts correctly, and the supervisor file's reference to$INSTALL_DIR/index.jscontinues to resolve afterautonomos upgradeswaps 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.
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
left a comment
There was a problem hiding this comment.
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:48→run: TARBALL=1 bun packages/server/build/build-binary.ts(matches the test script), or- teach
build-binary.tsto also acceptprocess.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
left a comment
There was a problem hiding this comment.
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/statuslifecycle is clean, PID-file source-of-truth + isPidAlive fallback is the right pattern. install-servicecorrectly refuses to overlay an active pm2 install absent--force, andmigrate-from-pm2preserves the pm2 PORT into the new supervisor.upgrade.tsSHA256-verifies before atomic swap and keeps a.previousdirectory for manual rollback — sensible.- Hermetic
test-install.shexercises the full install → start → status → /api/host → stop → install-service → uninstall-service loop on the CI matrix, including a stubclaudeso the provider check doesn't kill the daemon.
No new critical findings beyond the pre-existing thread.
nox-0x
left a comment
There was a problem hiding this comment.
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.tsin release.yml, or- add
process.argv.includes("--tarball")towantTarballin 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
.previousfor 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>
|
Addressed in commit 93f247e: Critical (the unresolved blocker):
Non-blocker follow-ups (all three):
Verification:
Ready for re-review. |
nox-0x
left a comment
There was a problem hiding this comment.
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.ymlnow usesTARBALL=1matchingtest-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.tsdownload → SHA256 verify → atomic rename → keep.previousfor rollback is correct. ThecompareSemverdowngrade-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.isPidAliveviakill(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-detectedprogramArgsis overridable via--bin. - pm2 migration —
pm2 save --forceis correctly used (the comment captures why--forceis mandatory after delete).findPm2Binarywalks the bun/volta/homebrew dirs that non-interactivecurl | bashwould otherwise miss. PORT is preserved through toinstall-service. - Hermetic test — the v1.0 critique (parsing log output instead of fetching
/api/system/version) is fixed; now actually does an authenticated GET withAUTONOMOS_TOKEN. Service-file written-then-removed assertion runs under--no-activateso it doesn't touch the user's real launchd/systemd. /api/system/upgradeis behindrequireAuth. The 500mssetTimeout→process.exit(0)flush window is acknowledged in the comment.
Minor follow-up nits (NOT blockers — fine in a later PR):
runOrThrowerror message includes stderr but not stdout — iflaunchctlemits diagnostic to stdout we lose it. Cosmetic.- During the brief post-rename window before the supervisor restarts, dashboard
serveStaticrequests will 404 because the captureddashboardDistpath no longer resolves. Acceptable transient. release.ymlisn't gated bytest-install.ymlpassing — 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.
Summary
Phase 1C of the desktop-app initiative. Polishes the autonomOS Server distribution into something shippable to external users: a public
curl install.sh | bashinstall 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
autonomos start--port/--embeddedflags.autonomos stopautonomos statusautonomos install-serviceautonomos uninstall-serviceautonomos upgradeautonomos migrate-from-pm2install.sh at
scripts/install.sh:$HOME/.localby default (no sudo)install-servicepost-installHTTP API additions:
GET /api/system/version— version + platform + archPOST /api/system/upgrade— server-side upgrade (callable from dashboard; UI button is a follow-up)Release pipeline at
.github/workflows/release.yml:v*tagsArchitecture changes
packages/cli/— autonomos CLI, depends on@autonomos/serverpackages/server/src/run.ts— exportsrunServer(argv); the CLI'sstartsubcommand calls this directly (no subprocess hop). Oldpackages/server/src/index.tsbecomes a thin entry that calls runServer for backward compat with Phase 1B's Electron-spawn contract.packages/server/src/pid-file.ts— JSON PID file at$configDir/autonomos.pid. Written on listen, removed on shutdown. Skipped in embedded mode.packages/server/src/version.ts— single source of truth for the running version.packages/server/src/upgrade.ts— shared upgrade logic used by CLI + HTTP endpoint.packages/server/build/build-binary.ts— now bundlespackages/cli/src/index.ts(the CLI is the top-level entry; it dispatches argv-only invocations torunServerfor the Phase 1B contract). Also writes the bundle's ownpackage.jsonfor runtime version reading.Backward compat for existing pm2 users
The migration is hard cutover, automatic on upgrade:
curl install.sh | bashdetects pm2-managed autonomosautonomos migrate-from-pm2:pm2 stop autonomospm2 delete autonomospm2 saveinstall-service --force(writes plist/unit + activates)install-serviceitself refuses to overwrite an active pm2 install unless--forceis passed — prevents accidental port collisions.Test plan
scripts/test-install.sh):.github/workflows/test-install.yml):tsc -bcleannpx biome check packages/clean (only pre-existing useTerminal.ts warnings unrelated to 1C)make prod/pm2 path is hard to fully exercise in CI.What's NOT in this PR
/api/system/upgradeendpoint exists; UI integration is a small follow-up.Commits (5)
feat(cli): commit 1/5 — packages/cli/ scaffold + start/stop/statusfeat(cli): commit 2/5 — install-service + uninstall-servicefeat: commit 3/5 — install.sh + GitHub Releases workflowfeat: commit 4/5 — autonomos upgrade + /api/system/* endpointsfeat: commit 5/5 — pm2 migration + hermetic test + CI matrixExisting make dev/make prod/pm2 flows: untouched until a user explicitly runs
autonomos upgradeormigrate-from-pm2.🤖 Generated with Claude Code