@agfpd/iapeer 0.2.17 → 0.2.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -116,6 +116,9 @@ describe('remove (registry record via the locked writer)', () => {
116
116
  const o = await removePeerCli('zombie', { env: e })
117
117
  expect(o.action).toBe('removed')
118
118
  expect(findPeer(readPeersIndex({ env: e }), 'zombie')).toBeNull()
119
+ // the folder is deliberately KEPT; the outcome carries the cwd so the verb can
120
+ // say so instead of leaving silent orphans (boris 10.06)
121
+ expect(o.cwd).toBe('/tmp/zombie')
119
122
  })
120
123
  test('removing an absent peer is an idempotent no-op (not an error)', async () => {
121
124
  const o = await removePeerCli('never-existed', { env: env() })
package/src/cli/index.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // sentinel-marked always-on plist) are stop/start-able.
12
12
 
13
13
  import { spawnSync } from 'child_process'
14
- import { readFileSync } from 'fs'
14
+ import { existsSync, readFileSync } from 'fs'
15
15
  import { fileURLToPath } from 'url'
16
16
  import {
17
17
  isInfraRuntime,
@@ -255,6 +255,10 @@ export interface RemoveOutcome {
255
255
  personality: string
256
256
  action: 'removed' | 'absent' | 'refused-live'
257
257
  reason?: string
258
+ /** The removed peer's cwd (registry fact, captured BEFORE the removal). remove
259
+ * deliberately keeps the folder — user data is never deleted by a registry reap
260
+ * (boris's finding 10.06: say so in the output instead of leaving silent orphans). */
261
+ cwd?: string
258
262
  }
259
263
 
260
264
  /**
@@ -285,7 +289,7 @@ export async function removePeerCli(
285
289
  }
286
290
  }
287
291
  await removePeer(personality, { env })
288
- return { personality, action: 'removed' }
292
+ return { personality, action: 'removed', cwd: peer.cwd }
289
293
  }
290
294
 
291
295
  // ─────────────────────────────────────────────────────────────────────────────
@@ -657,8 +661,14 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
657
661
  // peer unless --force (orphaning a running session from routing is the risk).
658
662
  if (!positionals[0]) return usage(errOut)
659
663
  const o = await removePeerCli(positionals[0], { force: flags.force === true, env })
660
- if (o.action === 'removed') out(`removed "${o.personality}" from the registry\n`)
661
- else if (o.action === 'absent') out(`"${o.personality}" not registered — no-op\n`)
664
+ if (o.action === 'removed') {
665
+ out(`removed "${o.personality}" from the registry\n`)
666
+ // Deliberate: the registry reap never deletes user data — but SAY so, or
667
+ // the default-location peers leave silent orphan folders (boris 10.06).
668
+ if (o.cwd && existsSync(o.cwd)) {
669
+ out(`folder kept: ${o.cwd} (remove never deletes peer data — \`rm -rf\` it yourself if it was a throwaway)\n`)
670
+ }
671
+ } else if (o.action === 'absent') out(`"${o.personality}" not registered — no-op\n`)
662
672
  else errOut(`remove: ${o.reason}\n`)
663
673
  return o.action === 'refused-live' ? 1 : 0
664
674
  }
@@ -55,7 +55,7 @@ function runtimeBin(runtime: OnboardRuntime, env: NodeJS.ProcessEnv): string {
55
55
  return env.IAPEER_CODEX_BIN?.trim() || 'codex'
56
56
  }
57
57
 
58
- function isExecutable(binOrName: string): boolean {
58
+ function isExecutable(binOrName: string, env: NodeJS.ProcessEnv = process.env): boolean {
59
59
  if (binOrName.includes('/')) {
60
60
  try {
61
61
  accessSync(binOrName, FS.X_OK)
@@ -64,13 +64,22 @@ function isExecutable(binOrName: string): boolean {
64
64
  return false
65
65
  }
66
66
  }
67
- // bare name → resolved by spawnSync against PATH; probe with `which`-free spawn.
68
- // HARD TIMEOUT (live find 10.06): `codex --version` HANGS FOREVER in a non-tty
69
- // environment (three stray probes sat 25+ min; an onboard --dry-run piped to a
70
- // file never printed a byte). A hung probe must degrade to 'runtime-missing',
71
- // not wedge the whole onboard.
72
- const r = spawnSync(binOrName, ['--version'], { stdio: 'ignore', timeout: 10_000 })
73
- return r.error === undefined && r.status !== null
67
+ // bare name → PRESENCE probe over PATH (`command -v` semantics), NO spawn.
68
+ // History (both live finds 10.06): the original `--version` ANSWER probe HANGS
69
+ // FOREVER for codex in a non-tty (three stray probes sat 25+ min); the 10 s
70
+ // timeout that replaced it then DEGRADED a LIVE codex to 'runtime-missing'
71
+ // masking a working runtime (boris's catch). The skip-decision only asks "is
72
+ // the runtime installed", and presence answers that without executing anything.
73
+ for (const dir of (env.PATH ?? '').split(':')) {
74
+ if (!dir) continue
75
+ try {
76
+ accessSync(join(dir, binOrName), FS.X_OK)
77
+ return true
78
+ } catch {
79
+ /* not in this PATH segment */
80
+ }
81
+ }
82
+ return false
74
83
  }
75
84
 
76
85
  /**
@@ -81,7 +90,11 @@ function isExecutable(binOrName: string): boolean {
81
90
  */
82
91
  export function isMarketplaceRegistered(runtime: OnboardRuntime, env: NodeJS.ProcessEnv = process.env): boolean {
83
92
  const bin = runtimeBin(runtime, env)
84
- const r = spawnSync(bin, ['plugin', 'marketplace', 'list'], { encoding: 'utf8' })
93
+ // HARD TIMEOUT the codex CLI hangs FOREVER in a non-tty on ANY subcommand
94
+ // (live 10.06: first `--version`, then `plugin marketplace list` after the
95
+ // presence-probe fix let a live codex through). Timeout → status null →
96
+ // "not registered" → the add (also time-bounded) decides; never a wedge.
97
+ const r = spawnSync(bin, ['plugin', 'marketplace', 'list'], { encoding: 'utf8', timeout: 60_000 })
85
98
  if (r.status !== 0) return false
86
99
  return isAgfpdInList(`${r.stdout ?? ''}`)
87
100
  }
@@ -102,8 +115,12 @@ export function isAgfpdInList(listOutput: string): boolean {
102
115
  /** Register OUR marketplace for this runtime (`<runtime> plugin marketplace add <ref>`). */
103
116
  function registerMarketplace(runtime: OnboardRuntime, env: NodeJS.ProcessEnv): { ok: boolean; detail?: string } {
104
117
  const bin = runtimeBin(runtime, env)
105
- const r = spawnSync(bin, ['plugin', 'marketplace', 'add', MARKETPLACE_REF], { encoding: 'utf8' })
106
- return r.status === 0 ? { ok: true } : { ok: false, detail: (r.stderr ?? '').trim() || `exit ${r.status}` }
118
+ // Same hard timeout as the list probe (codex non-tty hang class) — a wedged add
119
+ // degrades to a loud 'failed' line instead of freezing the host phase.
120
+ const r = spawnSync(bin, ['plugin', 'marketplace', 'add', MARKETPLACE_REF], { encoding: 'utf8', timeout: 120_000 })
121
+ return r.status === 0
122
+ ? { ok: true }
123
+ : { ok: false, detail: (r.stderr ?? '').trim() || (r.status === null ? 'timed out (non-tty hang?)' : `exit ${r.status}`) }
107
124
  }
108
125
 
109
126
  /**
@@ -119,7 +136,7 @@ export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
119
136
  const runtimes = opts.runtimes ?? (['claude', 'codex'] as OnboardRuntime[])
120
137
  const marketplaces: OnboardRuntimeResult[] = []
121
138
  for (const runtime of runtimes) {
122
- if (!isExecutable(runtimeBin(runtime, env))) {
139
+ if (!isExecutable(runtimeBin(runtime, env), env)) {
123
140
  marketplaces.push({ runtime, state: 'runtime-missing' })
124
141
  continue
125
142
  }