@agfpd/iapeer 0.3.0 → 0.4.1
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 +2 -1
- package/docs/03-peers-runtimes-identity.md +1 -1
- package/docs/06-storage.md +12 -0
- package/docs/ru/03-/320/277/320/270/321/200/321/213-/321/200/320/260/320/275/321/202/320/260/320/271/320/274/321/213-/320/270/320/264/320/265/320/275/321/202/320/270/321/207/320/275/320/276/321/201/321/202/321/214.md +1 -1
- package/docs/ru/06-/321/205/321/200/320/260/320/275/320/265/320/275/320/270/320/265.md +12 -0
- package/package.json +1 -1
- package/src/cli/index.ts +289 -53
- package/src/create/index.ts +38 -0
- package/src/daemon/index.ts +13 -4
- package/src/index.ts +2 -0
- package/src/init/index.ts +31 -0
- package/src/install/index.ts +61 -2
- package/src/launch/composeSystemPrompt.ts +8 -0
- package/src/onboard/memory.ts +28 -6
- package/src/tui/ansi.ts +71 -0
- package/src/tui/onboard/app.tsx +113 -16
- package/src/tui/onboard/run.tsx +116 -18
- package/src/uninstall/index.ts +244 -0
package/src/index.ts
CHANGED
|
@@ -23,6 +23,8 @@ export * from './init/index.ts'
|
|
|
23
23
|
export * from './install/index.ts'
|
|
24
24
|
// Update — the cloud-only deploy path (`iapeer update`: npm latest → rebuild → restart).
|
|
25
25
|
export * from './update/index.ts'
|
|
26
|
+
// Uninstall — symmetric foundation removal (namespace-safe; refuses on a foreign fleet).
|
|
27
|
+
export * from './uninstall/index.ts'
|
|
26
28
|
// Onboard — the host-phase (idempotent marketplace registration in claude + codex).
|
|
27
29
|
export * from './onboard/index.ts'
|
|
28
30
|
// Memory slot — the declarative provider slot (контракт «Слот памяти»): status read + onboard step.
|
package/src/init/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { IapError } from '../core/errors.ts'
|
|
|
21
21
|
import {
|
|
22
22
|
ensureLocalIapScaffold,
|
|
23
23
|
pluginStateDir,
|
|
24
|
+
resolveGlobalRoot,
|
|
24
25
|
writeFileAtomic,
|
|
25
26
|
type StorageOptions,
|
|
26
27
|
} from '../storage/index.ts'
|
|
@@ -245,6 +246,36 @@ export function ensureDoctrineTemplate(cwd: string): { path: string; created: bo
|
|
|
245
246
|
return { path, created: true }
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Create the GLOBAL host-doctrine template `~/.iapeer/IAPEER.md` if absent — the
|
|
251
|
+
* host-wide owner doctrine (Layer-2 GLOBAL in composeSystemPrompt, shared by EVERY
|
|
252
|
+
* peer; the general layer ABOVE each peer's own `.iapeer/IAPEER.md`). It is consumed at
|
|
253
|
+
* launch but was never scaffolded, so a fresh install left no host-doctrine file for the
|
|
254
|
+
* owner to find — asymmetric with the per-peer stub (ensureDoctrineTemplate). This
|
|
255
|
+
* mirrors that stub at host scope (discoverability + consistency). NEVER overwrites an
|
|
256
|
+
* existing host doctrine. Returns {path, created}.
|
|
257
|
+
*/
|
|
258
|
+
export function ensureGlobalDoctrineTemplate(env: NodeJS.ProcessEnv = process.env): { path: string; created: boolean } {
|
|
259
|
+
const root = resolveGlobalRoot(env)
|
|
260
|
+
const path = join(root, IAPEER_DOCTRINE_FILE)
|
|
261
|
+
if (existsSync(path)) return { path, created: false }
|
|
262
|
+
mkdirSync(root, { recursive: true })
|
|
263
|
+
writeFileSync(
|
|
264
|
+
path,
|
|
265
|
+
[
|
|
266
|
+
'# Host doctrine',
|
|
267
|
+
'',
|
|
268
|
+
'<!-- Host-wide rules shared by EVERY peer on this machine — the general layer,',
|
|
269
|
+
" merged into every peer's system prompt at launch ABOVE that peer's own",
|
|
270
|
+
' .iapeer/IAPEER.md. Replace this with your host-wide conventions. An empty',
|
|
271
|
+
' file contributes nothing. -->',
|
|
272
|
+
'',
|
|
273
|
+
].join('\n'),
|
|
274
|
+
{ mode: 0o644 },
|
|
275
|
+
)
|
|
276
|
+
return { path, created: true }
|
|
277
|
+
}
|
|
278
|
+
|
|
248
279
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
280
|
// initPeer — orchestrate: provision (identity + registry + infra plist) + per-peer
|
|
250
281
|
// ecosystem wiring (MCP transport per agentic runtime + doctrine template)
|
package/src/install/index.ts
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// signInstalledBinary (signing.ts) re-signs each install with the
|
|
14
14
|
// stable local identity so the designated requirement — and the grants — survive.
|
|
15
15
|
|
|
16
|
-
import { copyFileSync, existsSync, mkdirSync, renameSync, statSync } from 'fs'
|
|
16
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, renameSync, rmSync, statSync } from 'fs'
|
|
17
17
|
import { homedir } from 'os'
|
|
18
|
-
import { join } from 'path'
|
|
18
|
+
import { basename, join, relative, sep } from 'path'
|
|
19
19
|
import { spawnSync } from 'child_process'
|
|
20
|
+
import { resolveGlobalRoot } from '../storage/index.ts'
|
|
20
21
|
import { signInstalledBinary, type SigningOutcome } from './signing.ts'
|
|
21
22
|
|
|
22
23
|
/** The stable host-wide install path of the `iapeer` binary. Standard user-bin (no
|
|
@@ -95,6 +96,64 @@ export function installIapeer(cliEntrypoint: string, env: NodeJS.ProcessEnv = pr
|
|
|
95
96
|
return { binPath, prevPath, size, signing }
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Scaffold an ecosystem package's docs to the STABLE, versioned, per-package host
|
|
101
|
+
* path `~/.iapeer/docs/<pkg>/` so an agent can read the contract OFFLINE (a compiled
|
|
102
|
+
* binary embeds no docs; the npm tarball's docs/ is discarded after install — proven:
|
|
103
|
+
* `iapeer update` extracts to a temp dir it rm's in a `finally`, and the bunx cache
|
|
104
|
+
* does not retain the package findably). This is the FOUNDATION-OWNED CONVENTION every
|
|
105
|
+
* ecosystem package follows: each copies its OWN docs on its OWN install/update, so a
|
|
106
|
+
* package's on-host docs always match its installed version.
|
|
107
|
+
*
|
|
108
|
+
* Mechanics: copy <docsSource> → ~/.iapeer/docs/<pkg> EXCLUDING internals/ (mirroring
|
|
109
|
+
* the npm `files` exclusion), via an atomic temp-dir swap so a reader never sees a
|
|
110
|
+
* half-copied tree. BEST-EFFORT — a missing source or copy hiccup NEVER fails the
|
|
111
|
+
* install (the binary works without on-host docs); the caller logs the outcome.
|
|
112
|
+
*/
|
|
113
|
+
export function scaffoldHostDocs(
|
|
114
|
+
pkg: string,
|
|
115
|
+
docsSource: string,
|
|
116
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
117
|
+
): { copied: boolean; dest: string; reason?: string } {
|
|
118
|
+
const root = resolveGlobalRoot(env)
|
|
119
|
+
const dest = join(root, 'docs', pkg)
|
|
120
|
+
// Fail-closed sandbox guard (in spirit with assertInstallSandboxIsolated): never write
|
|
121
|
+
// the REAL machine ~/.iapeer under a sandboxed test that forgot to set IAPEER_ROOT.
|
|
122
|
+
// Compared against the ACTUAL OS home (homedir()), not env.HOME — the root IS the docs
|
|
123
|
+
// isolation, so an env.HOME-based check would false-trip when a test legitimately
|
|
124
|
+
// points IAPEER_ROOT at <tmp>/.iapeer (with env.HOME also <tmp>).
|
|
125
|
+
if (env.IAPEER_TEST_SANDBOX === '1' && root === join(homedir(), '.iapeer')) {
|
|
126
|
+
throw new Error(`refusing to scaffold docs into the REAL ${join(root, 'docs')} under IAPEER_TEST_SANDBOX=1 — set IAPEER_ROOT`)
|
|
127
|
+
}
|
|
128
|
+
if (!existsSync(docsSource)) return { copied: false, dest, reason: `docs source not found: ${docsSource}` }
|
|
129
|
+
const tmp = `${dest}.tmp-${process.pid}`
|
|
130
|
+
try {
|
|
131
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
132
|
+
mkdirSync(join(dest, '..'), { recursive: true })
|
|
133
|
+
cpSync(docsSource, tmp, {
|
|
134
|
+
recursive: true,
|
|
135
|
+
// Skip the internals/ subtree (matches package.json files: "!docs/internals";
|
|
136
|
+
// returning false for a directory skips its whole subtree) and macOS .DS_Store
|
|
137
|
+
// cruft (the foundation is macOS-targeted, so every dev/host tree carries it).
|
|
138
|
+
filter: src => {
|
|
139
|
+
if (basename(src) === '.DS_Store') return false
|
|
140
|
+
const rel = relative(docsSource, src)
|
|
141
|
+
return rel !== 'internals' && !rel.startsWith(`internals${sep}`)
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
rmSync(dest, { recursive: true, force: true })
|
|
145
|
+
renameSync(tmp, dest)
|
|
146
|
+
return { copied: true, dest }
|
|
147
|
+
} catch (e) {
|
|
148
|
+
try {
|
|
149
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
150
|
+
} catch {
|
|
151
|
+
/* best-effort cleanup */
|
|
152
|
+
}
|
|
153
|
+
return { copied: false, dest, reason: e instanceof Error ? e.message : String(e) }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
98
157
|
/** The previous-binary path kept by the last install for one-step rollback. */
|
|
99
158
|
export function iapeerPrevBinPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
100
159
|
return `${iapeerBinPath(env)}.prev`
|
|
@@ -103,6 +103,14 @@ export function composeSystemPrompt(input: ComposePromptInput): string {
|
|
|
103
103
|
// The YAML identity block is the only Layer-1 section (synthesized, no marker).
|
|
104
104
|
const sections: string[] = [yaml]
|
|
105
105
|
|
|
106
|
+
// Host-context pointer to the on-host ecosystem docs (FU6). Each package's install
|
|
107
|
+
// scaffolds its own contract docs to the stable, versioned, per-package path
|
|
108
|
+
// ~/.iapeer/docs/<package>/, so an agent can read "how does iapeer do X" offline
|
|
109
|
+
// instead of guessing. Synthesized Layer-1 fact (no marker). Wording: owner-approved.
|
|
110
|
+
sections.push(
|
|
111
|
+
'iapeer reference docs are on this host at ~/.iapeer/docs/ — one folder per package (start at iapeer/README.md). Consult them for how iapeer works instead of guessing.',
|
|
112
|
+
)
|
|
113
|
+
|
|
106
114
|
// Layer 2 — IAPEER.md doctrine, GLOBAL then LOCAL (general → specific), each its
|
|
107
115
|
// OWN marked file-section. The bash existence-gate (present-but-empty global → a
|
|
108
116
|
// bare "\n") is dropped deliberately: an empty doctrine file now contributes no
|
package/src/onboard/memory.ts
CHANGED
|
@@ -25,11 +25,26 @@ export interface MemoryOnboardOptions {
|
|
|
25
25
|
skip?: boolean
|
|
26
26
|
/** --memory <pkg>: override the provider package (default @agfpd/iapeer-memory). */
|
|
27
27
|
package?: string
|
|
28
|
+
/** Host agentic runtime to thread to the provider init (`--runtime <rt>`) — the
|
|
29
|
+
* onboard↔init contract: the provider provisions its role-peers (index/watcher/
|
|
30
|
+
* timer) on THIS runtime instead of guessing. Omitted on a no-runtime host →
|
|
31
|
+
* the provider degrades (base+BM25, role-peers skipped). */
|
|
32
|
+
runtime?: string
|
|
28
33
|
dryRun?: boolean
|
|
29
34
|
env?: NodeJS.ProcessEnv
|
|
35
|
+
/** Wall-clock backstop (ms) for the provider-init subprocess — a hung provider
|
|
36
|
+
* (e.g. a stalled native build) must NEVER wedge onboard (memory is optional).
|
|
37
|
+
* The interactive wizard passes a generous value; the linear path leaves it
|
|
38
|
+
* unset (no timeout). */
|
|
39
|
+
timeoutMs?: number
|
|
30
40
|
/** Injectable runner (tests). Default: availability probe + `npx -y <pkg> init …`
|
|
31
41
|
* with inherited stdio. */
|
|
32
|
-
runInit?: (
|
|
42
|
+
runInit?: (
|
|
43
|
+
pkg: string,
|
|
44
|
+
args: string[],
|
|
45
|
+
env: NodeJS.ProcessEnv,
|
|
46
|
+
timeoutMs?: number,
|
|
47
|
+
) => { status: number | null; unavailable: boolean }
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
export interface MemoryOnboardResult {
|
|
@@ -49,17 +64,24 @@ function defaultRunInit(
|
|
|
49
64
|
pkg: string,
|
|
50
65
|
args: string[],
|
|
51
66
|
env: NodeJS.ProcessEnv,
|
|
67
|
+
timeoutMs?: number,
|
|
52
68
|
): { status: number | null; unavailable: boolean } {
|
|
53
69
|
// Availability probe FIRST (cheap, side-effect-free): an unpublished package /
|
|
54
70
|
// no network → soft-skip per the contract (the provider's release order must
|
|
55
|
-
// never block the core's onboard).
|
|
56
|
-
const probe = spawnSync('npm', ['view', `${pkg}@latest`, 'version'], { encoding: 'utf8', env })
|
|
71
|
+
// never block the core's onboard). Bounded so the probe itself cannot hang.
|
|
72
|
+
const probe = spawnSync('npm', ['view', `${pkg}@latest`, 'version'], { encoding: 'utf8', env, timeout: 60_000 })
|
|
57
73
|
if (probe.status !== 0) return { status: probe.status, unavailable: true }
|
|
58
74
|
// INHERITED stdio — the provider owns its install questions (tty interactive
|
|
59
75
|
// happens here, once). npx bin-name nuance (cf. the 0.2.9 update pitfall): with
|
|
60
76
|
// the provider bin already on PATH npx runs the INSTALLED one — acceptable for
|
|
61
77
|
// an idempotent init (unlike the self-update case where it was a structural bug).
|
|
62
|
-
|
|
78
|
+
// timeout: a generous wall-clock backstop so a hung provider can't wedge onboard
|
|
79
|
+
// (the caller also makes Ctrl-C skip just this step).
|
|
80
|
+
const r = spawnSync('npx', ['-y', pkg, ...args], {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
env,
|
|
83
|
+
...(timeoutMs ? { timeout: timeoutMs } : {}),
|
|
84
|
+
})
|
|
63
85
|
return { status: r.status, unavailable: false }
|
|
64
86
|
}
|
|
65
87
|
|
|
@@ -91,12 +113,12 @@ export async function onboardMemoryProvider(opts: MemoryOnboardOptions = {}): Pr
|
|
|
91
113
|
detail: `slot is occupied by "${existing.provider}" (${existing.package}) — uninstall it first`,
|
|
92
114
|
}
|
|
93
115
|
}
|
|
94
|
-
const args = ['init', ...coreKnownInitArgs(env)]
|
|
116
|
+
const args = ['init', ...coreKnownInitArgs(env), ...(opts.runtime ? ['--runtime', opts.runtime] : [])]
|
|
95
117
|
if (opts.dryRun) {
|
|
96
118
|
return { state: 'dry-run', provider: null, detail: `would run: npx -y ${pkg} ${args.join(' ')}` }
|
|
97
119
|
}
|
|
98
120
|
const run = opts.runInit ?? defaultRunInit
|
|
99
|
-
const r = run(pkg, args, env)
|
|
121
|
+
const r = run(pkg, args, env, opts.timeoutMs)
|
|
100
122
|
if (r.unavailable) {
|
|
101
123
|
return {
|
|
102
124
|
state: 'skipped-unavailable',
|
package/src/tui/ansi.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// ANSI palette for plain-text CLI output that runs OUTSIDE Ink — chiefly the
|
|
2
|
+
// post-Ink onboard summary (run.tsx), which prints after the wizard unmounts and
|
|
3
|
+
// would otherwise read as a flat, colorless tail to a styled flow.
|
|
4
|
+
//
|
|
5
|
+
// It mirrors the Ink wizard's palette one-for-one (see app.tsx color()/glyph()):
|
|
6
|
+
// ok → green ✓ · warn → yellow ! · fail → red ✗ · pending → gray · ·
|
|
7
|
+
// headers → bold (cyan for emphasis) · secondary detail/paths → dim
|
|
8
|
+
// so the seam between the React-rendered part and the printed part is invisible.
|
|
9
|
+
//
|
|
10
|
+
// Color is GATED (colorEnabled): on by default only on a real TTY, force-on with
|
|
11
|
+
// FORCE_COLOR (covers inherited-pipe passthrough by a child process), and hard-off
|
|
12
|
+
// with NO_COLOR (https://no-color.org — presence disables, any value) so piped /
|
|
13
|
+
// redirected / CI captures stay byte-clean. This is the team-wide convention we
|
|
14
|
+
// hand to peer providers (e.g. iapeer-memory) whose stdout we surface verbatim.
|
|
15
|
+
|
|
16
|
+
const ESC = '\x1b['
|
|
17
|
+
const RESET = `${ESC}0m`
|
|
18
|
+
|
|
19
|
+
const CODE = {
|
|
20
|
+
bold: '1',
|
|
21
|
+
dim: '2',
|
|
22
|
+
red: '31',
|
|
23
|
+
green: '32',
|
|
24
|
+
yellow: '33',
|
|
25
|
+
cyan: '36',
|
|
26
|
+
gray: '90',
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
export type AnsiPaint = (s: string) => string
|
|
30
|
+
|
|
31
|
+
export interface Ansi {
|
|
32
|
+
bold: AnsiPaint
|
|
33
|
+
dim: AnsiPaint
|
|
34
|
+
red: AnsiPaint
|
|
35
|
+
green: AnsiPaint
|
|
36
|
+
yellow: AnsiPaint
|
|
37
|
+
cyan: AnsiPaint
|
|
38
|
+
gray: AnsiPaint
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether to emit ANSI color for `stream`. NO_COLOR (any value) hard-disables;
|
|
42
|
+
* FORCE_COLOR (truthy, not "0"/"false") forces on; otherwise gated on a real TTY. */
|
|
43
|
+
export function colorEnabled(
|
|
44
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
45
|
+
stream: { isTTY?: boolean } = process.stdout,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (env.NO_COLOR != null) return false
|
|
48
|
+
const fc = env.FORCE_COLOR
|
|
49
|
+
if (fc != null && fc !== '0' && fc !== 'false' && fc !== '') return true
|
|
50
|
+
return stream.isTTY === true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build a palette. When `enabled` is false every paint fn is identity (no codes),
|
|
54
|
+
* so the same call sites produce clean text for pipes/CI. Codes compose: an inner
|
|
55
|
+
* paint applies before the outer, both before the run resets — `a.bold(a.cyan(s))`
|
|
56
|
+
* renders bold+cyan. */
|
|
57
|
+
export function makeAnsi(enabled: boolean): Ansi {
|
|
58
|
+
const wrap =
|
|
59
|
+
(code: string): AnsiPaint =>
|
|
60
|
+
(s: string) =>
|
|
61
|
+
enabled ? `${ESC}${code}m${s}${RESET}` : s
|
|
62
|
+
return {
|
|
63
|
+
bold: wrap(CODE.bold),
|
|
64
|
+
dim: wrap(CODE.dim),
|
|
65
|
+
red: wrap(CODE.red),
|
|
66
|
+
green: wrap(CODE.green),
|
|
67
|
+
yellow: wrap(CODE.yellow),
|
|
68
|
+
cyan: wrap(CODE.cyan),
|
|
69
|
+
gray: wrap(CODE.gray),
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/tui/onboard/app.tsx
CHANGED
|
@@ -20,15 +20,24 @@ export interface WizardResult {
|
|
|
20
20
|
memoryConsent: boolean
|
|
21
21
|
advisories: string[]
|
|
22
22
|
summary: string[]
|
|
23
|
+
telegramConsent: boolean
|
|
24
|
+
/** Host agentic runtime detected during the run (claude if installed, else
|
|
25
|
+
* codex, else undefined on a no-runtime host) — threaded to the memory
|
|
26
|
+
* provider init as `--runtime` per the onboard↔init contract. */
|
|
27
|
+
hostRuntime?: OnboardRuntime
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
type Phase = 'gate' | 'running' | 'memory' | 'declined' | 'finishing'
|
|
30
|
+
type Phase = 'gate' | 'running' | 'telegram' | 'memory' | 'declined' | 'finishing'
|
|
26
31
|
type StepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'fail'
|
|
27
32
|
interface Step {
|
|
28
33
|
key: string
|
|
29
34
|
label: string
|
|
30
35
|
status: StepStatus
|
|
31
36
|
detail?: string
|
|
37
|
+
/** Honest "this can take a moment" note shown while the step is running (the
|
|
38
|
+
* spinner can't animate during a blocking spawnSync, so the label carries the
|
|
39
|
+
* expectation). */
|
|
40
|
+
hint?: string
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
@@ -85,6 +94,8 @@ export function OnboardApp({
|
|
|
85
94
|
const [phase, setPhase] = useState<Phase>('gate')
|
|
86
95
|
const [steps, setSteps] = useState<Step[]>([])
|
|
87
96
|
const [advisories, setAdvisories] = useState<string[]>([])
|
|
97
|
+
const [hostRuntime, setHostRuntime] = useState<OnboardRuntime | undefined>(undefined)
|
|
98
|
+
const [telegramConsent, setTelegramConsent] = useState(false)
|
|
88
99
|
const spin = useSpinnerFrame(phase === 'running')
|
|
89
100
|
|
|
90
101
|
const finish = (memoryConsent: boolean): void => {
|
|
@@ -94,7 +105,7 @@ export function OnboardApp({
|
|
|
94
105
|
return ` ${mark} ${s.label}${s.detail ? ` — ${s.detail}` : ''}`
|
|
95
106
|
})
|
|
96
107
|
summary.unshift(failed ? 'Onboard finished with errors:' : 'Onboard complete:')
|
|
97
|
-
onResult({ code: failed ? 1 : 0, memoryConsent, advisories, summary })
|
|
108
|
+
onResult({ code: failed ? 1 : 0, memoryConsent, telegramConsent, advisories, summary, hostRuntime })
|
|
98
109
|
setPhase('finishing')
|
|
99
110
|
setTimeout(() => exit(), 30)
|
|
100
111
|
}
|
|
@@ -108,6 +119,22 @@ export function OnboardApp({
|
|
|
108
119
|
{ isActive: phase === 'gate' },
|
|
109
120
|
)
|
|
110
121
|
|
|
122
|
+
// Telegram consent — DEFAULT-YES ([Y/n]). Records the decision; the actual setup
|
|
123
|
+
// (install + ask name/user_id) runs post-Ink (its readline needs a cooked
|
|
124
|
+
// terminal). Then → memory consent.
|
|
125
|
+
useInput(
|
|
126
|
+
(input, key) => {
|
|
127
|
+
if (input === 'n' || input === 'N') {
|
|
128
|
+
setTelegramConsent(false)
|
|
129
|
+
setPhase('memory')
|
|
130
|
+
} else if (input === 'y' || input === 'Y' || key.return) {
|
|
131
|
+
setTelegramConsent(true)
|
|
132
|
+
setPhase('memory')
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{ isActive: phase === 'telegram' },
|
|
136
|
+
)
|
|
137
|
+
|
|
111
138
|
// Memory consent — DEFAULT-YES ([Y/n]: Enter or y → install).
|
|
112
139
|
useInput(
|
|
113
140
|
(input, key) => {
|
|
@@ -120,7 +147,7 @@ export function OnboardApp({
|
|
|
120
147
|
// Declined → exit 1 (no host mutation).
|
|
121
148
|
useEffect(() => {
|
|
122
149
|
if (phase !== 'declined') return
|
|
123
|
-
onResult({ code: 1, memoryConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
|
|
150
|
+
onResult({ code: 1, memoryConsent: false, telegramConsent: false, advisories: [], summary: ['onboard aborted — risk not accepted.'] })
|
|
124
151
|
const t = setTimeout(() => exit(), 30)
|
|
125
152
|
return () => clearTimeout(t)
|
|
126
153
|
}, [phase, onResult, exit])
|
|
@@ -132,10 +159,16 @@ export function OnboardApp({
|
|
|
132
159
|
const set = (k: string, patch: Partial<Step>) =>
|
|
133
160
|
setSteps(prev => prev.map(s => (s.key === k ? { ...s, ...patch } : s)))
|
|
134
161
|
setSteps([
|
|
135
|
-
{ key: 'daemon', label: 'Start router daemon', status: 'pending' },
|
|
162
|
+
{ key: 'daemon', label: 'Start router daemon', status: 'pending', hint: 'a few seconds' },
|
|
136
163
|
{ key: 'market', label: 'Register marketplace (claude, codex)', status: 'pending' },
|
|
137
164
|
{ key: 'auth', label: 'Check runtime sign-in', status: 'pending' },
|
|
138
165
|
{ key: 'fda', label: 'Check macOS Full Disk Access', status: 'pending' },
|
|
166
|
+
{
|
|
167
|
+
key: 'notifier',
|
|
168
|
+
label: 'Install scheduler (timer · watcher)',
|
|
169
|
+
status: 'pending',
|
|
170
|
+
hint: 'first run can take up to a minute',
|
|
171
|
+
},
|
|
139
172
|
])
|
|
140
173
|
|
|
141
174
|
// Backend steps use synchronous spawnSync (launchctl, runtime plugin list) —
|
|
@@ -164,10 +197,19 @@ export function OnboardApp({
|
|
|
164
197
|
const r = onboardHost({ env })
|
|
165
198
|
const installed: OnboardRuntime[] = []
|
|
166
199
|
for (const m of r.marketplaces) if (m.state !== 'runtime-missing') installed.push(m.runtime)
|
|
200
|
+
// host-runtime for the memory init contract: claude if present (covers
|
|
201
|
+
// both-installed → claude default), else codex, else undefined (no runtime).
|
|
202
|
+
setHostRuntime(installed.includes('claude') ? 'claude' : installed[0])
|
|
167
203
|
const mFail = r.marketplaces.some(m => m.state === 'failed')
|
|
204
|
+
const allMissing = r.marketplaces.length > 0 && r.marketplaces.every(m => m.state === 'runtime-missing')
|
|
168
205
|
set('market', {
|
|
169
|
-
|
|
170
|
-
|
|
206
|
+
// runtime-missing is NOT success — show '!' (warn), never a contradictory ✓.
|
|
207
|
+
status: mFail ? 'fail' : allMissing ? 'warn' : 'ok',
|
|
208
|
+
detail: allMissing
|
|
209
|
+
? 'no agent runtime installed — skipped'
|
|
210
|
+
: r.marketplaces
|
|
211
|
+
.map(m => `${m.runtime}: ${m.state === 'runtime-missing' ? 'not installed' : m.state}`)
|
|
212
|
+
.join(', '),
|
|
171
213
|
})
|
|
172
214
|
if (cancelled) return
|
|
173
215
|
await tick()
|
|
@@ -177,8 +219,11 @@ export function OnboardApp({
|
|
|
177
219
|
if (installed.length === 0) {
|
|
178
220
|
set('auth', { status: 'warn', detail: 'no runtime installed' })
|
|
179
221
|
adv.push(
|
|
180
|
-
'⚠ SETUP INCOMPLETE — no agent runtime found. iapeer needs Claude Code
|
|
181
|
-
'
|
|
222
|
+
'⚠ SETUP INCOMPLETE — no agent runtime found. iapeer needs an agent runtime: Claude Code OR Codex (either works).\n' +
|
|
223
|
+
' Install EITHER (commands verified against the official docs), sign in, then re-run `iapeer onboard`:\n' +
|
|
224
|
+
' Claude Code: curl -fsSL https://claude.ai/install.sh | bash (or: brew install --cask claude-code)\n' +
|
|
225
|
+
' Codex: npm install -g @openai/codex (or: brew install --cask codex)\n' +
|
|
226
|
+
' If a download is blocked in your region, try the brew form or the other runtime.',
|
|
182
227
|
)
|
|
183
228
|
} else {
|
|
184
229
|
const notes = installed.map(rt => runtimeAuthNote(rt, env)).filter((n): n is string => n != null)
|
|
@@ -188,6 +233,7 @@ export function OnboardApp({
|
|
|
188
233
|
})
|
|
189
234
|
adv.push(...notes)
|
|
190
235
|
}
|
|
236
|
+
setAdvisories([...adv]) // surface SETUP-INCOMPLETE / auth advisories IN the wizard now
|
|
191
237
|
if (cancelled) return
|
|
192
238
|
|
|
193
239
|
set('fda', { status: 'running' })
|
|
@@ -197,9 +243,32 @@ export function OnboardApp({
|
|
|
197
243
|
set('fda', { status: tcc ? 'warn' : 'ok', detail: fda === true ? 'granted' : tcc ? 'not granted' : 'n/a' })
|
|
198
244
|
if (tcc) adv.push(tcc)
|
|
199
245
|
if (cancelled) return
|
|
246
|
+
await tick()
|
|
247
|
+
|
|
248
|
+
// notifier — the scheduler (timer · watcher) that memory's index upkeep
|
|
249
|
+
// rides on; a silent default. Its npx install captures stdio (no terminal
|
|
250
|
+
// contention), so it runs in-wizard like the other steps. Idempotent: a
|
|
251
|
+
// present package/peers → no-op. A failure must NOT fail onboard (degrade).
|
|
252
|
+
set('notifier', { status: 'running' })
|
|
253
|
+
await tick()
|
|
254
|
+
try {
|
|
255
|
+
// canonical backbone step (same as the linear onboard path): zero-question,
|
|
256
|
+
// captured-stdio, idempotent, soft-skip on unavailable.
|
|
257
|
+
const { onboardNotifierStep } = await import('../../onboard/steps.ts')
|
|
258
|
+
const ns = await onboardNotifierStep({ env, warn: () => {} })
|
|
259
|
+
set('notifier', {
|
|
260
|
+
status: ns.state === 'deployed' ? 'ok' : 'warn',
|
|
261
|
+
detail: ns.state === 'deployed' ? `${ns.peers.length} peer(s)` : (ns.detail ?? ns.state),
|
|
262
|
+
})
|
|
263
|
+
if (ns.state !== 'deployed') adv.push(`Scheduler (notifier): ${ns.detail ?? ns.state}`)
|
|
264
|
+
} catch (e) {
|
|
265
|
+
set('notifier', { status: 'warn', detail: (e instanceof Error ? e.message : 'failed').slice(0, 60) })
|
|
266
|
+
}
|
|
267
|
+
if (cancelled) return
|
|
268
|
+
await tick()
|
|
200
269
|
|
|
201
|
-
setAdvisories(adv)
|
|
202
|
-
setPhase('
|
|
270
|
+
setAdvisories([...adv])
|
|
271
|
+
setPhase('telegram')
|
|
203
272
|
})()
|
|
204
273
|
|
|
205
274
|
return () => {
|
|
@@ -236,23 +305,51 @@ export function OnboardApp({
|
|
|
236
305
|
return <Text color="red">onboard aborted — risk not accepted.</Text>
|
|
237
306
|
}
|
|
238
307
|
|
|
239
|
-
// running | memory | finishing
|
|
308
|
+
// running | telegram | memory | finishing
|
|
240
309
|
return (
|
|
241
310
|
<Box flexDirection="column">
|
|
242
|
-
<
|
|
243
|
-
|
|
311
|
+
<Box marginBottom={1}>
|
|
312
|
+
<Text bold color="cyan">
|
|
313
|
+
iapeer
|
|
314
|
+
</Text>
|
|
315
|
+
<Text bold> · setting up the host</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
<Box flexDirection="column">
|
|
244
318
|
{steps.map(s => (
|
|
245
319
|
<Box key={s.key}>
|
|
246
320
|
<Text color={color(s.status)}>{glyph(s.status, spin)} </Text>
|
|
247
321
|
<Text>{s.label}</Text>
|
|
248
|
-
{s.detail ?
|
|
322
|
+
{s.detail ? (
|
|
323
|
+
<Text dimColor> — {s.detail}</Text>
|
|
324
|
+
) : s.status === 'running' && s.hint ? (
|
|
325
|
+
<Text dimColor> — {s.hint}…</Text>
|
|
326
|
+
) : null}
|
|
249
327
|
</Box>
|
|
250
328
|
))}
|
|
251
329
|
</Box>
|
|
330
|
+
{advisories.length > 0 ? (
|
|
331
|
+
<Box marginTop={1} flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
|
|
332
|
+
{advisories.map((a, i) => (
|
|
333
|
+
<Text key={i} color="yellow">
|
|
334
|
+
{a}
|
|
335
|
+
</Text>
|
|
336
|
+
))}
|
|
337
|
+
</Box>
|
|
338
|
+
) : null}
|
|
339
|
+
{phase === 'telegram' ? (
|
|
340
|
+
<Box marginTop={1} flexDirection="column">
|
|
341
|
+
<Text bold color="cyan">
|
|
342
|
+
? Set up Telegram — talk to your agents from your phone?
|
|
343
|
+
</Text>
|
|
344
|
+
<Text dimColor> It will ask your name + Telegram user_id next. (bot token comes later) [Y/n]</Text>
|
|
345
|
+
</Box>
|
|
346
|
+
) : null}
|
|
252
347
|
{phase === 'memory' ? (
|
|
253
348
|
<Box marginTop={1} flexDirection="column">
|
|
254
|
-
<Text
|
|
255
|
-
|
|
349
|
+
<Text bold color="cyan">
|
|
350
|
+
? Install the shared-memory provider (@agfpd/iapeer-memory)?
|
|
351
|
+
</Text>
|
|
352
|
+
<Text dimColor> Gives your agents a shared knowledge vault. [Y/n]</Text>
|
|
256
353
|
</Box>
|
|
257
354
|
) : null}
|
|
258
355
|
</Box>
|