@agfpd/iapeer 0.4.64 → 0.4.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/12-cli-reference.md +12 -0
- package/docs/16-tray.md +88 -0
- package/docs/README.md +1 -0
- package/docs/ru/16-tray.md +83 -0
- package/docs/ru/README.md +1 -0
- package/package.json +1 -1
- package/src/cli/index.ts +96 -0
- package/src/daemon/index.ts +32 -0
- package/src/tray/client.ts +266 -0
- package/src/tray/index.ts +146 -0
- package/src/tray/install.ts +282 -0
- package/src/tray/render.ts +243 -0
package/docs/12-cli-reference.md
CHANGED
|
@@ -94,3 +94,15 @@ Commands an agent runs in its own session (require `PEER_IDENTITY` in the enviro
|
|
|
94
94
|
| `run-infra <name> <runtime>` | An infrastructure peer's entry point; launched from its launchd plist. |
|
|
95
95
|
|
|
96
96
|
These two commands are invoked by launchd, not the operator directly.
|
|
97
|
+
|
|
98
|
+
## Tray (menu-bar dashboard)
|
|
99
|
+
|
|
100
|
+
| Command | What it does |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `tray install [--plugin-only]` | Install the SwiftBar fleet dashboard: install SwiftBar.app when absent (owner-sanctioned), write the plugin, launch + refresh. `--plugin-only` writes just the plugin file. Idempotent. |
|
|
103
|
+
| `tray uninstall` | Remove the plugin file + refresh SwiftBar. Never touches the fleet; leaves SwiftBar.app installed. |
|
|
104
|
+
| `tray render [--stream]` | Print the SwiftBar plugin output (`--stream` = the streamable loop the plugin runs). |
|
|
105
|
+
| `tray cmd <command> <peer> [runtime]` | POST a fleet command (`wake`/`stop`/`start`/`new`/`refresh`/`interrupt`/`compact`) — the menu's lifecycle actions. |
|
|
106
|
+
| `tray status` | Read-only: fleet API reachability, SwiftBar install + plugin dir, plugin file presence. |
|
|
107
|
+
|
|
108
|
+
The tray is the first external [Fleet API](15-fleet-api.md) client — see [16 — Tray](16-tray.md).
|
package/docs/16-tray.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 16 — Tray (menu-bar fleet dashboard)
|
|
2
|
+
|
|
3
|
+
A macOS menu-bar dashboard of the fleet — the peer list of `iapeer list` rendered as a
|
|
4
|
+
[SwiftBar](https://github.com/swiftbar/SwiftBar) plugin. It is the **first external
|
|
5
|
+
client of the [Fleet API](15-fleet-api.md)**: it discovers the daemon via `router.json`,
|
|
6
|
+
reads `/fleet/v1/snapshot`, follows `/fleet/v1/events`, and issues commands over
|
|
7
|
+
`/fleet/v1/peers/<peer>/<cmd>` — all over HTTP, against the normative contract, with no
|
|
8
|
+
knowledge of daemon internals. If the daemon and the contract ever diverge, the tray is
|
|
9
|
+
the second consumer that feels it (the first being the daemon itself, which wrote the
|
|
10
|
+
snapshot).
|
|
11
|
+
|
|
12
|
+
Packaging: the tray ships **with the foundation** — it is another face of the daemon, the
|
|
13
|
+
same class as the `iapeer list` TUI, not a separate project. `iapeer install` drops the
|
|
14
|
+
plugin file (inert until SwiftBar is present); `iapeer tray install` is the one-time
|
|
15
|
+
activation that also installs SwiftBar.app when it is absent.
|
|
16
|
+
|
|
17
|
+
## What you see
|
|
18
|
+
|
|
19
|
+
- **Menu bar:** an antenna glyph + the count of peers with a live session. When no
|
|
20
|
+
advertised daemon address answers, a red warning triangle instead (the daemon is down —
|
|
21
|
+
a purely API-fed dashboard cannot show a broken daemon any other way; the built-in TUI
|
|
22
|
+
keeps its direct read path for that case, see [15](15-fleet-api.md)).
|
|
23
|
+
- **Dropdown:** a host header (`iapeer <version> · up <uptime>`) and the memory/voice
|
|
24
|
+
provider heartbeats, then one row per peer:
|
|
25
|
+
- `<peer> <runtime><glyph> … <age>` with the same glyph semantics as the CLI —
|
|
26
|
+
`●` live · `○` asleep · `✕` stopped — green when any runtime is live. Badges: `👤`
|
|
27
|
+
a human is attached, `🔒` launchd-managed (H4 read-only), `⏳N` an ephemeral peer's
|
|
28
|
+
queue depth.
|
|
29
|
+
- **Click a peer → Terminal.app running `iapeer attach <peer>`.** Attach is a terminal
|
|
30
|
+
handoff the API deliberately keeps client-side (a PTY inside a GUI is out of scope);
|
|
31
|
+
the same row's submenu carries the lifecycle actions (Wake / Stop / Start / New /
|
|
32
|
+
Interrupt / Refresh / Compact), each a `POST /fleet/v1/peers/<peer>/<cmd>` via
|
|
33
|
+
`iapeer tray cmd`.
|
|
34
|
+
|
|
35
|
+
The list updates itself: the plugin is **streamable** — it subscribes to the event stream
|
|
36
|
+
and re-fetches the snapshot on every change (a peer waking, an operator stop/start, a
|
|
37
|
+
death), coalescing bursts. A ~15 s heartbeat keeps ages fresh and self-heals a missed
|
|
38
|
+
event; the stream reconnects across daemon restarts. No manual refresh needed (there is a
|
|
39
|
+
Refresh item anyway).
|
|
40
|
+
|
|
41
|
+
## Verbs
|
|
42
|
+
|
|
43
|
+
| Verb | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `iapeer tray install [--plugin-only]` | Install SwiftBar.app when absent (owner-sanctioned), point it at the plugin dir, write the plugin, launch + refresh. `--plugin-only` writes just the plugin file (no app, no launch — what the foundation install does). Idempotent. |
|
|
46
|
+
| `iapeer tray uninstall` | Remove the plugin file and refresh SwiftBar. **Never touches the fleet** — the daemon, TUI and delivery keep running; SwiftBar.app is left installed (it may host other plugins). |
|
|
47
|
+
| `iapeer tray render [--stream]` | Print the SwiftBar plugin output. `--stream` is the streamable loop the plugin runs; the one-shot form is the poll fallback and the test surface. |
|
|
48
|
+
| `iapeer tray cmd <command> <peer> [runtime]` | POST a fleet command (`wake`/`stop`/`start`/`new`/`refresh`/`interrupt`/`compact`) — the menu's lifecycle actions call this. |
|
|
49
|
+
| `iapeer tray status` | Read-only: is the fleet API up (from `router.json`), is SwiftBar installed and where is its plugin dir, is the plugin file present. Repairs nothing. |
|
|
50
|
+
|
|
51
|
+
## How it is wired
|
|
52
|
+
|
|
53
|
+
The SwiftBar plugin file (`iapeer.10s.sh` in the plugin dir) is a thin shim: SwiftBar
|
|
54
|
+
metadata (`<swiftbar.type>streamable</swiftbar.type>`) plus one line —
|
|
55
|
+
`exec "<~/.local/bin/iapeer>" tray render --stream`. All rendering, streaming and
|
|
56
|
+
command logic lives in the compiled binary (typed, tested); the `.sh` is stable and
|
|
57
|
+
carries no logic, so an `iapeer update` never has to rewrite it.
|
|
58
|
+
|
|
59
|
+
Plugin directory: an already-configured SwiftBar `PluginDirectory` is respected (the
|
|
60
|
+
plugin is installed into the user's own dir); otherwise a dedicated
|
|
61
|
+
`~/.iapeer/tray/plugins/`, and SwiftBar is pointed at it via
|
|
62
|
+
`defaults write com.ameba.SwiftBar PluginDirectory` — a plain-string default SwiftBar
|
|
63
|
+
reads on launch (it is not sandboxed and stores no security-scoped bookmark), so the
|
|
64
|
+
folder picker is skipped.
|
|
65
|
+
|
|
66
|
+
Auth follows the Fleet API: on an open-local host no bearer is needed; when the daemon is
|
|
67
|
+
configured with `IAPEER_BEARER_TOKEN`, the plugin's environment must carry it too.
|
|
68
|
+
|
|
69
|
+
Config: `IAPEER_TRAY_HEARTBEAT_MS` tunes the heartbeat cadence (default 15 s, clamped
|
|
70
|
+
1 s…10 min).
|
|
71
|
+
|
|
72
|
+
## Client obligations
|
|
73
|
+
|
|
74
|
+
Because the tray is a Fleet API client, it obeys the normative rules in
|
|
75
|
+
[15 — Fleet API §Client obligations](15-fleet-api.md): unknown `ev` kinds and unknown
|
|
76
|
+
JSON fields are ignored (additive evolution), events are at-least-once (the snapshot is
|
|
77
|
+
the state, events are change hints), and a missing `fleet: 1` marker in `router.json`
|
|
78
|
+
means "no fleet API" — the tray degrades to the daemon-down state rather than probing
|
|
79
|
+
endpoints.
|
|
80
|
+
|
|
81
|
+
## Boundaries
|
|
82
|
+
|
|
83
|
+
- **GUI teardown ≠ fleet teardown.** Removing the plugin (or quitting SwiftBar) leaves the
|
|
84
|
+
daemon, the fleet and the CLI/TUI fully working — the dashboard is an optional face.
|
|
85
|
+
- **Attach is not an API command** — it is a terminal handoff the client performs.
|
|
86
|
+
- Native app, pet overlay and approval buttons are later phases (a native `iapeer-tray`
|
|
87
|
+
Swift app is considered only if the SwiftBar version proves insufficient); all would be
|
|
88
|
+
further thin clients of the same Fleet API.
|
package/docs/README.md
CHANGED
|
@@ -77,6 +77,7 @@ The sections are layered: from "install and use" to "understand the architecture
|
|
|
77
77
|
- [13 — Architecture and contract](13-architecture-and-contract.md) — for plugin and runtime-package developers
|
|
78
78
|
- [14 — Configuration and logs](14-configuration-and-logs.md) — environment variables, logs, diagnostics
|
|
79
79
|
- [15 — Fleet API](15-fleet-api.md) — the daemon's HTTP surface for operator clients (dashboards, tray apps, scripts): snapshot, event stream, commands
|
|
80
|
+
- [16 — Tray](16-tray.md) — the macOS menu-bar fleet dashboard (SwiftBar plugin), the first external Fleet API client
|
|
80
81
|
|
|
81
82
|
## Status
|
|
82
83
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# 16 — Трей (дашборд флота в меню-баре)
|
|
2
|
+
|
|
3
|
+
Дашборд флота в меню-баре macOS — список пиров `iapeer list`, отрисованный как плагин
|
|
4
|
+
[SwiftBar](https://github.com/swiftbar/SwiftBar). Это **первый внешний клиент
|
|
5
|
+
[Fleet API](15-fleet-api.md)**: он находит демон через `router.json`, читает
|
|
6
|
+
`/fleet/v1/snapshot`, следит за `/fleet/v1/events` и шлёт команды через
|
|
7
|
+
`/fleet/v1/peers/<peer>/<cmd>` — целиком по HTTP, против нормативного контракта, без знания
|
|
8
|
+
внутренностей демона. Если демон и контракт разойдутся, трей — второй потребитель, который
|
|
9
|
+
это почувствует (первый — сам демон, который снапшот и пишет).
|
|
10
|
+
|
|
11
|
+
Упаковка: трей поставляется **вместе с фундаментом** — это ещё одно «лицо» демона, тот же
|
|
12
|
+
класс, что TUI `iapeer list`, а не отдельный проект. `iapeer install` кладёт файл плагина
|
|
13
|
+
(инертен, пока нет SwiftBar); `iapeer tray install` — разовая активация, которая доустановит
|
|
14
|
+
SwiftBar.app, если его нет.
|
|
15
|
+
|
|
16
|
+
## Что видно
|
|
17
|
+
|
|
18
|
+
- **Меню-бар:** глиф-антенна + число пиров с живой сессией. Если ни один объявленный адрес
|
|
19
|
+
демона не отвечает — красный треугольник (демон недоступен; чисто API-дашборд иначе не
|
|
20
|
+
покажет сломанный демон — встроенный TUI держит прямой путь чтения для этого случая, см.
|
|
21
|
+
[15](15-fleet-api.md)).
|
|
22
|
+
- **Выпадашка:** шапка хоста (`iapeer <версия> · up <аптайм>`) и хартбиты провайдеров памяти
|
|
23
|
+
и голоса, затем строка на пира:
|
|
24
|
+
- `<peer> <рантайм><глиф> … <возраст>` с той же семантикой глифов, что в CLI —
|
|
25
|
+
`●` живой · `○` спит · `✕` остановлен — зелёный, если хоть один рантайм живой. Бейджи:
|
|
26
|
+
`👤` подключён человек, `🔒` под launchd (H4 read-only), `⏳N` глубина очереди эфемерного
|
|
27
|
+
пира.
|
|
28
|
+
- **Клик по пиру → Terminal.app с `iapeer attach <peer>`.** Attach — терминальная передача,
|
|
29
|
+
которую API намеренно оставляет клиенту (PTY внутри GUI вне рамок); подменю той же строки
|
|
30
|
+
несёт lifecycle-действия (Wake / Stop / Start / New / Interrupt / Refresh / Compact),
|
|
31
|
+
каждое — `POST /fleet/v1/peers/<peer>/<cmd>` через `iapeer tray cmd`.
|
|
32
|
+
|
|
33
|
+
Список обновляется сам: плагин **streamable** — подписан на поток событий и перечитывает
|
|
34
|
+
снапшот на каждое изменение (пробуждение пира, стоп/старт оператором, смерть), склеивая
|
|
35
|
+
всплески. Хартбит ~15 с держит возрасты свежими и самозалечивает пропущенное событие; поток
|
|
36
|
+
переподключается через рестарты демона. Ручной refresh не нужен (пункт Refresh всё же есть).
|
|
37
|
+
|
|
38
|
+
## Verb'ы
|
|
39
|
+
|
|
40
|
+
| Verb | Что делает |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `iapeer tray install [--plugin-only]` | Доустановит SwiftBar.app, если его нет (санкция владельца), нацелит его на каталог плагинов, запишет плагин, запустит + обновит. `--plugin-only` пишет только файл плагина (без приложения и запуска — так делает foundation-install). Идемпотентно. |
|
|
43
|
+
| `iapeer tray uninstall` | Удалит файл плагина и обновит SwiftBar. **Флот не трогает** — демон, TUI и доставка живут; SwiftBar.app остаётся (может держать другие плагины). |
|
|
44
|
+
| `iapeer tray render [--stream]` | Печатает вывод плагина SwiftBar. `--stream` — streamable-цикл, который крутит плагин; одноразовая форма — поллинг-фолбэк и тест-поверхность. |
|
|
45
|
+
| `iapeer tray cmd <command> <peer> [runtime]` | POST fleet-команды (`wake`/`stop`/`start`/`new`/`refresh`/`interrupt`/`compact`) — lifecycle-пункты меню зовут это. |
|
|
46
|
+
| `iapeer tray status` | Только чтение: жив ли fleet API (из `router.json`), установлен ли SwiftBar и где его каталог плагинов, на месте ли файл плагина. Ничего не чинит. |
|
|
47
|
+
|
|
48
|
+
## Как устроено
|
|
49
|
+
|
|
50
|
+
Файл плагина SwiftBar (`iapeer.10s.sh` в каталоге плагинов) — тонкий шим: метаданные SwiftBar
|
|
51
|
+
(`<swiftbar.type>streamable</swiftbar.type>`) плюс одна строка —
|
|
52
|
+
`exec "<~/.local/bin/iapeer>" tray render --stream`. Вся отрисовка, стриминг и команды живут
|
|
53
|
+
в скомпилированном бинаре (типизировано, покрыто тестами); `.sh` стабилен и логики не несёт,
|
|
54
|
+
поэтому `iapeer update` его не переписывает.
|
|
55
|
+
|
|
56
|
+
Каталог плагинов: уже настроенный `PluginDirectory` SwiftBar уважается (плагин ставится в
|
|
57
|
+
каталог пользователя); иначе — выделенный `~/.iapeer/tray/plugins/`, и SwiftBar нацеливается на
|
|
58
|
+
него через `defaults write com.ameba.SwiftBar PluginDirectory` — обычный строковый default,
|
|
59
|
+
который SwiftBar читает при запуске (он не sandboxed и не хранит security-scoped bookmark),
|
|
60
|
+
поэтому окно выбора папки пропускается.
|
|
61
|
+
|
|
62
|
+
Авторизация — как у Fleet API: на open-local хосте bearer не нужен; если демон настроен с
|
|
63
|
+
`IAPEER_BEARER_TOKEN`, окружение плагина тоже должно его нести.
|
|
64
|
+
|
|
65
|
+
Конфиг: `IAPEER_TRAY_HEARTBEAT_MS` настраивает каденцию хартбита (по умолчанию 15 с, зажато
|
|
66
|
+
1 с…10 мин).
|
|
67
|
+
|
|
68
|
+
## Обязанности клиента
|
|
69
|
+
|
|
70
|
+
Поскольку трей — клиент Fleet API, он соблюдает нормативные правила из
|
|
71
|
+
[15 — Fleet API §Client obligations](15-fleet-api.md): неизвестные виды `ev` и неизвестные
|
|
72
|
+
поля JSON игнорируются (аддитивная эволюция), события at-least-once (снапшот — состояние,
|
|
73
|
+
события — подсказки об изменении), а отсутствие маркера `fleet: 1` в `router.json` означает
|
|
74
|
+
«нет fleet API» — трей деградирует в состояние «демон недоступен», а не зондирует эндпоинты.
|
|
75
|
+
|
|
76
|
+
## Границы
|
|
77
|
+
|
|
78
|
+
- **Снос GUI ≠ снос флота.** Удаление плагина (или выход из SwiftBar) оставляет демон, флот
|
|
79
|
+
и CLI/TUI полностью рабочими — дашборд опционален.
|
|
80
|
+
- **Attach — не команда API**, это терминальная передача, которую выполняет клиент.
|
|
81
|
+
- Нативное приложение, питомец-оверлей и кнопки подтверждений — более поздние фазы (нативное
|
|
82
|
+
Swift-приложение `iapeer-tray` рассматривается, только если версии на SwiftBar не хватит);
|
|
83
|
+
все они — тоже тонкие клиенты того же Fleet API.
|
package/docs/ru/README.md
CHANGED
|
@@ -77,6 +77,7 @@ iapeer send assistant --message "привет, ты на связи?"
|
|
|
77
77
|
- [13 — Архитектура и контракт](13-архитектура-и-контракт.md) — для разработчиков плагинов и рантайм-пакетов
|
|
78
78
|
- [14 — Конфигурация и журналы](14-конфигурация-и-журналы.md) — переменные окружения, журналы, диагностика
|
|
79
79
|
- [15 — Fleet API](15-fleet-api.md) — HTTP-поверхность демона для операторских клиентов (дашборды, tray-приложения, скрипты): снапшот, поток событий, команды
|
|
80
|
+
- [16 — Трей](16-tray.md) — дашборд флота в меню-баре macOS (плагин SwiftBar), первый внешний клиент Fleet API
|
|
80
81
|
|
|
81
82
|
## Статус
|
|
82
83
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -1165,7 +1165,9 @@ const BOOLEAN_FLAGS: ReadonlySet<string> = new Set([
|
|
|
1165
1165
|
'no-telegram',
|
|
1166
1166
|
'no-voice',
|
|
1167
1167
|
'npx',
|
|
1168
|
+
'plugin-only',
|
|
1168
1169
|
'remove-codesign-identity',
|
|
1170
|
+
'stream',
|
|
1169
1171
|
'yes',
|
|
1170
1172
|
])
|
|
1171
1173
|
|
|
@@ -1275,6 +1277,10 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
1275
1277
|
sig: 'supervisor up|start|attach|list|kill <sess> [runtime]',
|
|
1276
1278
|
desc: 'DARK (cutover Block 2): detach-persistent pty-supervisor PoC port; serves nothing on the live fleet (throwaway validation only)',
|
|
1277
1279
|
},
|
|
1280
|
+
{
|
|
1281
|
+
sig: 'tray install|uninstall|render [--stream]|cmd <c> <peer>|status',
|
|
1282
|
+
desc: 'the macOS menu-bar fleet dashboard (SwiftBar plugin) — first external Fleet API client; install activates it (installs SwiftBar when absent). See docs/16.',
|
|
1283
|
+
},
|
|
1278
1284
|
]
|
|
1279
1285
|
|
|
1280
1286
|
/** Greedy word-wrap to `width` columns. A token longer than `width` keeps its own
|
|
@@ -2275,6 +2281,19 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2275
2281
|
// docs on its OWN install, version = its binary). install runs from source, so
|
|
2276
2282
|
// docs/ sits at <entry>/../../docs. Best-effort: never fails the install.
|
|
2277
2283
|
const docs = scaffoldHostDocs('iapeer', join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'docs'), env)
|
|
2284
|
+
// Ship the tray face WITH the foundation (owner decision — same class as the
|
|
2285
|
+
// `iapeer list` TUI). Plugin-file ONLY: drop/refresh the SwiftBar plugin shim
|
|
2286
|
+
// (into a configured SwiftBar dir if present, else the dedicated ~/.iapeer dir).
|
|
2287
|
+
// Inert without SwiftBar; NEVER auto-installs the GUI here (that is the explicit
|
|
2288
|
+
// `iapeer tray install`). Best-effort — a hiccup never fails the foundation install.
|
|
2289
|
+
let trayLine = ''
|
|
2290
|
+
try {
|
|
2291
|
+
const { installTray } = await import('../tray/index.ts')
|
|
2292
|
+
const t = installTray({ env, installApp: false, launch: false })
|
|
2293
|
+
trayLine = ` tray plugin: ${t.wrote ? 'written' : 'unchanged'} → ${t.pluginFile} (${t.dir})\n`
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
trayLine = ` tray plugin: skipped (${e instanceof Error ? e.message : String(e)})\n`
|
|
2296
|
+
}
|
|
2278
2297
|
const { path: plist, changed: plistChanged } = installDaemonPlist({ env })
|
|
2279
2298
|
const signingLine =
|
|
2280
2299
|
r.signing == null
|
|
@@ -2291,6 +2310,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2291
2310
|
` scaffold: ~/.iapeer/ ensured (peers/, state, logs, cache, runtimes)\n` +
|
|
2292
2311
|
` host doctrine: ${hostDoctrine.path}${hostDoctrine.created ? ' (stub created — fill it in)' : ' (kept)'}\n` +
|
|
2293
2312
|
` docs: ${docs.copied ? docs.dest : `skipped (${docs.reason})`}\n` +
|
|
2313
|
+
trayLine +
|
|
2294
2314
|
` daemon plist ${plistChanged ? 'written' : 'unchanged (byte-identical — no write, no BTM notification)'}: ${plist}\n` +
|
|
2295
2315
|
` (NOT loaded — a live daemon migration is a separate step: launchctl bootstrap gui/$(id -u) ${plist})\n`,
|
|
2296
2316
|
)
|
|
@@ -2469,6 +2489,82 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2469
2489
|
const { runSupervisorCli } = await import('../supervisor/index.ts')
|
|
2470
2490
|
return await runSupervisorCli(rest)
|
|
2471
2491
|
}
|
|
2492
|
+
case 'tray': {
|
|
2493
|
+
// Ф1 of iapeer-tray — the SwiftBar fleet dashboard, an EXTERNAL fleet-API
|
|
2494
|
+
// client (docs/15). Subcommands: render[--stream] · cmd · install · uninstall
|
|
2495
|
+
// · status. The plugin file is a shim that execs `tray render --stream`; all
|
|
2496
|
+
// rendering/streaming lives here (tested TS), the .sh carries only metadata.
|
|
2497
|
+
const tray = await import('../tray/index.ts')
|
|
2498
|
+
const sub = positionals[0]
|
|
2499
|
+
switch (sub) {
|
|
2500
|
+
case 'render': {
|
|
2501
|
+
if (flags.stream === true) {
|
|
2502
|
+
await tray.streamTray(env) // SwiftBar streamable loop — runs until the process is killed
|
|
2503
|
+
return 0
|
|
2504
|
+
}
|
|
2505
|
+
out(await tray.renderTrayOnce(env))
|
|
2506
|
+
return 0
|
|
2507
|
+
}
|
|
2508
|
+
case 'cmd': {
|
|
2509
|
+
const command = positionals[1]
|
|
2510
|
+
const peer = positionals[2]
|
|
2511
|
+
const runtime = positionals[3]
|
|
2512
|
+
if (!command || !peer) {
|
|
2513
|
+
return argErr(errOut, 'usage: iapeer tray cmd <wake|stop|start|new|refresh|interrupt|compact> <peer> [runtime]')
|
|
2514
|
+
}
|
|
2515
|
+
let r: Awaited<ReturnType<typeof tray.trayCmd>>
|
|
2516
|
+
try {
|
|
2517
|
+
r = await tray.trayCmd(env, command, peer, runtime)
|
|
2518
|
+
} catch (e) {
|
|
2519
|
+
errOut(`tray cmd: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
2520
|
+
return 1
|
|
2521
|
+
}
|
|
2522
|
+
out(`${JSON.stringify(r.body)}\n`)
|
|
2523
|
+
return r.ok ? 0 : 1
|
|
2524
|
+
}
|
|
2525
|
+
case 'install': {
|
|
2526
|
+
// Explicit activation verb: installs SwiftBar.app when absent
|
|
2527
|
+
// (owner-sanctioned) + launches, unless --plugin-only (file only).
|
|
2528
|
+
const pluginOnly = flags['plugin-only'] === true
|
|
2529
|
+
const r = tray.installTray({ env, installApp: !pluginOnly, launch: !pluginOnly })
|
|
2530
|
+
out(
|
|
2531
|
+
`tray plugin ${r.wrote ? 'written' : 'unchanged'}: ${r.pluginFile}\n` +
|
|
2532
|
+
` plugin dir: ${r.pluginDir} (${r.dir})\n` +
|
|
2533
|
+
` SwiftBar: ${r.app}${r.appReason ? ` — ${r.appReason}` : ''}\n` +
|
|
2534
|
+
(r.launched ? ' launched + refreshed SwiftBar\n' : '') +
|
|
2535
|
+
(r.app === 'absent' || r.app === 'install-failed'
|
|
2536
|
+
? ' (SwiftBar not active — run `iapeer tray install` to install it, then it appears in the menu bar)\n'
|
|
2537
|
+
: ''),
|
|
2538
|
+
)
|
|
2539
|
+
return 0
|
|
2540
|
+
}
|
|
2541
|
+
case 'uninstall': {
|
|
2542
|
+
const r = tray.uninstallTray({ env, launch: true })
|
|
2543
|
+
out(
|
|
2544
|
+
(r.removed.length ? `removed:\n${r.removed.map(f => ` ${f}`).join('\n')}\n` : 'no tray plugin file found\n') +
|
|
2545
|
+
(r.refreshed ? ' refreshed SwiftBar\n' : '') +
|
|
2546
|
+
' fleet untouched (daemon / TUI / delivery unaffected)\n',
|
|
2547
|
+
)
|
|
2548
|
+
return 0
|
|
2549
|
+
}
|
|
2550
|
+
case 'status': {
|
|
2551
|
+
const s = tray.trayStatus({ env })
|
|
2552
|
+
out(
|
|
2553
|
+
`daemon fleet API: ${s.daemon.fleet ? `up (v${s.daemon.version ?? '?'})` : 'DOWN / no fleet:1'}\n` +
|
|
2554
|
+
` sock: ${s.daemon.sock ?? '—'}\n` +
|
|
2555
|
+
` tcp: ${s.daemon.tcp ?? '—'}\n` +
|
|
2556
|
+
`SwiftBar: ${s.swiftbar.installed ? `installed (plugin dir: ${s.swiftbar.pluginDir ?? 'unset'})` : 'not installed'}\n` +
|
|
2557
|
+
`plugin: ${s.plugin.installed ? s.plugin.path : 'not installed'}\n`,
|
|
2558
|
+
)
|
|
2559
|
+
return 0
|
|
2560
|
+
}
|
|
2561
|
+
default:
|
|
2562
|
+
return argErr(
|
|
2563
|
+
errOut,
|
|
2564
|
+
'usage: iapeer tray <render [--stream] | cmd <command> <peer> [runtime] | install [--plugin-only] | uninstall | status>',
|
|
2565
|
+
)
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2472
2568
|
default: {
|
|
2473
2569
|
// `iapeer <runtime>` (launch) — folder-launch the cwd's peer, ALWAYS fresh. On a TTY this is
|
|
2474
2570
|
// the human "start a fresh session and work in it" verb (parallel to `attach`, which resumes):
|
package/src/daemon/index.ts
CHANGED
|
@@ -351,6 +351,26 @@ export function daemonDiscoveryPath(options: StorageOptions = {}): string {
|
|
|
351
351
|
return join(pluginStateDir('iapeer', options), 'router.json')
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Normalize an HTTP request-target to origin-form (path[+query]) for path routing.
|
|
356
|
+
* A request that traversed an HTTP proxy arrives in ABSOLUTE-FORM (RFC 7230 §5.3.2:
|
|
357
|
+
* "GET http://host/path"), and Node exposes the whole target as `req.url` — so a
|
|
358
|
+
* path check like `req.url.startsWith('/fleet/')` misses and the request wrongly
|
|
359
|
+
* falls to the MCP catch-all (→ 406). Any client with HTTP(S)_PROXY set is affected
|
|
360
|
+
* (Bun's fetch proxies loopback even when NO_PROXY lists 127.0.0.1). Origin-form and
|
|
361
|
+
* unparseable targets pass through unchanged. Exported for tests. */
|
|
362
|
+
export function normalizeRequestTarget(url: string | undefined): string | undefined {
|
|
363
|
+
if (url && /^https?:\/\//i.test(url)) {
|
|
364
|
+
try {
|
|
365
|
+
const u = new URL(url)
|
|
366
|
+
return u.pathname + u.search
|
|
367
|
+
} catch {
|
|
368
|
+
/* unparseable — leave as received */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return url
|
|
372
|
+
}
|
|
373
|
+
|
|
354
374
|
export interface DaemonHandle {
|
|
355
375
|
socketPath?: string
|
|
356
376
|
host?: string
|
|
@@ -498,6 +518,18 @@ export async function startDaemon(opts: StartDaemonOptions = {}): Promise<Daemon
|
|
|
498
518
|
// gate first, then the fleet-API branch (when wired), then stateless MCP —
|
|
499
519
|
// a fresh Server + transport per request, fully isolated.
|
|
500
520
|
const handle = (req: IncomingMessage, res: ServerResponse): void => {
|
|
521
|
+
// RFC 7230 §5.3.2 request-target hygiene (FIRST — before ANY path routing). A
|
|
522
|
+
// request that traversed an HTTP proxy carries the ABSOLUTE-FORM target
|
|
523
|
+
// ("GET http://127.0.0.1:8765/fleet/v1/snapshot"), and Node puts that whole string
|
|
524
|
+
// in req.url — so path routing (/fleet vs /mcp) would miss and fall through to MCP
|
|
525
|
+
// (→ a 406 "must accept text/event-stream" for a plain GET). This bites any client
|
|
526
|
+
// whose environment has HTTP(S)_PROXY set: Bun's fetch (and others) route loopback
|
|
527
|
+
// through the proxy even when NO_PROXY lists 127.0.0.1 (Bun ignores NO_PROXY). We
|
|
528
|
+
// can't fix every client, so the daemon normalizes to origin-form here — a proxied
|
|
529
|
+
// request then routes identically to a direct one, for BOTH the fleet and MCP
|
|
530
|
+
// surfaces. (live-proven: HTTP_PROXY pointed at the daemon itself reproduced the
|
|
531
|
+
// 406; normalization restores /fleet routing.)
|
|
532
|
+
req.url = normalizeRequestTarget(req.url)
|
|
501
533
|
// H8 bearer layer (FIRST, before any MCP/fleet work): when a token is configured,
|
|
502
534
|
// reject anything not carrying `Authorization: Bearer <token>`. Off when unset.
|
|
503
535
|
if (bearer && req.headers.authorization !== `Bearer ${bearer}`) {
|