@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 +1 -1
- package/src/update/index.ts +49 -8
- package/src/update/update.test.ts +2 -2
package/package.json
CHANGED
package/src/update/index.ts
CHANGED
|
@@ -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.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
151
|
-
throw new IapError('refusing a real
|
|
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
|
|
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
|
|
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' },
|