Commit Graph
3 Commits
Author SHA1 Message Date
adrianbonpin 5ca37fbc79 Make orphaning structurally impossible: the process is the state
The extension used to record the spawned pid in LocalStorage, which meant
two pieces of state had to stay in sync: the record and the process. When
the record was lost (extension reinstalled, storage cleared, a crash
between spawn and save) the process kept holding a sleep assertion that
nothing could ever release — a machine that silently never sleeps. That
happened for real during development.

Fix it by removing the thing that can be lost rather than guarding it:
the process now carries its own identity, so the OS is the single source
of truth.

Each session is tagged in its own command line:

  vicinae-caffeinate:v1:<startedAt>:<flags>   e.g. ...:1789895271765:id

- macOS: caffeinate has no free-form field, so the marker is installed as
  argv[0] via the shell's `exec -a`. exec replaces the shell in place, so
  the pid Node receives is the caffeinate pid. (Verified that both `ps -o
  command=` and `ps -o comm=` surface the custom argv[0].)
- Linux: the marker rides in systemd-inhibit's --why next to a
  human-readable reason.

status is now a `ps` scan for the marker; stop signals every match. This
deletes lib/state.ts and every LocalStorage call — the extension is
stateless, which also retires the Vicinae null-vs-undefined storage quirk
as a hazard for this codebase.

Also:
- Signal the process group (-pid) rather than a single pid. Sessions are
  spawned detached so the child is a group leader, and on Linux
  systemd-inhibit forks a `sleep` that a parent-only signal can leave
  behind.
- start() clears existing sessions first, so assertions cannot stack.
- If the spawn never registers a marker, roll back instead of leaving a
  half-started session.
- Drop the now-meaningless "stale" concept; add `extra` so an unexpected
  duplicate surfaces in the status view instead of being invisible.
- Add describeFlags() to render flag letters for humans.

Verified by deleting the installed extension while a session was active,
restarting Vicinae, then reinstalling: the fresh install saw the leftover
as ACTIVE and stopped it rather than starting a second and orphaning the
first. A user-owned `caffeinate -u -dmi` was untouched throughout.

vici lint, tsc, eslint, 17/17 vitest, vici build all green; live toggle
on/off round-trips cleanly.
2026-09-20 17:08:19 +08:00
adrianbonpin a303e161d3 Reduce to a pure on/off extension
Durations were the only thing keeping a lot of machinery alive, so drop
them and let the dead code go with them.

Removed:
- The "Caffeinate for a Duration" command.
- lib/duration.ts entirely (parseDuration / formatDuration /
  formatRemaining / normalizeDuration / DURATION_PRESETS) plus its test
  file. Every export was reachable only from duration paths.
- The 30s setInterval re-render in the status view. It existed only to
  keep a "27m left" countdown honest, and with no timed sessions there is
  nothing to count down.
- The duration-preset actions in the status view.
- describeOptions(), which only the duration form used to explain itself.
- durationSeconds / expiresAt from the options, state and command
  builder, along with the `-t <seconds>` and `sleep <n>` branches. Both
  platforms now hold the assertion until signalled, which is what "on"
  means here.
- The unused "restarted" member of ToggleResult.

The status view now reports "since <time>" instead of a countdown, and
keeps a Refresh action.

Also fixes a type error the narrower helper surfaced: the status view's
run() helper was typed () => Promise<void> while startCaffeination
returns Promise<CaffeinateState> and stopCaffeination returns
Promise<boolean>.

407 deletions against 57 insertions.

Verified: vici lint, tsc --noEmit, eslint, 8/8 vitest, vici build, and
live in Vicinae — toggle on spawns `caffeinate -i`, toggle off stops it,
and a pre-existing user-owned `caffeinate -u -dmi` is left untouched
throughout.
2026-09-20 17:01:30 +08:00
adrianbonpin 65005b22a6 Add native Vicinae caffeinate extension
Keeps the computer awake from Vicinae. Written against @vicinae/api
directly because the Raycast `coffee` extension cannot work here:

- Its `index` command is `menu-bar` mode, which Vicinae does not support.
- Its `status` command crashes on `Cannot read properties of null
  (reading 'IsManuallyDecafed')` because it gates on
  `LocalStorage.getItem(key) === undefined`. Vicinae resolves a missing
  key to `null`, not `undefined` (verified with a runtime probe).

Sleep prevention is an OS-level assertion, so the extension delegates and
gets out of the way:

- macOS:  caffeinate -i [-d] [-s] [-t <seconds>]
- Linux:  systemd-inhibit --what=idle:sleep --why=<reason> sleep <n|infinity>

The process is spawned detached + unref'd, because Vicinae unloads a
no-view command as soon as its function returns. Timed sessions need no
babysitter: `caffeinate -t` and `sleep <n>` self-terminate.

Commands: toggle (no-view), status (view), caffeinate-for (view).

Two deliberate safety choices:
- Stopping signals the recorded PID rather than running `killall
  caffeinate`, so a caffeinate the user started themselves survives.
  Verified against a real concurrent `caffeinate -u -dmi` process.
- The PID's name is re-checked with `ps -o comm=` before signalling, since
  PIDs get recycled and a stale entry could otherwise kill something
  unrelated.

Also: display-sleep prevention is macOS-only, and the UI says so on Linux
rather than pretending systemd-inhibit can hold the display awake.

Verified: vici lint, tsc --noEmit, eslint, 23/23 vitest, vici build, and
live in Vicinae — toggle starts `caffeinate -i` which survives the
command unloading, toggle again stops only ours, both view commands load
with clean stdout/stderr.
2026-09-20 16:16:57 +08:00