@agfpd/iapeer 0.4.51 → 0.4.53
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/package.json +1 -1
- package/src/cli/index.ts +62 -9
- package/src/enable/index.ts +24 -7
- package/src/identity/index.ts +20 -3
- package/src/launch/invocation.ts +7 -1
- package/src/onboard/index.ts +4 -1
- package/src/provision/index.ts +3 -0
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -244,12 +244,21 @@ export function stopPeer(personality: string, runtime: string | undefined, opts:
|
|
|
244
244
|
const identity = buildProcessAddress(rt, personality)
|
|
245
245
|
const sock = buildSocketPath(rt, personality, cfg.sockDir)
|
|
246
246
|
if (isInfraRuntime(rt)) {
|
|
247
|
-
// Audit #13: do NOT swallow the launchctl result
|
|
248
|
-
//
|
|
249
|
-
// the
|
|
247
|
+
// Audit #13 + В48: do NOT swallow the launchctl result — but DISCRIMINATE. An
|
|
248
|
+
// already-unloaded job (exit 3 / "No such process", verified live) is BENIGN for
|
|
249
|
+
// stop: the desired state ("not running") already holds, so it carries NO reason
|
|
250
|
+
// and must not fail the verb. Any other non-zero is a REAL error and sets reason
|
|
251
|
+
// (the CLI maps a reasoned bootout to exit 1).
|
|
250
252
|
const r = spawnSync('launchctl', ['bootout', `gui/${uid()}/${launchdLabel(personality)}`], { encoding: 'utf8' })
|
|
251
253
|
killSession(sock, identity, env)
|
|
252
|
-
|
|
254
|
+
const stderrText = (r.stderr ?? '').trim()
|
|
255
|
+
const benign = r.status === 0 || r.status === 3 || /No such process/i.test(stderrText)
|
|
256
|
+
out.push({
|
|
257
|
+
personality,
|
|
258
|
+
runtime: rt,
|
|
259
|
+
action: 'bootout',
|
|
260
|
+
reason: benign ? undefined : `launchctl bootout exited ${r.status}${stderrText ? `: ${stderrText}` : ''}`,
|
|
261
|
+
})
|
|
253
262
|
} else {
|
|
254
263
|
// A deliberate stop is a CLEAN PARK, not a death (stop→start must survive ≥
|
|
255
264
|
// idle-reap): park-mark BEFORE the kill so the post-`start`
|
|
@@ -1131,6 +1140,32 @@ function parseIdentity(identity: string): { personality: string; runtime: Runtim
|
|
|
1131
1140
|
// CLI dispatch — `iapeer <verb> …`
|
|
1132
1141
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1133
1142
|
|
|
1143
|
+
/** В49 — flags that never take a value. Without the schema the look-ahead form ate the
|
|
1144
|
+
* NEXT positional: `iapeer update --force 0.4.31` parsed as flags.force='0.4.31' with NO
|
|
1145
|
+
* positionals → an UNforced full-cascade update instead of a forced version pin (live
|
|
1146
|
+
* incident shape). A boolean flag is always `true`; forcing a value onto one is only
|
|
1147
|
+
* possible via the explicit `--key=value` form (which stays verbatim). */
|
|
1148
|
+
const BOOLEAN_FLAGS: ReadonlySet<string> = new Set([
|
|
1149
|
+
'accept-risk',
|
|
1150
|
+
'all',
|
|
1151
|
+
'check',
|
|
1152
|
+
'dry-run',
|
|
1153
|
+
'fix',
|
|
1154
|
+
'force',
|
|
1155
|
+
'foundation-only',
|
|
1156
|
+
'install-plist',
|
|
1157
|
+
'json',
|
|
1158
|
+
'no-bootstrap',
|
|
1159
|
+
'no-memory',
|
|
1160
|
+
'no-notifier',
|
|
1161
|
+
'no-setup',
|
|
1162
|
+
'no-telegram',
|
|
1163
|
+
'no-voice',
|
|
1164
|
+
'npx',
|
|
1165
|
+
'remove-codesign-identity',
|
|
1166
|
+
'yes',
|
|
1167
|
+
])
|
|
1168
|
+
|
|
1134
1169
|
export function parseArgs(argv: string[]): { positionals: string[]; flags: Record<string, string | true> } {
|
|
1135
1170
|
const positionals: string[] = []
|
|
1136
1171
|
const flags: Record<string, string | true> = {}
|
|
@@ -1145,6 +1180,11 @@ export function parseArgs(argv: string[]): { positionals: string[]; flags: Recor
|
|
|
1145
1180
|
continue
|
|
1146
1181
|
}
|
|
1147
1182
|
const key = a.slice(2)
|
|
1183
|
+
// В49 — a KNOWN boolean flag never consumes the next token as its value.
|
|
1184
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
1185
|
+
flags[key] = true
|
|
1186
|
+
continue
|
|
1187
|
+
}
|
|
1148
1188
|
const next = argv[i + 1]
|
|
1149
1189
|
if (next === undefined || next.startsWith('--')) flags[key] = true
|
|
1150
1190
|
else flags[key] = argv[++i]
|
|
@@ -1684,9 +1724,13 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1684
1724
|
out(
|
|
1685
1725
|
`initialized "${r.personality}" (default: ${r.runtime}; runtimes: ${allRuntimes.join(', ')}); ` +
|
|
1686
1726
|
`mcp: ${r.mcpConfigPaths.join(', ') || r.codexMcpConfigPath || 'none'}` +
|
|
1687
|
-
`${r.bootstrapped ? `; bootstrap: ${r.bootstrapped.state}` : ''}
|
|
1727
|
+
`${r.bootstrapped ? `; bootstrap: ${r.bootstrapped.state}` : ''}` +
|
|
1728
|
+
`${r.selfConfig?.state === 'failed' ? '; self-config: FAILED (infra peer NOT started)' : ''}\n`,
|
|
1688
1729
|
)
|
|
1689
|
-
|
|
1730
|
+
// В47 — a failed runtime self-config means initPeer deliberately did NOT bootstrap:
|
|
1731
|
+
// the always-on peer is not running. Exit 0 here read as success to automation.
|
|
1732
|
+
if (r.selfConfig?.state === 'failed') return 1
|
|
1733
|
+
return r.bootstrapped && (r.bootstrapped.state === 'failed' || r.bootstrapped.state === 'refused-foreign') ? 1 : 0
|
|
1690
1734
|
}
|
|
1691
1735
|
case 'create': {
|
|
1692
1736
|
// cwd-INDEPENDENT: resolve a location (default ~/.iapeer/peers/<p> or --path),
|
|
@@ -1753,8 +1797,13 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1753
1797
|
out(
|
|
1754
1798
|
`created "${r.personality}" (default: ${r.runtime}; runtimes: ${allRuntimes.join(', ')}) at ${r.location}; ` +
|
|
1755
1799
|
`mcp: ${r.mcpConfigPaths.join(', ') || r.codexMcpConfigPath || 'none'}` +
|
|
1756
|
-
`${r.plistPath ? `; plist: ${r.plistPath}` : ''}${r.bootstrapped ? `; bootstrap: ${r.bootstrapped.state}` : ''}
|
|
1800
|
+
`${r.plistPath ? `; plist: ${r.plistPath}` : ''}${r.bootstrapped ? `; bootstrap: ${r.bootstrapped.state}` : ''}` +
|
|
1801
|
+
`${r.selfConfig?.state === 'failed' ? '; self-config: FAILED (infra peer NOT started)' : ''}\n`,
|
|
1757
1802
|
)
|
|
1803
|
+
// В47 — selfConfig 'failed' deliberately skips the bootstrap (initPeer), so the
|
|
1804
|
+
// always-on peer is NOT running; `create x --runtime telegram` with a broken hook
|
|
1805
|
+
// exited 0 and automation read a dead telegram router as provisioned.
|
|
1806
|
+
if (r.selfConfig?.state === 'failed') return 1
|
|
1758
1807
|
return r.bootstrapped && (r.bootstrapped.state === 'failed' || r.bootstrapped.state === 'refused-foreign') ? 1 : 0
|
|
1759
1808
|
}
|
|
1760
1809
|
case 'list': {
|
|
@@ -1865,7 +1914,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1865
1914
|
if (!peers) return argErr(errOut, 'stop needs a peer or --all — usage: iapeer stop <peer> [runtime] | --all')
|
|
1866
1915
|
const outcomes = peers.flatMap(p => stopPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1867
1916
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1868
|
-
|
|
1917
|
+
// В48 — a REAL bootout error (reason set; benign already-unloaded carries none) is exit 1:
|
|
1918
|
+
// automation must see that an always-on peer may still be running.
|
|
1919
|
+
return outcomes.some(o => o.action === 'refused-foreign-launchd' || (o.action === 'bootout' && o.reason !== undefined)) ? 1 : 0
|
|
1869
1920
|
}
|
|
1870
1921
|
case 'add-runtime': {
|
|
1871
1922
|
// Fleet-switch enabler (codex-parity audit): add an agentic runtime to
|
|
@@ -1917,7 +1968,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1917
1968
|
if (!peers) return argErr(errOut, 'start needs a peer or --all — usage: iapeer start <peer> [runtime] | --all')
|
|
1918
1969
|
const outcomes = peers.flatMap(p => startPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1919
1970
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1920
|
-
|
|
1971
|
+
// В48 — a failed launchctl bootstrap (reason set) means the always-on peer is NOT
|
|
1972
|
+
// started; exit 0 here let automation read a dead telegram/notifier as running.
|
|
1973
|
+
return outcomes.some(o => o.action === 'refused-foreign-launchd' || (o.action === 'bootstrap' && o.reason !== undefined)) ? 1 : 0
|
|
1921
1974
|
}
|
|
1922
1975
|
case 'refresh': {
|
|
1923
1976
|
// LAZY soft-reload (fleet doctrine refresh): arm `.fresh-next` so each agentic peer comes up FRESH
|
package/src/enable/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { spawnSync } from 'child_process'
|
|
|
24
24
|
import { existsSync, readFileSync, realpathSync } from 'fs'
|
|
25
25
|
import { basename, join } from 'path'
|
|
26
26
|
import { homedir } from 'os'
|
|
27
|
-
import { MARKETPLACE_NAME, isMarketplaceRegisteredAs, refreshMarketplace, registerMarketplace } from '../onboard/index.ts'
|
|
27
|
+
import { MARKETPLACE_NAME, isExecutable, isMarketplaceRegisteredAs, refreshMarketplace, registerMarketplace } from '../onboard/index.ts'
|
|
28
28
|
import { findPeer, readPeersIndex } from '../registry/index.ts'
|
|
29
29
|
import { IapError } from '../core/errors.ts'
|
|
30
30
|
import { normalizeNameCandidate } from '../core/normalize.ts'
|
|
@@ -205,10 +205,14 @@ function resolvePeer(opts: EnableOptions): { personality: string; cwd: string; r
|
|
|
205
205
|
return { personality, cwd: rec.cwd, runtimes: agentic }
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// В57 — the local `--version` ANSWER probe was removed: it spawned the runtime CLI with NO
|
|
209
|
+
// timeout, so a wedged binary (the known macOS launch-approval park after a cask update —
|
|
210
|
+
// onboard's live incident: probes sat 25+ min) hung `enable` forever. enable now uses
|
|
211
|
+
// onboard's PRESENCE probe (accessSync X_OK / PATH scan — no spawn at all), and every real
|
|
212
|
+
// runtime-CLI invocation below carries a hard timeout, mirroring onboard.
|
|
213
|
+
const LIST_TIMEOUT_MS = 60_000 // read-only queries (plugin list)
|
|
214
|
+
const MUTATE_TIMEOUT_MS = 120_000 // install / enable / add (may clone from GitHub)
|
|
215
|
+
const SETUP_TIMEOUT_MS = 300_000 // a СЛОЖНЫЙ plugin's own installer — the longest legitimate leg
|
|
212
216
|
|
|
213
217
|
/** Read the peer-scoped entry for this plugin from a fresh `claude plugin list --json`.
|
|
214
218
|
* CRITICAL: `enabled` is evaluated relative to the CURRENT cwd's project (verified
|
|
@@ -227,6 +231,7 @@ function claudeEntry(
|
|
|
227
231
|
encoding: 'utf8',
|
|
228
232
|
env: env as Record<string, string>,
|
|
229
233
|
maxBuffer: 64 * 1024 * 1024,
|
|
234
|
+
timeout: LIST_TIMEOUT_MS,
|
|
230
235
|
})
|
|
231
236
|
return findPeerScopedEntry(parseInstalledPlugins(list.stdout ?? ''), plugin, marketplace, peerCwd)
|
|
232
237
|
}
|
|
@@ -251,6 +256,7 @@ function enableClaude(plugin: string, marketplace: string, peerCwd: string, env:
|
|
|
251
256
|
cwd: peerCwd,
|
|
252
257
|
encoding: 'utf8',
|
|
253
258
|
env: env as Record<string, string>,
|
|
259
|
+
timeout: MUTATE_TIMEOUT_MS,
|
|
254
260
|
})
|
|
255
261
|
if (inst.status !== 0) {
|
|
256
262
|
return { runtime: 'claude', state: 'failed', detail: (inst.stderr || inst.stdout || `exit ${inst.status}`).trim() }
|
|
@@ -264,6 +270,7 @@ function enableClaude(plugin: string, marketplace: string, peerCwd: string, env:
|
|
|
264
270
|
cwd: peerCwd,
|
|
265
271
|
encoding: 'utf8',
|
|
266
272
|
env: env as Record<string, string>,
|
|
273
|
+
timeout: MUTATE_TIMEOUT_MS,
|
|
267
274
|
})
|
|
268
275
|
const confirmed = claudeEntry(bin, plugin, marketplace, peerCwd, env)
|
|
269
276
|
if (!confirmed?.enabled) {
|
|
@@ -300,7 +307,12 @@ export function parseCodexPluginStatus(jsonOutput: string, id: string): 'enabled
|
|
|
300
307
|
}
|
|
301
308
|
|
|
302
309
|
function codexState(bin: string, id: string, env: NodeJS.ProcessEnv): 'enabled' | 'disabled' | 'absent' {
|
|
303
|
-
const r = spawnSync(bin, ['plugin', 'list', '--json'], {
|
|
310
|
+
const r = spawnSync(bin, ['plugin', 'list', '--json'], {
|
|
311
|
+
encoding: 'utf8',
|
|
312
|
+
env: env as Record<string, string>,
|
|
313
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
314
|
+
timeout: LIST_TIMEOUT_MS,
|
|
315
|
+
})
|
|
304
316
|
return parseCodexPluginStatus(r.stdout ?? '', id)
|
|
305
317
|
}
|
|
306
318
|
|
|
@@ -314,7 +326,11 @@ function enableCodex(plugin: string, marketplace: string, env: NodeJS.ProcessEnv
|
|
|
314
326
|
const id = `${plugin}@${marketplace}`
|
|
315
327
|
const before = codexState(bin, id, env)
|
|
316
328
|
if (before === 'enabled') return { runtime: 'codex', state: 'already-enabled' }
|
|
317
|
-
const add = spawnSync(bin, ['plugin', 'add', id], {
|
|
329
|
+
const add = spawnSync(bin, ['plugin', 'add', id], {
|
|
330
|
+
encoding: 'utf8',
|
|
331
|
+
env: env as Record<string, string>,
|
|
332
|
+
timeout: MUTATE_TIMEOUT_MS,
|
|
333
|
+
})
|
|
318
334
|
if (add.status !== 0) {
|
|
319
335
|
return { runtime: 'codex', state: 'failed', detail: (add.stderr || add.stdout || `exit ${add.status}`).trim() }
|
|
320
336
|
}
|
|
@@ -346,6 +362,7 @@ function callSetup(
|
|
|
346
362
|
cwd: installPath,
|
|
347
363
|
encoding: 'utf8',
|
|
348
364
|
env: setupEnv as Record<string, string>,
|
|
365
|
+
timeout: SETUP_TIMEOUT_MS,
|
|
349
366
|
})
|
|
350
367
|
if (r.error || (r.status ?? 1) !== 0) {
|
|
351
368
|
return { ok: false, detail: (r.stderr || r.stdout || r.error?.message || `exit ${r.status}`).trim() }
|
package/src/identity/index.ts
CHANGED
|
@@ -120,6 +120,12 @@ export interface EnsurePeerProfileOptions {
|
|
|
120
120
|
* always-on plist so launchd's minimal PATH resolves it. Forwarded to
|
|
121
121
|
* installAlwaysOnPlist; ignored for warm-on-demand runtimes. */
|
|
122
122
|
runtimeBin?: string
|
|
123
|
+
/** В36 — peer description, persisted into the LOCAL profile (the source of truth the
|
|
124
|
+
* registry is reindexed FROM). Writing it only to the registry made it a stale copy:
|
|
125
|
+
* the first reindexFromLocals (addRuntime on a dual-runtime create, connect telegram,
|
|
126
|
+
* verify --fix) projected the profile's empty description over it, wiping the Layer-3
|
|
127
|
+
* addressing signal fleet-wide. */
|
|
128
|
+
description?: string
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -513,7 +519,9 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
513
519
|
personality,
|
|
514
520
|
runtime: options.runtime,
|
|
515
521
|
runtimes: discoveredRuntimes,
|
|
516
|
-
description
|
|
522
|
+
// В36 — the description lands in the LOCAL profile at birth (source of truth);
|
|
523
|
+
// the registry row is a projection of this file, not the other way around.
|
|
524
|
+
description: options.description?.trim() ?? '',
|
|
517
525
|
intelligence: defaultIntelligenceForRuntime(options.runtime),
|
|
518
526
|
}
|
|
519
527
|
ensureLocalRuntimeScopes(cwd, profile.runtimes)
|
|
@@ -573,8 +581,17 @@ export function ensurePeerProfile(options: EnsurePeerProfileOptions): PeerProfil
|
|
|
573
581
|
})
|
|
574
582
|
intelligence = defaultIntelligenceForRuntime(options.runtime)
|
|
575
583
|
}
|
|
576
|
-
|
|
577
|
-
|
|
584
|
+
// В36 — an explicit description on RE-provision updates the local profile too (H1 in
|
|
585
|
+
// writePeerProfileAtomic still guards: absent/empty never wipes an existing value).
|
|
586
|
+
const explicitDescription = options.description?.trim()
|
|
587
|
+
const descriptionChanged = Boolean(explicitDescription) && explicitDescription !== existing.description
|
|
588
|
+
if (mergedRuntimes.length !== existing.runtimes.length || intelligence !== existing.intelligence || descriptionChanged) {
|
|
589
|
+
const updated = {
|
|
590
|
+
...existing,
|
|
591
|
+
runtimes: mergedRuntimes,
|
|
592
|
+
intelligence,
|
|
593
|
+
...(descriptionChanged ? { description: explicitDescription! } : {}),
|
|
594
|
+
}
|
|
578
595
|
writePeerProfileAtomic(cwd, updated)
|
|
579
596
|
return updated
|
|
580
597
|
}
|
package/src/launch/invocation.ts
CHANGED
|
@@ -50,7 +50,13 @@ export function buildLaunchInvocation(
|
|
|
50
50
|
PEER_PERSONALITY: spec.personality,
|
|
51
51
|
PEER_RUNTIME: spec.runtime,
|
|
52
52
|
PEER_IDENTITY: spec.identity,
|
|
53
|
-
|
|
53
|
+
// В45 — a per-peer launch.env PATH override WINS: this literal sits after the launchEnv
|
|
54
|
+
// spread, so `env.PATH ?? default` silently clobbered the operator's override (env.PATH
|
|
55
|
+
// is nearly always set). The legacy launcher sourced the file — there PATH took effect.
|
|
56
|
+
PATH:
|
|
57
|
+
launchEnv.env.PATH ??
|
|
58
|
+
env.PATH ??
|
|
59
|
+
`${homedir()}/.bun/bin:${homedir()}/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin`,
|
|
54
60
|
}
|
|
55
61
|
// Strip the claude-code-internal session namespace from the inherited env. When a launch is
|
|
56
62
|
// INITIATED from inside a running claude session (an operator CLI, or a claude peer shelling out),
|
package/src/onboard/index.ts
CHANGED
|
@@ -71,7 +71,10 @@ function runtimeBin(runtime: OnboardRuntime, env: NodeJS.ProcessEnv): string {
|
|
|
71
71
|
return env.IAPEER_CODEX_BIN?.trim() || 'codex'
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/** PRESENCE probe (no spawn) — exported for enable/ (В57): the same wedged-runtime class
|
|
75
|
+
* (macOS launch-approval parking every invocation pre-main) hung enable's `--version`
|
|
76
|
+
* ANSWER probe forever; presence answers "is it installed" without executing anything. */
|
|
77
|
+
export function isExecutable(binOrName: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
75
78
|
if (binOrName.includes('/')) {
|
|
76
79
|
try {
|
|
77
80
|
accessSync(binOrName, FS.X_OK)
|
package/src/provision/index.ts
CHANGED
|
@@ -89,6 +89,9 @@ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<Provisi
|
|
|
89
89
|
peers,
|
|
90
90
|
personality: opts.personality,
|
|
91
91
|
runtimeBin,
|
|
92
|
+
// В36 — description goes into the LOCAL profile (source of truth); the registry
|
|
93
|
+
// upsert below then persists a value reindexFromLocals can no longer wipe.
|
|
94
|
+
description: opts.description,
|
|
92
95
|
warn: opts.warn,
|
|
93
96
|
})
|
|
94
97
|
// Birth-time native-memory lever (контракт «Слот памяти» §Native-память рантаймов):
|