storebound watch: store events for every app on the machine (rejected, approved, live, halted, crash threshold, reviews) to ntfy / Telegram / webhook / OS notification, loop or --once, baseline first, events.jsonl; storebound events and MCP events_recent; docs/watch.md; tests

This commit is contained in:
CRTkafa 2026-10-22 22:38:15 +08:00
parent 4fe26326cc
commit 78c541f2b8
16 changed files with 984 additions and 4 deletions

View file

@ -5,7 +5,7 @@ Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered
(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap.
Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools).
**97 commands:** 91 covered, 0 gaps, 6 not for the desktop.
**99 commands:** 91 covered, 0 gaps, 8 not for the desktop.
| command | status | where in the window | methods | owner step |
|---|---|---|---|---|
@ -13,6 +13,8 @@ Owner-only parts are owner actions in the window or owner steps with their termi
| `import` | covered | App folders / first-run guide: Import from fastlane | `host:project_import_fastlane` | |
| `install` | covered | More tools: Install for an AI tool | `agent.install` | |
| `mcp` | not-for-desktop | the stdio server for AI tools; the window itself is the GUI over the same commands | | |
| `watch` | not-for-desktop | a server loop for people and agents (PM2, cron); the window already reads the stores in the background and notifies from the tray | | |
| `events` | not-for-desktop | the log that watch writes for terminals and agents; the window shows its own what-changed feed and tray notifications | | |
| `serve` | not-for-desktop | the JSON-RPC sidecar the window runs on; nothing to click | | |
| `apps` | covered | Accounts (store apps per account, key check) and App folders (list, add, remove) | `host:store_apps_refresh`, `host:account_check`, `host:projects_list`, `host:project_add`, `host:project_remove` | |
| `desktop` | not-for-desktop | opens or installs this window from a terminal | | |

67
docs/watch.md Normal file
View file

@ -0,0 +1,67 @@
# storebound watch
Store events for every app on this machine, for people and agents. Read-only on the stores.
```
storebound watch # loop, every 15 minutes
storebound watch --once # one pass (cron, PM2 restarts)
storebound watch --dir ./my-app # one folder instead of the machine's app list
storebound watch --notify os,ntfy # choose the sinks (default: every configured one)
storebound watch --dry --json # print the events that would go out; nothing is sent or saved
storebound watch test # send one test event to every configured sink
storebound events --since 24h --kind rejected --json
```
Apps come from the machine's app folder list (`storebound apps folders`, the same list as the desktop app: folders
and store-only apps) or from `--dir`. Each app is read with the credentials its commands use (the project's
`.storebound/`, its profile, or the store-only app's account).
Each pass reads release state per store (like `release status`), crash / ANR health (like `health`, at most once an
hour) and new reviews (like `reviews list`), compares with `~/.storebound/watch-state.json` and emits events. The first
pass of an app only records the baseline: no event storm. A store that cannot be read in a pass keeps its last state
and produces no event.
## Events
| kind | when |
| --- | --- |
| `rejected` | a version went from in review to rejected (App Store; link to App Review) |
| `approved` | a version passed review and waits for release (App Store pending developer release) |
| `live` | a version became live, or its rollout started |
| `rollout_halted` | a Play staged rollout was halted, or an App Store phased release paused |
| `crash_threshold` | Play crash or ANR rate (28 d) crossed the bad behavior threshold; a new App Store metric or diagnostic rated poor |
| `review_low` | a new review of 2 stars or less |
| `review_needs_reply` | a new review of 3-5 stars without a reply |
Each event is one JSON object: `{ at, app, store, kind, version, from, to, detail, url }`, where `url` is the absolute
console link for the app. Events are appended to `~/.storebound/events.jsonl` (rotated at 5 MB, one `.1` file kept).
`STOREBOUND_HOME` moves `~/.storebound`. `storebound events` and the MCP tool `events_recent` read the file.
## Sinks
All optional. Configure with environment variables or `~/.storebound/watch.json`; values are never printed.
| sink | variables |
| --- | --- |
| ntfy | `STOREBOUND_NTFY_TOPIC` (required), `STOREBOUND_NTFY_URL` (default `https://ntfy.sh`), `STOREBOUND_NTFY_TOKEN` (optional). Title, Tags and Click headers; Priority high for `rejected` and `rollout_halted` |
| Telegram | `STOREBOUND_TELEGRAM_TOKEN` and `STOREBOUND_TELEGRAM_CHAT` |
| webhook | `STOREBOUND_WEBHOOK_URL` (JSON POST of the event) |
| OS notification | `--notify os`, or `"notify": ["os"]` in watch.json (Windows toast, macOS osascript, Linux notify-send; skipped quietly when unavailable) |
```json
{ "notify": ["os"], "ntfy": { "topic": "..." }, "telegram": { "token": "...", "chat": "..." }, "webhook": { "url": "..." } }
```
Network calls time out after 10 s and are retried once. A failing sink is logged (by name) and never stops the watch.
## On a server with PM2
```
pm2 start "npx -y storebound watch --notify ntfy" --name storebound-watch
pm2 save
```
Put the credentials profiles (`~/.storebound/credentials/`) and the app folder list (`storebound apps add <folder>`)
on that server, and the sink variables in the PM2 environment (`STOREBOUND_NTFY_TOPIC=...`). The loop polls every
`--every` (5m at least, 15m default, +-10 % jitter), backs off after failed passes (up to 4 h), writes one heartbeat
line per pass and stops cleanly on SIGINT / SIGTERM. With cron use `--once`.