@agfpd/iapeer 0.4.75 → 0.4.76
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/07-install-and-update.md +1 -1
- package/docs/16-tray.md +9 -1
- package/docs/ru/16-tray.md +9 -2
- package/package.json +1 -1
- package/src/cli/index.ts +14 -2
- package/src/onboard/tray.ts +101 -0
- package/src/tray/render.ts +47 -1
- package/src/tui/onboard/app.tsx +36 -7
- package/src/tui/onboard/run.tsx +26 -1
|
@@ -35,7 +35,7 @@ The same operation is available as `iapeer install` if the binary is already in
|
|
|
35
35
|
iapeer onboard
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`onboard` prepares the host for agents. First of all it **brings up the daemon** — loads the plist into launchd, after which launchd holds the daemon itself, bringing it back up on a crash and a reboot. No separate `launchctl` command is needed for this. Then onboard registers the extension marketplace in Claude and Codex, provisions the scheduler (`notifier`), creates a human peer on Telegram,
|
|
38
|
+
`onboard` prepares the host for agents. First of all it **brings up the daemon** — loads the plist into launchd, after which launchd holds the daemon itself, bringing it back up on a crash and a reboot. No separate `launchctl` command is needed for this. Then onboard registers the extension marketplace in Claude and Codex, provisions the scheduler (`notifier`), creates a human peer on Telegram, installs the memory provider and the voice backend, and offers the menu-bar tray face (SwiftBar). The daemon start always runs; the memory/voice/notifier/telegram steps are on by default and turned off with flags (`--no-notifier`, `--no-telegram`, `--no-memory`, `--no-voice`). The **tray** is the exception — default **off** (it pulls in a GUI app via Homebrew), opted into with `--tray` on the non-interactive path or a `[y/N]` prompt in the interactive wizard; without Homebrew it degrades to a soft skip (the plugin is left in place, install SwiftBar manually with `iapeer tray install --app`).
|
|
39
39
|
|
|
40
40
|
Check the daemon is alive and the host is ready:
|
|
41
41
|
|
package/docs/16-tray.md
CHANGED
|
@@ -42,8 +42,16 @@ activation that also installs SwiftBar.app when it is absent.
|
|
|
42
42
|
own `terminal=true`, whose Terminal launch drives a `Cmd-T` System-Events keystroke that
|
|
43
43
|
silently no-ops unless SwiftBar holds Accessibility permission. **Launchd-managed infra
|
|
44
44
|
peers (telegram / notifier, `🔒`) are not pty-attachable** — their rows are plain status
|
|
45
|
-
lines. Lifecycle actions (
|
|
45
|
+
lines. Lifecycle actions (Stop / Start / New / Interrupt / Refresh / Compact) live
|
|
46
46
|
in the **Manage** submenu, each a `POST /fleet/v1/peers/<peer>/<cmd>` via `iapeer tray cmd`.
|
|
47
|
+
(No Wake item — a peer-row click already wakes a sleeping peer before it attaches, so
|
|
48
|
+
it was redundant.) For an agentic peer the Manage submenu also carries an
|
|
49
|
+
**approval-mode toggle** (docs/17): it reads the current mode and flips it via the local
|
|
50
|
+
`iapeer approval-mode <peer> <mode>` verb, applied on the peer's **next fresh session**
|
|
51
|
+
(a click never respawns a live session). The friction is asymmetric to the security
|
|
52
|
+
weight: `yolo → gated` (adds human approval) is one safe tap; `gated → yolo` (REMOVES the
|
|
53
|
+
approval perimeter) sits behind the submenu as an explicit red ⚠ confirm, so it is never
|
|
54
|
+
dropped by a stray click.
|
|
47
55
|
- **Pending approvals (docs/17):** when the queue is non-empty, the **top of the dropdown** shows
|
|
48
56
|
each pending request **expanded, with no extra clicks** — a header (`<peer> · <tool>`), the
|
|
49
57
|
**verbatim action content** (the command / diff / plan, monospace, capped for the menu; the FULL
|
package/docs/ru/16-tray.md
CHANGED
|
@@ -39,8 +39,15 @@ SwiftBar.app, если его нет.
|
|
|
39
39
|
родной `terminal=true` SwiftBar, чей запуск Terminal бьёт `Cmd-T` кейстроком через System
|
|
40
40
|
Events и молча не срабатывает без права Accessibility у SwiftBar. **Launchd-инфра-пиры
|
|
41
41
|
(telegram / notifier, `🔒`) не pty-аттачатся** — их строки просто статус. Lifecycle-действия
|
|
42
|
-
(
|
|
43
|
-
`POST /fleet/v1/peers/<peer>/<cmd>` через `iapeer tray cmd`.
|
|
42
|
+
(Stop / Start / New / Interrupt / Refresh / Compact) — в подменю **Manage**, каждое —
|
|
43
|
+
`POST /fleet/v1/peers/<peer>/<cmd>` через `iapeer tray cmd`. (Пункта Wake нет — клик по строке
|
|
44
|
+
пира и так будит спящего перед аттачем, отдельный Wake был избыточен.) Для агентного пира
|
|
45
|
+
подменю Manage несёт ещё **переключатель approval-режима** (docs/17): читает текущий режим и
|
|
46
|
+
флипает его локальным verb'ом `iapeer approval-mode <peer> <mode>`, применяется на **следующей
|
|
47
|
+
свежей сессии** пира (клик никогда не респавнит живую сессию). Трение асимметрично весу
|
|
48
|
+
безопасности: `yolo → gated` (добавляет human-approval) — один безопасный тап; `gated → yolo`
|
|
49
|
+
(СНИМАЕТ периметр approval) спрятан в подменю как явный красный ⚠ confirm, чтобы его не сбросить
|
|
50
|
+
случайным кликом.
|
|
44
51
|
- **Ожидающие подтверждения (docs/17):** когда очередь непуста, **вверху выпадашки** каждый
|
|
45
52
|
ожидающий запрос показан **раскрытым, без доп. кликов** — заголовок (`<peer> · <tool>`),
|
|
46
53
|
**дословное содержимое действия** (команда / диф / план, моноширинно, усечено для меню; ПОЛНОЕ
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -1168,6 +1168,7 @@ const BOOLEAN_FLAGS: ReadonlySet<string> = new Set([
|
|
|
1168
1168
|
'plugin-only',
|
|
1169
1169
|
'remove-codesign-identity',
|
|
1170
1170
|
'stream',
|
|
1171
|
+
'tray',
|
|
1171
1172
|
'yes',
|
|
1172
1173
|
])
|
|
1173
1174
|
|
|
@@ -1219,8 +1220,8 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
1219
1220
|
{ sig: 'help | --help | -h', desc: 'print this usage (works appended to any verb; executes nothing)' },
|
|
1220
1221
|
{ sig: 'daemon [--install-plist]', desc: 'run the host-wide HTTP-MCP router (launchd-held)' },
|
|
1221
1222
|
{
|
|
1222
|
-
sig: 'onboard [--accept-risk] [--dry-run] [--no-notifier] [--no-telegram] [--telegram-human <p>] [--telegram-user-id <id>] [--no-memory] [--memory <pkg>] [--no-voice] [--voice <pkg>] [--infra <csv>]',
|
|
1223
|
-
desc: 'backbone host-phase: marketplace → notifier → telegram (human peer) → memory → voice (
|
|
1223
|
+
sig: 'onboard [--accept-risk] [--dry-run] [--no-notifier] [--no-telegram] [--telegram-human <p>] [--telegram-user-id <id>] [--no-memory] [--memory <pkg>] [--no-voice] [--voice <pkg>] [--tray] [--infra <csv>]',
|
|
1224
|
+
desc: 'backbone host-phase: marketplace → notifier → telegram (human peer) → memory → voice (default YES) → tray (default NO, --tray opts in). --accept-risk (or IAPEER_ACCEPT_RISK=1) accepts the security warning non-interactively',
|
|
1224
1225
|
},
|
|
1225
1226
|
{ sig: 'status', desc: 'host snapshot: version, daemon health, memory + voice slots (<provider> | none)' },
|
|
1226
1227
|
{ sig: 'live-runtime <peer>', desc: 'print the peer\'s CURRENT live runtime (freshest pane-log among pid-alive sessions; NOT default_runtime). Exit 1 + no output if none alive' },
|
|
@@ -1526,6 +1527,17 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1526
1527
|
})
|
|
1527
1528
|
const voiceLabel = voice.provider ? `${voice.provider.provider} ${voice.provider.version}` : 'none'
|
|
1528
1529
|
out(`voice: ${voice.state}${voice.detail ? ` — ${voice.detail}` : ''} (slot: ${voiceLabel})\n`)
|
|
1530
|
+
// Tray face (SwiftBar menu-bar fleet dashboard): OPTIONAL and DEFAULT-NO on the
|
|
1531
|
+
// linear path (a GUI dependency — conservatively opt-in). `--tray` opts in;
|
|
1532
|
+
// without it the step is skipped (non-interactive never hangs — the step asks
|
|
1533
|
+
// nothing). The plugin FILE is already dropped by install; this is GUI activation.
|
|
1534
|
+
const { onboardTrayStep } = await import('../onboard/tray.ts')
|
|
1535
|
+
const tray = await onboardTrayStep({
|
|
1536
|
+
skip: flags.tray !== true,
|
|
1537
|
+
dryRun: flags['dry-run'] === true,
|
|
1538
|
+
env,
|
|
1539
|
+
})
|
|
1540
|
+
out(`tray: ${tray.state}${tray.detail ? ` — ${tray.detail}` : ''}\n`)
|
|
1529
1541
|
// Runtime auth readiness (clean-host prerequisite): a peer runs the runtime's
|
|
1530
1542
|
// interactive TUI; the launcher auto-clears first-run modals (theme/trust) but
|
|
1531
1543
|
// CANNOT complete a login OAuth flow. Warn for every INSTALLED runtime (marketplace
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Tray-face onboard step (the SwiftBar menu-bar fleet dashboard).
|
|
2
|
+
//
|
|
3
|
+
// Host-phase, OPTIONAL, DEFAULT-NO (a GUI dependency — conservative: do not pull it
|
|
4
|
+
// onto the host without an explicit yes). This is a thin WRAPPER over the existing
|
|
5
|
+
// activation `installTray` (src/tray/install.ts) — the SAME mechanics as `iapeer tray
|
|
6
|
+
// install --app`: brew cask + dequarantine + launch + refresh — dressed in an
|
|
7
|
+
// onboard-shaped result vocabulary, with a SOFT no-Homebrew path (report + continue,
|
|
8
|
+
// NEVER fail the onboard) per the phase principle "unavailability is a soft skip".
|
|
9
|
+
//
|
|
10
|
+
// IDEMPOTENT: SwiftBar already installed → NOT reinstalled, only the plugin is
|
|
11
|
+
// (re)activated + launched. The plugin FILE itself is dropped by the foundation
|
|
12
|
+
// install step regardless (inert without SwiftBar); this step is the GUI activation.
|
|
13
|
+
//
|
|
14
|
+
// Outcome semantics: this step NEVER fails the onboard exit code — no tray is a fully
|
|
15
|
+
// valid state regardless of why (skipped / no brew / cask failure). Mirrors
|
|
16
|
+
// onboard/voice.ts deliberately (same shape), minus the provider-slot concepts.
|
|
17
|
+
|
|
18
|
+
import { isExecutable } from './index.ts'
|
|
19
|
+
import { installTray, swiftBarInstalled, type Runner } from '../tray/install.ts'
|
|
20
|
+
|
|
21
|
+
export interface TrayOnboardOptions {
|
|
22
|
+
/** --no-tray (wizard) / absence of --tray (linear): skip the step entirely. */
|
|
23
|
+
skip?: boolean
|
|
24
|
+
dryRun?: boolean
|
|
25
|
+
env?: NodeJS.ProcessEnv
|
|
26
|
+
/** Injectable side-effect runner (tests). Default: installTray's own spawnSync. */
|
|
27
|
+
run?: Runner
|
|
28
|
+
/** Injectable SwiftBar.app presence probe (tests). Default swiftBarInstalled(). */
|
|
29
|
+
probeApp?: () => boolean
|
|
30
|
+
/** Injectable Homebrew-availability probe (tests). Default: `brew` on PATH. */
|
|
31
|
+
brewAvailable?: () => boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TrayOnboardResult {
|
|
35
|
+
state:
|
|
36
|
+
| 'installed' // SwiftBar.app installed (brew cask) + plugin activated + launched
|
|
37
|
+
| 'activated' // SwiftBar already present → plugin activated + launched (idempotent, no reinstall)
|
|
38
|
+
| 'skipped-flag' // --no-tray / not opted-in
|
|
39
|
+
| 'skipped-no-brew' // SwiftBar absent AND no Homebrew — soft skip (plugin dropped inert; guidance given)
|
|
40
|
+
| 'install-failed' // brew ran and failed (network/cask) — soft (reported; plugin still lands)
|
|
41
|
+
| 'dry-run'
|
|
42
|
+
detail?: string
|
|
43
|
+
/** The resolved plugin file, when known. */
|
|
44
|
+
pluginFile?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DOCS = 'docs/16-tray.md'
|
|
48
|
+
|
|
49
|
+
export async function onboardTrayStep(opts: TrayOnboardOptions = {}): Promise<TrayOnboardResult> {
|
|
50
|
+
const env = opts.env ?? process.env
|
|
51
|
+
if (opts.skip) return { state: 'skipped-flag' }
|
|
52
|
+
const probeApp = opts.probeApp ?? swiftBarInstalled
|
|
53
|
+
const brewAvailable = opts.brewAvailable ?? ((): boolean => isExecutable('brew', env))
|
|
54
|
+
|
|
55
|
+
if (opts.dryRun) {
|
|
56
|
+
const present = probeApp()
|
|
57
|
+
return {
|
|
58
|
+
state: 'dry-run',
|
|
59
|
+
detail: present
|
|
60
|
+
? 'SwiftBar present — would activate the fleet plugin + launch'
|
|
61
|
+
: brewAvailable()
|
|
62
|
+
? 'would install SwiftBar.app (brew cask) + activate the fleet plugin + launch'
|
|
63
|
+
: 'no Homebrew — would drop the plugin inert + advise manual SwiftBar install',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const present = probeApp()
|
|
68
|
+
|
|
69
|
+
// SwiftBar absent AND no Homebrew → SOFT skip. The auto-install path is brew-only, so
|
|
70
|
+
// without it there is nothing to run; drop the plugin inert (it activates the moment
|
|
71
|
+
// SwiftBar appears), guide the user, and NEVER fail the onboard.
|
|
72
|
+
if (!present && !brewAvailable()) {
|
|
73
|
+
const r = installTray({ env, installApp: false, launch: false, run: opts.run, probeApp: opts.probeApp })
|
|
74
|
+
return {
|
|
75
|
+
state: 'skipped-no-brew',
|
|
76
|
+
pluginFile: r.pluginFile,
|
|
77
|
+
detail:
|
|
78
|
+
'Homebrew not found — the menu-bar app (SwiftBar) needs it. The fleet plugin is in place; ' +
|
|
79
|
+
'install SwiftBar manually (`brew install --cask swiftbar`, or https://swiftbar.app), then run ' +
|
|
80
|
+
`\`iapeer tray install --app\`. Details: ${DOCS}`,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Present → activate ONLY (installApp:false — never reinstall). Absent + brew → install the cask.
|
|
85
|
+
const r = installTray({ env, installApp: !present, launch: true, run: opts.run, probeApp: opts.probeApp })
|
|
86
|
+
if (r.app === 'install-failed') {
|
|
87
|
+
return {
|
|
88
|
+
state: 'install-failed',
|
|
89
|
+
pluginFile: r.pluginFile,
|
|
90
|
+
detail:
|
|
91
|
+
`SwiftBar install via brew failed (${r.appReason ?? 'unknown'}) — the fleet plugin is in place; ` +
|
|
92
|
+
`retry with \`iapeer tray install --app\` or install SwiftBar manually (${DOCS}).`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const activatedMsg = r.launched ? 'plugin activated + SwiftBar launched' : 'plugin written (SwiftBar not launched)'
|
|
96
|
+
return {
|
|
97
|
+
state: present ? 'activated' : 'installed',
|
|
98
|
+
pluginFile: r.pluginFile,
|
|
99
|
+
detail: `${activatedMsg}${r.dir === 'existing-swiftbar' ? ' (your SwiftBar plugin dir)' : ''}`,
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/tray/render.ts
CHANGED
|
@@ -144,7 +144,9 @@ const lockRank = (p: TrayPeer): number => (p.launchd_managed ? 0 : 1)
|
|
|
144
144
|
// ── renderers ─────────────────────────────────────────────────────────────────
|
|
145
145
|
|
|
146
146
|
const MANAGE_COMMANDS: Array<{ cmd: string; label: string }> = [
|
|
147
|
-
|
|
147
|
+
// NB: no 'Wake' — a peer-row click (attach) already wakes a sleeping peer before it
|
|
148
|
+
// connects, so a separate Wake item was redundant (owner decision). `tray cmd wake`
|
|
149
|
+
// stays available on the CLI / fleet API; only the menu item is dropped.
|
|
148
150
|
{ cmd: 'stop', label: 'Stop' },
|
|
149
151
|
{ cmd: 'start', label: 'Start' },
|
|
150
152
|
{ cmd: 'new', label: 'New session (fresh)' },
|
|
@@ -295,10 +297,54 @@ function renderManage(peers: TrayPeer[], bin: string): string[] {
|
|
|
295
297
|
}),
|
|
296
298
|
)
|
|
297
299
|
}
|
|
300
|
+
// approval-mode toggle — only for agentic (attachable) peers; meaningless for the
|
|
301
|
+
// human channel / notifier infra (arthur/timer/watcher), which are always yolo.
|
|
302
|
+
if (attachable(p)) out.push(...renderApprovalToggle(p, bin))
|
|
298
303
|
}
|
|
299
304
|
return out
|
|
300
305
|
}
|
|
301
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The per-peer approval-mode control in the Manage submenu (docs/17): READS the current
|
|
309
|
+
* mode and FLIPS it via the local `approval-mode` verb.
|
|
310
|
+
*
|
|
311
|
+
* APPLY = next session, NOT now. The flip persists the mode + brings the runtime
|
|
312
|
+
* surfaces to it; it does NOT pass --now, so it takes effect on the peer's NEXT fresh
|
|
313
|
+
* session. A menu tap must never kill a live session mid-work (a respawn is a heavy,
|
|
314
|
+
* surprising side effect of a click). The item says "(next session)" so the wait is
|
|
315
|
+
* explicit.
|
|
316
|
+
*
|
|
317
|
+
* refresh=true is the ONE deliberate exception to the "no refresh in the streamable
|
|
318
|
+
* menu" rule: the flip runs the local verb (no daemon round-trip → NO SSE event), so
|
|
319
|
+
* the plugin must re-render to reflect the new mode.
|
|
320
|
+
*
|
|
321
|
+
* SECURITY-ASYMMETRIC friction (a mode-flip changes the security PERIMETER — unlike a
|
|
322
|
+
* one-off Deny): yolo→gated STRENGTHENS (adds human approval) → a single safe tap;
|
|
323
|
+
* gated→yolo WEAKENS (REMOVES human approval) → the current mode sits on a submenu and
|
|
324
|
+
* the flip is an explicit red ⚠ confirm CHILD, so the perimeter is never dropped by one
|
|
325
|
+
* stray click. SwiftBar has no native confirm dialog — the nested explicit item IS the
|
|
326
|
+
* confirm.
|
|
327
|
+
*/
|
|
328
|
+
function renderApprovalToggle(p: TrayPeer, bin: string): string[] {
|
|
329
|
+
const flip = (target: 'gated' | 'yolo'): Record<string, string | boolean> => ({
|
|
330
|
+
bash: bin,
|
|
331
|
+
param1: 'approval-mode',
|
|
332
|
+
param2: p.personality,
|
|
333
|
+
param3: target,
|
|
334
|
+
terminal: false,
|
|
335
|
+
refresh: true, // local verb → no SSE event → re-render to show the new mode
|
|
336
|
+
})
|
|
337
|
+
if (p.approval_mode === 'gated') {
|
|
338
|
+
// WEAKEN — current on the parent (no action); the flip is an explicit red confirm child.
|
|
339
|
+
return [
|
|
340
|
+
line(2, 'Approval: gated 🛡', { color: COLOR.warn }),
|
|
341
|
+
line(3, '⚠ Switch to yolo — removes human approval (next session)', { color: COLOR.down, ...flip('yolo') }),
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
// STRENGTHEN (yolo → gated) — safe, one tap.
|
|
345
|
+
return [line(2, 'Approval: yolo → gated 🛡 (next session)', flip('gated'))]
|
|
346
|
+
}
|
|
347
|
+
|
|
302
348
|
// ── approval renderers (docs/17 — the always-on approval channel in the bar) ─────
|
|
303
349
|
|
|
304
350
|
/** Menu-bar pending badge: a red `N.circle.fill` SF Symbol (the iOS-notification look — a red circle
|
package/src/tui/onboard/app.tsx
CHANGED
|
@@ -19,6 +19,8 @@ export interface WizardResult {
|
|
|
19
19
|
code: number
|
|
20
20
|
memoryConsent: boolean
|
|
21
21
|
voiceConsent: boolean
|
|
22
|
+
/** Install the SwiftBar menu-bar tray face (default-NO — a GUI dependency). */
|
|
23
|
+
trayConsent: boolean
|
|
22
24
|
advisories: string[]
|
|
23
25
|
summary: string[]
|
|
24
26
|
telegramConsent: boolean
|
|
@@ -28,7 +30,7 @@ export interface WizardResult {
|
|
|
28
30
|
hostRuntime?: OnboardRuntime
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
type Phase = 'gate' | 'running' | 'telegram' | 'memory' | 'voice' | 'declined' | 'finishing'
|
|
33
|
+
type Phase = 'gate' | 'running' | 'telegram' | 'memory' | 'voice' | 'tray' | 'declined' | 'finishing'
|
|
32
34
|
type StepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'fail'
|
|
33
35
|
interface Step {
|
|
34
36
|
key: string
|
|
@@ -98,16 +100,20 @@ export function OnboardApp({
|
|
|
98
100
|
const [hostRuntime, setHostRuntime] = useState<OnboardRuntime | undefined>(undefined)
|
|
99
101
|
const [telegramConsent, setTelegramConsent] = useState(false)
|
|
100
102
|
const [memoryConsent, setMemoryConsent] = useState(false)
|
|
103
|
+
const [voiceConsent, setVoiceConsent] = useState(false)
|
|
101
104
|
const spin = useSpinnerFrame(phase === 'running')
|
|
102
105
|
|
|
103
|
-
const finish = (
|
|
106
|
+
const finish = (trayConsent: boolean): void => {
|
|
104
107
|
const failed = steps.some(s => s.status === 'fail')
|
|
105
108
|
const summary = steps.map(s => {
|
|
106
109
|
const mark = s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'fail' ? '✗' : '·'
|
|
107
110
|
return ` ${mark} ${s.label}${s.detail ? ` — ${s.detail}` : ''}`
|
|
108
111
|
})
|
|
109
112
|
summary.unshift(failed ? 'Onboard finished with errors:' : 'Onboard complete:')
|
|
110
|
-
|
|
113
|
+
// voiceConsent is committed state by now (its phase completed a render cycle before
|
|
114
|
+
// tray); trayConsent is the last decision, passed directly (its setState wouldn't
|
|
115
|
+
// have flushed within this same input handler).
|
|
116
|
+
onResult({ code: failed ? 1 : 0, memoryConsent, voiceConsent, trayConsent, telegramConsent, advisories, summary, hostRuntime })
|
|
111
117
|
setPhase('finishing')
|
|
112
118
|
setTimeout(() => exit(), 30)
|
|
113
119
|
}
|
|
@@ -152,19 +158,34 @@ export function OnboardApp({
|
|
|
152
158
|
{ isActive: phase === 'memory' },
|
|
153
159
|
)
|
|
154
160
|
|
|
155
|
-
// Voice consent — DEFAULT-YES ([Y/n]: Enter or y → install).
|
|
161
|
+
// Voice consent — DEFAULT-YES ([Y/n]: Enter or y → install). Then → tray.
|
|
156
162
|
useInput(
|
|
157
163
|
(input, key) => {
|
|
158
|
-
if (input === 'n' || input === 'N')
|
|
159
|
-
|
|
164
|
+
if (input === 'n' || input === 'N') {
|
|
165
|
+
setVoiceConsent(false)
|
|
166
|
+
setPhase('tray')
|
|
167
|
+
} else if (input === 'y' || input === 'Y' || key.return) {
|
|
168
|
+
setVoiceConsent(true)
|
|
169
|
+
setPhase('tray')
|
|
170
|
+
}
|
|
160
171
|
},
|
|
161
172
|
{ isActive: phase === 'voice' },
|
|
162
173
|
)
|
|
163
174
|
|
|
175
|
+
// Tray consent — DEFAULT-NO ([y/N]: Enter or n → skip; y → install). A GUI
|
|
176
|
+
// dependency (SwiftBar), so conservatively opt-IN. The LAST consent → finish.
|
|
177
|
+
useInput(
|
|
178
|
+
(input, key) => {
|
|
179
|
+
if (input === 'y' || input === 'Y') finish(true)
|
|
180
|
+
else if (input === 'n' || input === 'N' || key.return || key.escape) finish(false)
|
|
181
|
+
},
|
|
182
|
+
{ isActive: phase === 'tray' },
|
|
183
|
+
)
|
|
184
|
+
|
|
164
185
|
// Declined → exit 1 (no host mutation).
|
|
165
186
|
useEffect(() => {
|
|
166
187
|
if (phase !== 'declined') return
|
|
167
|
-
onResult({ code: 1, memoryConsent: false, voiceConsent: false, telegramConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
|
|
188
|
+
onResult({ code: 1, memoryConsent: false, voiceConsent: false, trayConsent: false, telegramConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
|
|
168
189
|
const t = setTimeout(() => exit(), 30)
|
|
169
190
|
return () => clearTimeout(t)
|
|
170
191
|
}, [phase, onResult, exit])
|
|
@@ -377,6 +398,14 @@ export function OnboardApp({
|
|
|
377
398
|
<Text dimColor> A local text-to-speech / speech-to-text HTTP service. Per-agent voice tools are a separate `iapeer enable`. [Y/n]</Text>
|
|
378
399
|
</Box>
|
|
379
400
|
) : null}
|
|
401
|
+
{phase === 'tray' ? (
|
|
402
|
+
<Box marginTop={1} flexDirection="column">
|
|
403
|
+
<Text bold color="cyan">
|
|
404
|
+
? Install the menu-bar tray face (SwiftBar)?
|
|
405
|
+
</Text>
|
|
406
|
+
<Text dimColor> A live fleet dashboard in your macOS menu bar (installs SwiftBar via Homebrew). [y/N]</Text>
|
|
407
|
+
</Box>
|
|
408
|
+
) : null}
|
|
380
409
|
</Box>
|
|
381
410
|
)
|
|
382
411
|
}
|
package/src/tui/onboard/run.tsx
CHANGED
|
@@ -71,7 +71,7 @@ export async function runOnboardWizard(opts: OnboardWizardOptions = {}): Promise
|
|
|
71
71
|
// wizard only runs on one), off under NO_COLOR.
|
|
72
72
|
const a = makeAnsi(colorEnabled(env, process.stdout))
|
|
73
73
|
|
|
74
|
-
let result: WizardResult = { code: 0, memoryConsent: false, voiceConsent: false, telegramConsent: false, advisories: [], summary: [] }
|
|
74
|
+
let result: WizardResult = { code: 0, memoryConsent: false, voiceConsent: false, trayConsent: false, telegramConsent: false, advisories: [], summary: [] }
|
|
75
75
|
try {
|
|
76
76
|
const app = render(<OnboardApp env={env} onResult={r => (result = r)} />)
|
|
77
77
|
await app.waitUntilExit()
|
|
@@ -164,6 +164,31 @@ export async function runOnboardWizard(opts: OnboardWizardOptions = {}): Promise
|
|
|
164
164
|
'\n' + paintResult('voice', 'skipped', ' — set it up later with `npx @agfpd/voice-connect init`', a) + '\n',
|
|
165
165
|
)
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
if (result.trayConsent) {
|
|
169
|
+
const { onboardTrayStep } = await import('../../onboard/tray.ts')
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
'\n' +
|
|
172
|
+
a.bold('Installing the menu-bar tray face (SwiftBar)…') +
|
|
173
|
+
'\n' +
|
|
174
|
+
a.dim(
|
|
175
|
+
'A live fleet dashboard in the macOS menu bar. Needs Homebrew to auto-install SwiftBar;\n' +
|
|
176
|
+
'without brew the plugin is dropped inert and you install SwiftBar manually (`iapeer tray install --app`).',
|
|
177
|
+
) +
|
|
178
|
+
'\n\n',
|
|
179
|
+
)
|
|
180
|
+
try {
|
|
181
|
+
const t = await onboardTrayStep({ env })
|
|
182
|
+
process.stdout.write('\n' + paintResult('tray', t.state, t.detail ? ` — ${t.detail}` : '', a) + '\n')
|
|
183
|
+
} catch (e) {
|
|
184
|
+
const rest = ` — ${e instanceof Error ? e.message : String(e)} ${a.dim('(activate later: `iapeer tray install --app`)')}`
|
|
185
|
+
process.stdout.write('\n' + paintResult('tray', 'step skipped', rest, a) + '\n')
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
'\n' + paintResult('tray', 'skipped', ' — add it later with `iapeer tray install --app`', a) + '\n',
|
|
190
|
+
)
|
|
191
|
+
}
|
|
167
192
|
} finally {
|
|
168
193
|
process.removeListener('SIGINT', ignore)
|
|
169
194
|
for (const h of prevInt) process.on('SIGINT', h as NodeJS.SignalsListener)
|