@agfpd/iapeer 0.2.99 → 0.3.0
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/README.md +20 -6
- package/package.json +5 -1
- package/src/cli/index.ts +20 -0
- package/src/tui/onboard/app.tsx +260 -0
- package/src/tui/onboard/run.tsx +72 -0
- package/tsconfig.json +3 -1
package/README.md
CHANGED
|
@@ -22,19 +22,33 @@ recipient peer — asleep? the daemon wakes it and delivers on wake
|
|
|
22
22
|
|
|
23
23
|
## Quick start
|
|
24
24
|
|
|
25
|
-
Requirements: **macOS
|
|
25
|
+
Requirements: **macOS** (Linux is on the [Roadmap](#roadmap)) and at least one agent runtime — Claude Code or Codex CLI (each AI peer is a live session of one). The installer handles the rest ([bun](https://bun.sh) and `~/.local/bin` on `PATH`).
|
|
26
|
+
|
|
27
|
+
**Install — one line.** Installs bun if missing, builds the `iapeer` binary, then hands off to the interactive `onboard`:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
curl -fsSL https://agfpd.github.io/iapeer/install.sh | sh
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Prefer not to pipe a script into your shell?** Run the same steps by hand (needs [bun](https://bun.sh) on `PATH`):
|
|
26
34
|
|
|
27
35
|
```sh
|
|
28
|
-
#
|
|
29
|
-
npx @agfpd/iapeer
|
|
36
|
+
# install — the iapeer binary and the ~/.iapeer layout
|
|
37
|
+
npx @agfpd/iapeer # or: bunx @agfpd/iapeer
|
|
30
38
|
|
|
31
|
-
#
|
|
39
|
+
# set up the host — router daemon, scheduler peers, Telegram, shared memory
|
|
32
40
|
iapeer onboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Either path runs `onboard` interactively: it surfaces the security checks (runtime sign-in, macOS Full Disk Access) and asks before installing the memory provider — nothing is auto-approved behind your back. You can read [`install.sh`](./install.sh) before running it, or preview every step with `IAPEER_INSTALL_DRYRUN=1`.
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
**Then create your first agent and talk to it:**
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# create your first agent
|
|
35
49
|
iapeer create assistant --runtime claude
|
|
36
50
|
|
|
37
|
-
#
|
|
51
|
+
# talk to it live in your terminal — detach with Ctrl-] (or just close it; the agent keeps running)
|
|
38
52
|
iapeer attach assistant
|
|
39
53
|
```
|
|
40
54
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "agfpd",
|
|
@@ -56,13 +56,17 @@
|
|
|
56
56
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
57
57
|
"@xterm/addon-serialize": "0.14.0",
|
|
58
58
|
"@xterm/headless": "6.0.0",
|
|
59
|
+
"ink": "^7.1.0",
|
|
59
60
|
"proper-lockfile": "^4.1.2",
|
|
61
|
+
"react": "^19.2.7",
|
|
62
|
+
"react-devtools-core": "^7.0.1",
|
|
60
63
|
"transliteration": "^2.6.1"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@types/bun": "latest",
|
|
64
67
|
"@types/node": "^22.0.0",
|
|
65
68
|
"@types/proper-lockfile": "^4.1.4",
|
|
69
|
+
"@types/react": "^19.2.17",
|
|
66
70
|
"typescript": "^5.6.0"
|
|
67
71
|
}
|
|
68
72
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -1002,6 +1002,26 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1002
1002
|
try {
|
|
1003
1003
|
switch (verb) {
|
|
1004
1004
|
case 'onboard': {
|
|
1005
|
+
// Interactive TUI wizard (Фаза TUI-редизайн) — the DEFAULT for an
|
|
1006
|
+
// interactive `iapeer onboard`. Routed ONLY in a real interactive terminal
|
|
1007
|
+
// and never for an automation path (--accept-risk / --dry-run / --no-memory
|
|
1008
|
+
// / --infra), which stay on the deterministic linear flow below. Escape
|
|
1009
|
+
// hatch: IAPEER_ONBOARD_WIZARD=0 forces the linear path (recovery / scripts
|
|
1010
|
+
// that want the old flow). The wizard owns its own gate screen, so we route
|
|
1011
|
+
// BEFORE the readline gate. It fails closed: no real TTY →
|
|
1012
|
+
// WIZARD_NOT_INTERACTIVE → fall through to linear.
|
|
1013
|
+
const wizardOptOut = /^(0|false|no)$/i.test((env.IAPEER_ONBOARD_WIZARD ?? '').trim())
|
|
1014
|
+
const automationFlag =
|
|
1015
|
+
flags['accept-risk'] === true ||
|
|
1016
|
+
flags['dry-run'] === true ||
|
|
1017
|
+
flags['no-memory'] === true ||
|
|
1018
|
+
typeof flags.infra === 'string'
|
|
1019
|
+
if (!wizardOptOut && !automationFlag && process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
1020
|
+
const { runOnboardWizard, WIZARD_NOT_INTERACTIVE } = await import('../tui/onboard/run.tsx')
|
|
1021
|
+
const wc = await runOnboardWizard({ env })
|
|
1022
|
+
if (wc !== WIZARD_NOT_INTERACTIVE) return wc
|
|
1023
|
+
// not a real interactive TTY after all → linear path below
|
|
1024
|
+
}
|
|
1005
1025
|
// SECURITY GATE (pre-release): the operator must consciously accept the risk of
|
|
1006
1026
|
// beta infra with live agents BEFORE onboard mutates the host. --accept-risk /
|
|
1007
1027
|
// IAPEER_ACCEPT_RISK accepts non-interactively (one-liner installers / CI); a
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// OnboardApp — the interactive onboard wizard (Ink). Owns the interactive +
|
|
2
|
+
// live-progress part and EXITS; the inherited-stdio memory install + the final
|
|
3
|
+
// summary run post-Ink (see run.tsx). Drives the SAME backend functions as the
|
|
4
|
+
// linear non-interactive path (no logic duplication), honoring the phase
|
|
5
|
+
// principles: explicit prompts (what to press), visible progress on otherwise
|
|
6
|
+
// silent steps (daemon start), and a clear next-step instead of a cryptic code.
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useState } from 'react'
|
|
9
|
+
import { Box, Text, useApp, useInput, useStdin } from 'ink'
|
|
10
|
+
import { ONBOARD_SECURITY_WARNING } from '../../onboard/risk.ts'
|
|
11
|
+
import { onboardHost, runtimeAuthNote, tccFullDiskAccessNote, type OnboardRuntime } from '../../onboard/index.ts'
|
|
12
|
+
import { probeFullDiskAccess } from '../../status/index.ts'
|
|
13
|
+
import { ensureDaemonStarted } from '../../daemon/main.ts'
|
|
14
|
+
import { iapeerBinPath } from '../../install/index.ts'
|
|
15
|
+
|
|
16
|
+
/** What the wizard hands back to run.tsx for the post-Ink phase (memory install +
|
|
17
|
+
* summary). */
|
|
18
|
+
export interface WizardResult {
|
|
19
|
+
code: number
|
|
20
|
+
memoryConsent: boolean
|
|
21
|
+
advisories: string[]
|
|
22
|
+
summary: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Phase = 'gate' | 'running' | 'memory' | 'declined' | 'finishing'
|
|
26
|
+
type StepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'fail'
|
|
27
|
+
interface Step {
|
|
28
|
+
key: string
|
|
29
|
+
label: string
|
|
30
|
+
status: StepStatus
|
|
31
|
+
detail?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
35
|
+
|
|
36
|
+
function useSpinnerFrame(active: boolean): string {
|
|
37
|
+
const [i, setI] = useState(0)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!active) return
|
|
40
|
+
const t = setInterval(() => setI(n => (n + 1) % SPINNER.length), 80)
|
|
41
|
+
return () => clearInterval(t)
|
|
42
|
+
}, [active])
|
|
43
|
+
return SPINNER[i]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function glyph(status: StepStatus, spin: string): string {
|
|
47
|
+
switch (status) {
|
|
48
|
+
case 'running':
|
|
49
|
+
return spin
|
|
50
|
+
case 'ok':
|
|
51
|
+
return '✓'
|
|
52
|
+
case 'warn':
|
|
53
|
+
return '!'
|
|
54
|
+
case 'fail':
|
|
55
|
+
return '✗'
|
|
56
|
+
default:
|
|
57
|
+
return '·'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function color(status: StepStatus): string {
|
|
62
|
+
switch (status) {
|
|
63
|
+
case 'ok':
|
|
64
|
+
return 'green'
|
|
65
|
+
case 'warn':
|
|
66
|
+
return 'yellow'
|
|
67
|
+
case 'fail':
|
|
68
|
+
return 'red'
|
|
69
|
+
case 'running':
|
|
70
|
+
return 'cyan'
|
|
71
|
+
default:
|
|
72
|
+
return 'gray'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function OnboardApp({
|
|
77
|
+
env,
|
|
78
|
+
onResult,
|
|
79
|
+
}: {
|
|
80
|
+
env: NodeJS.ProcessEnv
|
|
81
|
+
onResult: (r: WizardResult) => void
|
|
82
|
+
}): React.ReactElement {
|
|
83
|
+
const { exit } = useApp()
|
|
84
|
+
const { isRawModeSupported } = useStdin()
|
|
85
|
+
const [phase, setPhase] = useState<Phase>('gate')
|
|
86
|
+
const [steps, setSteps] = useState<Step[]>([])
|
|
87
|
+
const [advisories, setAdvisories] = useState<string[]>([])
|
|
88
|
+
const spin = useSpinnerFrame(phase === 'running')
|
|
89
|
+
|
|
90
|
+
const finish = (memoryConsent: boolean): void => {
|
|
91
|
+
const failed = steps.some(s => s.status === 'fail')
|
|
92
|
+
const summary = steps.map(s => {
|
|
93
|
+
const mark = s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'fail' ? '✗' : '·'
|
|
94
|
+
return ` ${mark} ${s.label}${s.detail ? ` — ${s.detail}` : ''}`
|
|
95
|
+
})
|
|
96
|
+
summary.unshift(failed ? 'Onboard finished with errors:' : 'Onboard complete:')
|
|
97
|
+
onResult({ code: failed ? 1 : 0, memoryConsent, advisories, summary })
|
|
98
|
+
setPhase('finishing')
|
|
99
|
+
setTimeout(() => exit(), 30)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Gate input — explicit y/N.
|
|
103
|
+
useInput(
|
|
104
|
+
(input, key) => {
|
|
105
|
+
if (input === 'y' || input === 'Y') setPhase('running')
|
|
106
|
+
else if (input === 'n' || input === 'N' || key.escape) setPhase('declined')
|
|
107
|
+
},
|
|
108
|
+
{ isActive: phase === 'gate' },
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Memory consent — DEFAULT-YES ([Y/n]: Enter or y → install).
|
|
112
|
+
useInput(
|
|
113
|
+
(input, key) => {
|
|
114
|
+
if (input === 'n' || input === 'N') finish(false)
|
|
115
|
+
else if (input === 'y' || input === 'Y' || key.return) finish(true)
|
|
116
|
+
},
|
|
117
|
+
{ isActive: phase === 'memory' },
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Declined → exit 1 (no host mutation).
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (phase !== 'declined') return
|
|
123
|
+
onResult({ code: 1, memoryConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
|
|
124
|
+
const t = setTimeout(() => exit(), 30)
|
|
125
|
+
return () => clearTimeout(t)
|
|
126
|
+
}, [phase, onResult, exit])
|
|
127
|
+
|
|
128
|
+
// Running phase — drive the backend steps sequentially with live status.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (phase !== 'running') return
|
|
131
|
+
let cancelled = false
|
|
132
|
+
const set = (k: string, patch: Partial<Step>) =>
|
|
133
|
+
setSteps(prev => prev.map(s => (s.key === k ? { ...s, ...patch } : s)))
|
|
134
|
+
setSteps([
|
|
135
|
+
{ key: 'daemon', label: 'Start router daemon', status: 'pending' },
|
|
136
|
+
{ key: 'market', label: 'Register marketplace (claude, codex)', status: 'pending' },
|
|
137
|
+
{ key: 'auth', label: 'Check runtime sign-in', status: 'pending' },
|
|
138
|
+
{ key: 'fda', label: 'Check macOS Full Disk Access', status: 'pending' },
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
// Backend steps use synchronous spawnSync (launchctl, runtime plugin list) —
|
|
142
|
+
// those block the event loop, so we yield (tick) after each status change so
|
|
143
|
+
// Ink flushes a frame and steps paint as they progress, not all at once.
|
|
144
|
+
// (Intra-step spinner can't animate while a single spawnSync blocks — a known
|
|
145
|
+
// limitation until those backend calls go async; the step still shows.)
|
|
146
|
+
const tick = () => new Promise<void>(r => setTimeout(r, 0))
|
|
147
|
+
|
|
148
|
+
void (async () => {
|
|
149
|
+
const adv: string[] = []
|
|
150
|
+
set('daemon', { status: 'running' })
|
|
151
|
+
await tick()
|
|
152
|
+
try {
|
|
153
|
+
const d = await ensureDaemonStarted({ env })
|
|
154
|
+
const ok = d.state !== 'failed' && d.healthy !== false
|
|
155
|
+
set('daemon', { status: ok ? 'ok' : 'fail', detail: d.detail ?? d.state })
|
|
156
|
+
} catch (e) {
|
|
157
|
+
set('daemon', { status: 'fail', detail: e instanceof Error ? e.message : String(e) })
|
|
158
|
+
}
|
|
159
|
+
if (cancelled) return
|
|
160
|
+
await tick()
|
|
161
|
+
|
|
162
|
+
set('market', { status: 'running' })
|
|
163
|
+
await tick()
|
|
164
|
+
const r = onboardHost({ env })
|
|
165
|
+
const installed: OnboardRuntime[] = []
|
|
166
|
+
for (const m of r.marketplaces) if (m.state !== 'runtime-missing') installed.push(m.runtime)
|
|
167
|
+
const mFail = r.marketplaces.some(m => m.state === 'failed')
|
|
168
|
+
set('market', {
|
|
169
|
+
status: mFail ? 'fail' : 'ok',
|
|
170
|
+
detail: r.marketplaces.map(m => `${m.runtime}:${m.state}`).join(' '),
|
|
171
|
+
})
|
|
172
|
+
if (cancelled) return
|
|
173
|
+
await tick()
|
|
174
|
+
|
|
175
|
+
set('auth', { status: 'running' })
|
|
176
|
+
await tick()
|
|
177
|
+
if (installed.length === 0) {
|
|
178
|
+
set('auth', { status: 'warn', detail: 'no runtime installed' })
|
|
179
|
+
adv.push(
|
|
180
|
+
'⚠ SETUP INCOMPLETE — no agent runtime found. iapeer needs Claude Code or Codex CLI\n' +
|
|
181
|
+
' to run peers. Install one, sign in, then re-run `iapeer onboard`.',
|
|
182
|
+
)
|
|
183
|
+
} else {
|
|
184
|
+
const notes = installed.map(rt => runtimeAuthNote(rt, env)).filter((n): n is string => n != null)
|
|
185
|
+
set('auth', {
|
|
186
|
+
status: notes.length ? 'warn' : 'ok',
|
|
187
|
+
detail: notes.length ? `${notes.length} runtime(s) not signed in` : installed.join(', '),
|
|
188
|
+
})
|
|
189
|
+
adv.push(...notes)
|
|
190
|
+
}
|
|
191
|
+
if (cancelled) return
|
|
192
|
+
|
|
193
|
+
set('fda', { status: 'running' })
|
|
194
|
+
await tick()
|
|
195
|
+
const fda = probeFullDiskAccess(env)
|
|
196
|
+
const tcc = tccFullDiskAccessNote({ fda, binPath: iapeerBinPath(env) })
|
|
197
|
+
set('fda', { status: tcc ? 'warn' : 'ok', detail: fda === true ? 'granted' : tcc ? 'not granted' : 'n/a' })
|
|
198
|
+
if (tcc) adv.push(tcc)
|
|
199
|
+
if (cancelled) return
|
|
200
|
+
|
|
201
|
+
setAdvisories(adv)
|
|
202
|
+
setPhase('memory')
|
|
203
|
+
})()
|
|
204
|
+
|
|
205
|
+
return () => {
|
|
206
|
+
cancelled = true
|
|
207
|
+
}
|
|
208
|
+
}, [phase, env])
|
|
209
|
+
|
|
210
|
+
if (isRawModeSupported === false) {
|
|
211
|
+
return (
|
|
212
|
+
<Text color="yellow">
|
|
213
|
+
iapeer onboard needs an interactive terminal. Open a normal terminal and run: iapeer onboard
|
|
214
|
+
</Text>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (phase === 'gate') {
|
|
219
|
+
return (
|
|
220
|
+
<Box flexDirection="column">
|
|
221
|
+
<Box borderStyle="round" borderColor="yellow" paddingX={1} flexDirection="column">
|
|
222
|
+
<Text color="yellow" bold>
|
|
223
|
+
Security warning — please read.
|
|
224
|
+
</Text>
|
|
225
|
+
<Text>{ONBOARD_SECURITY_WARNING.replace(/^Security warning — please read\.\n\n/, '')}</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
<Box marginTop={1}>
|
|
228
|
+
<Text bold>Continue? </Text>
|
|
229
|
+
<Text dimColor>[y/N] </Text>
|
|
230
|
+
</Box>
|
|
231
|
+
</Box>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (phase === 'declined') {
|
|
236
|
+
return <Text color="red">onboard aborted — risk not accepted.</Text>
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// running | memory | finishing
|
|
240
|
+
return (
|
|
241
|
+
<Box flexDirection="column">
|
|
242
|
+
<Text bold>Setting up the host…</Text>
|
|
243
|
+
<Box flexDirection="column" marginTop={1}>
|
|
244
|
+
{steps.map(s => (
|
|
245
|
+
<Box key={s.key}>
|
|
246
|
+
<Text color={color(s.status)}>{glyph(s.status, spin)} </Text>
|
|
247
|
+
<Text>{s.label}</Text>
|
|
248
|
+
{s.detail ? <Text dimColor> — {s.detail}</Text> : null}
|
|
249
|
+
</Box>
|
|
250
|
+
))}
|
|
251
|
+
</Box>
|
|
252
|
+
{phase === 'memory' ? (
|
|
253
|
+
<Box marginTop={1} flexDirection="column">
|
|
254
|
+
<Text>Install the shared-memory provider (@agfpd/iapeer-memory)?</Text>
|
|
255
|
+
<Text dimColor>It gives your agents a shared knowledge vault. [Y/n] </Text>
|
|
256
|
+
</Box>
|
|
257
|
+
) : null}
|
|
258
|
+
</Box>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// runOnboardWizard — the Ink entry point for the interactive onboard wizard.
|
|
2
|
+
//
|
|
3
|
+
// Boundary (контракт «интерактив только в реальном TTY», фаза TUI-редизайн):
|
|
4
|
+
// the wizard runs ONLY in a real interactive terminal. The CLI routes here only
|
|
5
|
+
// when stdin AND stdout are TTYs and no non-interactive flag is set; install.sh
|
|
6
|
+
// never launches it (it prints a next-step to open a normal terminal). As a
|
|
7
|
+
// belt-and-suspenders guard against the readline-class wedge (Bun raw-mode on a
|
|
8
|
+
// redirect-opened /dev/tty reports isTTY=true but never receives input), we also
|
|
9
|
+
// fail CLOSED here: no real TTY → return WIZARD_NOT_INTERACTIVE so the caller
|
|
10
|
+
// falls back to the linear non-interactive path instead of rendering raw and
|
|
11
|
+
// hanging.
|
|
12
|
+
//
|
|
13
|
+
// Structure: Ink owns the interactive + live-progress part (gate, daemon/
|
|
14
|
+
// marketplace/auth/FDA steps, memory consent) and EXITS. The memory provider
|
|
15
|
+
// install — an INHERITED-STDIO subprocess (it owns its own tty questions) — runs
|
|
16
|
+
// AFTER Ink has unmounted, so the two never share the terminal at once. The final
|
|
17
|
+
// summary is plain text post-Ink (it folds in the memory outcome).
|
|
18
|
+
|
|
19
|
+
import React from 'react'
|
|
20
|
+
import { render } from 'ink'
|
|
21
|
+
import { OnboardApp, type WizardResult } from './app.tsx'
|
|
22
|
+
|
|
23
|
+
/** Sentinel: the environment is not a real interactive TTY — caller should fall
|
|
24
|
+
* back to the linear non-interactive onboard path (never render raw + hang). */
|
|
25
|
+
export const WIZARD_NOT_INTERACTIVE = -1
|
|
26
|
+
|
|
27
|
+
export interface OnboardWizardOptions {
|
|
28
|
+
env?: NodeJS.ProcessEnv
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Render the interactive onboard wizard. Resolves with the process exit code, or
|
|
32
|
+
* WIZARD_NOT_INTERACTIVE when there is no real TTY to drive it. */
|
|
33
|
+
export async function runOnboardWizard(opts: OnboardWizardOptions = {}): Promise<number> {
|
|
34
|
+
const env = opts.env ?? process.env
|
|
35
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
36
|
+
return WIZARD_NOT_INTERACTIVE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let result: WizardResult = { code: 0, memoryConsent: false, advisories: [], summary: [] }
|
|
40
|
+
try {
|
|
41
|
+
const app = render(<OnboardApp env={env} onResult={r => (result = r)} />)
|
|
42
|
+
await app.waitUntilExit()
|
|
43
|
+
} catch {
|
|
44
|
+
return WIZARD_NOT_INTERACTIVE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Post-Ink: the memory provider install owns the terminal (inherited stdio).
|
|
48
|
+
// Ink is fully unmounted now, so there is no contention.
|
|
49
|
+
if (result.memoryConsent) {
|
|
50
|
+
const { onboardMemoryProvider } = await import('../../onboard/memory.ts')
|
|
51
|
+
process.stdout.write('\nInstalling shared-memory provider (@agfpd/iapeer-memory)…\n')
|
|
52
|
+
try {
|
|
53
|
+
const mem = await onboardMemoryProvider({ env })
|
|
54
|
+
const label = mem.provider ? `${mem.provider.provider} ${mem.provider.version}` : 'none'
|
|
55
|
+
process.stdout.write(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${label})\n`)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
process.stdout.write(`memory: step failed — ${e instanceof Error ? e.message : String(e)}\n`)
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
process.stdout.write('\nmemory: skipped — set it up later with `iapeer onboard` or `npx @agfpd/iapeer-memory init`\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Final summary (plain text, post-Ink).
|
|
64
|
+
process.stdout.write('\n')
|
|
65
|
+
for (const line of result.summary) process.stdout.write(line + '\n')
|
|
66
|
+
if (result.advisories.length > 0) {
|
|
67
|
+
process.stdout.write('\n')
|
|
68
|
+
for (const a of result.advisories) process.stdout.write(a + '\n')
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write('\nNext: create your first agent — `iapeer create <name> --runtime claude`\n')
|
|
71
|
+
return result.code
|
|
72
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "bundler",
|
|
6
6
|
"allowImportingTsExtensions": true,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"jsxImportSource": "react",
|
|
7
9
|
"verbatimModuleSyntax": true,
|
|
8
10
|
"noEmit": true,
|
|
9
11
|
"strict": true,
|
|
@@ -14,5 +16,5 @@
|
|
|
14
16
|
"types": ["bun", "node"],
|
|
15
17
|
"lib": ["ESNext"]
|
|
16
18
|
},
|
|
17
|
-
"include": ["src/**/*.ts"]
|
|
19
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
18
20
|
}
|