@agfpd/iapeer 0.2.8 → 0.2.9

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.8",
3
+ "version": "0.2.9",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,9 +6,10 @@
6
6
  // Flow:
7
7
  // 1. latest = `npm view @agfpd/iapeer version` (the cloud's truth)
8
8
  // 2. installed == latest && !--force → "already latest" (no needless rebuild/restart)
9
- // 3. `npx @agfpd/iapeer@<latest> install` (fetch + rebuild ~/.local/bin/iapeer
10
- // atomically; the COMPILED binary can't rebuild itself from source, so we shell to
11
- // npx, which runs the freshly-fetched package's own install same path consumers use)
9
+ // 3. fetch the published tarball + build from its SOURCE (defaultRunInstall) — the
10
+ // COMPILED binary can't rebuild itself, so we pull the freshly-published package
11
+ // and run ITS own source installer. DELIBERATELY NOT `npx install` (see
12
+ // defaultRunInstall for why npx is unsafe here).
12
13
  // 4. kickstart com.agfpd.iapeer IF loaded (activate the new binary)
13
14
  //
14
15
  // Scope: the foundation ONLY (the @agfpd/iapeer binary + its daemon). It never
@@ -20,7 +21,10 @@
20
21
  // unit-testable with no network and no launchctl; the defaults are the real impls.
21
22
 
22
23
  import { spawnSync } from 'child_process'
24
+ import { mkdtempSync, readdirSync, rmSync } from 'fs'
23
25
  import { connect } from 'net'
26
+ import { tmpdir } from 'os'
27
+ import { join } from 'path'
24
28
  import { IapError } from '../core/errors.ts'
25
29
  import { IAPEER_VERSION } from '../core/version.ts'
26
30
  import { kickstartDaemon, type DaemonRestartResult } from '../launch/launchd.ts'
@@ -144,14 +148,51 @@ function defaultResolveVersion(spec: string, env: NodeJS.ProcessEnv): string | n
144
148
  return SEMVER_RE.test(v) ? v : null
145
149
  }
146
150
 
147
- /** Default installer: `npx -y @agfpd/iapeer@<version> install` (pull from cloud + rebuild). */
151
+ /**
152
+ * Default installer — fetch the published tarball and build from its SOURCE,
153
+ * DELIBERATELY bypassing `npx`. Pull from the cloud + rebuild ~/.local/bin/iapeer.
154
+ *
155
+ * Why not `npx -y @agfpd/iapeer@<v> install`: the package's bin is named `iapeer`,
156
+ * and once `~/.local/bin/iapeer` is on PATH (true on every host AFTER the first
157
+ * install) npx resolves that bin NAME to the COMPILED binary already on PATH and
158
+ * runs ITS `install` — which cannot rebuild itself from source (`bun build --compile`
159
+ * gets a `/$bunfs/root` entrypoint → FileNotFound) — instead of fetching + running the
160
+ * freshly-published source. Verified reproducible (09.06, 0.2.8 deploy): with NO
161
+ * `iapeer` on PATH the same npx invocation prints `command not found` — it never
162
+ * installs the package — so this is a structural bin-name collision, NOT the
163
+ * publish-propagation transient (waiting/retry does not cure it).
164
+ *
165
+ * Deterministic path instead — no npx command-resolution in the loop:
166
+ * 1. `npm pack <pkg>@<v>` → the published tarball (rooted at `package/`).
167
+ * 2. `tar xzf` → extract.
168
+ * 3. `npm install --omit=dev` in the extracted dir — the tarball ships only
169
+ * src/bin (no node_modules), and the source build imports prod deps
170
+ * (@modelcontextprotocol/sdk, …).
171
+ * 4. run the package's OWN bin shim `bash <pkg>/bin/iapeer install` — that is
172
+ * `bun src/cli/index.ts install` from the REAL fetched source → builds the prod
173
+ * binary atomically (keeps `.prev`).
174
+ * Needs npm + tar + bash + bun on PATH (the toolchain the bootstrap already assumes).
175
+ */
148
176
  function defaultRunInstall(version: string, env: NodeJS.ProcessEnv): boolean {
149
177
  if (env.IAPEER_TEST_SANDBOX === '1') {
150
- // A real npx install rebuilds the prod ~/.local/bin/iapeer — never under a test.
151
- throw new IapError('refusing a real `npx install` under IAPEER_TEST_SANDBOX=1 — inject runInstall in tests')
178
+ // A real install rebuilds the prod ~/.local/bin/iapeer — never under a test.
179
+ throw new IapError('refusing a real install under IAPEER_TEST_SANDBOX=1 — inject runInstall in tests')
180
+ }
181
+ const tmp = mkdtempSync(join(tmpdir(), 'iapeer-deploy-'))
182
+ try {
183
+ const pack = spawnSync('npm', ['pack', '--silent', '--pack-destination', tmp, `${IAPEER_PACKAGE}@${version}`], { encoding: 'utf8', env })
184
+ if (pack.status !== 0) return false
185
+ const tgz = readdirSync(tmp).find(f => f.endsWith('.tgz'))
186
+ if (!tgz) return false
187
+ if (spawnSync('tar', ['xzf', join(tmp, tgz), '-C', tmp], { env }).status !== 0) return false
188
+ const pkg = join(tmp, 'package') // npm-pack tarballs always root at `package/`
189
+ const deps = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund', '--silent'], { cwd: pkg, stdio: 'inherit', env })
190
+ if (deps.status !== 0) return false
191
+ const build = spawnSync('bash', [join(pkg, 'bin', 'iapeer'), 'install'], { stdio: 'inherit', env })
192
+ return build.status === 0
193
+ } finally {
194
+ rmSync(tmp, { recursive: true, force: true })
152
195
  }
153
- const r = spawnSync('npx', ['-y', `${IAPEER_PACKAGE}@${version}`, 'install'], { stdio: 'inherit', env })
154
- return r.status === 0
155
196
  }
156
197
 
157
198
  /**
@@ -137,9 +137,9 @@ describe('updateIapeer — failure paths', () => {
137
137
  })
138
138
 
139
139
  describe('updateIapeer — real-installer sandbox guard', () => {
140
- test('default runInstall refuses a real npx install under IAPEER_TEST_SANDBOX', () => {
140
+ test('default runInstall refuses a real install under IAPEER_TEST_SANDBOX', () => {
141
141
  // fetchLatest injected (newer) so the gate proceeds to the DEFAULT installer,
142
- // which must refuse rather than npx-install over the prod ~/.local/bin/iapeer.
142
+ // which must refuse rather than fetch+build over the prod ~/.local/bin/iapeer.
143
143
  expect(() =>
144
144
  updateIapeer({
145
145
  env: { IAPEER_TEST_SANDBOX: '1' },