@agfpd/iapeer 0.4.1 → 0.4.3
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/docs/07-install-and-update.md +19 -10
- package/docs/ru/07-/321/203/321/201/321/202/320/260/320/275/320/276/320/262/320/272/320/260-/320/270-/320/276/320/261/320/275/320/276/320/262/320/273/320/265/320/275/320/270/320/265.md +19 -10
- package/package.json +1 -1
- package/src/cli/index.ts +104 -61
- package/src/onboard/index.ts +61 -3
- package/src/onboard/memory.ts +71 -0
- package/src/update/index.ts +51 -3
|
@@ -47,25 +47,34 @@ You'll see the core version, the daemon's health, and the memory-slot state. The
|
|
|
47
47
|
|
|
48
48
|
A breakdown of each onboard step is in [02 — Quick start](02-quickstart.md); runtimes and memory in detail — in [09](09-runtimes.md) and [11](11-extensions-and-memory.md).
|
|
49
49
|
|
|
50
|
-
## Updating
|
|
50
|
+
## Updating
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
iapeer update
|
|
54
|
-
iapeer update
|
|
55
|
-
iapeer update
|
|
53
|
+
iapeer update # cascade-update the WHOLE stack: core + runtimes + memory
|
|
54
|
+
iapeer update --foundation-only # update just the core
|
|
55
|
+
iapeer update 0.2.80 # pin the CORE to a specific version (core only)
|
|
56
|
+
iapeer update --force # reinstall even if the version is already in place
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
A bare `iapeer update` updates the **whole host stack** in one command — the core (foundation), every installed runtime, and the memory provider — so "updated" means the whole machine, not just the core. The core goes first; the runtimes and memory follow only if the core came up healthy.
|
|
60
|
+
|
|
61
|
+
**1. Core (foundation)** — and it ABORTS the cascade on a hard failure (never update runtimes onto a broken core):
|
|
59
62
|
|
|
60
63
|
1. Learns the target version from npm. If the installed one already matches and there's no `--force` — it does nothing.
|
|
61
|
-
2. Downloads and builds the new binary
|
|
64
|
+
2. Downloads and builds the new binary via `npm pack`, not `npx` (the binary doesn't rebuild itself).
|
|
62
65
|
3. Restarts the daemon onto the new binary.
|
|
63
|
-
4. Restarts the infrastructure launchd jobs the core owns (`com.iapeer.*` — the `timer`, `watcher`, Telegram routers), moving them onto the just-replaced binary.
|
|
64
|
-
5. Verifies the daemon
|
|
66
|
+
4. Restarts the infrastructure launchd jobs the core owns (`com.iapeer.*` — the `timer`, `watcher`, Telegram routers), moving them onto the just-replaced binary. It's the JOBS that restart, not the runtimes' package code — that's the next step. If the daemon didn't come up in step 3, this is skipped.
|
|
67
|
+
5. Verifies the daemon came up healthy — by a probe, not by the restart command exiting: it connects to the daemon's socket and counts it healthy only when it accepts a connection **twice in a row** (a single probe would also pass a daemon that bound the socket and immediately crashed). The check is given 15 seconds; on failure the command says so and advises rolling back.
|
|
68
|
+
|
|
69
|
+
**2. Runtimes** — each installed runtime package (telegram-runtime, notifier-runtime) is version-gated, re-installed, re-provisioned, and its peers restarted (via the regular stop/start — the persistent-peer fleet is refused, never forced).
|
|
70
|
+
|
|
71
|
+
**3. Memory provider** — if a provider is claimed, its own update verb runs (the provider owns its update, its slot version, and its memory-daemon restart).
|
|
72
|
+
|
|
73
|
+
Runtimes and memory are **best-effort**: a component failure is reported and the rest still run; the command exits non-zero if anything failed. Agentic peers (Claude/Codex) pick up new core doctrine lazily on their next wake.
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
`iapeer update --foundation-only` does just step 1 (the narrow case). A pinned `iapeer update <version>` is **core-only** by design — a version pin is core-specific (downgrade, or pin a proven version, deeper than one rollback step).
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
> **Upgrading from before the cascade (< 0.4.2).** The cascade is driven by the *installed* binary, so the very first `iapeer update` from a pre-cascade version runs the OLD (foundation-only) logic: it upgrades the core to the cascade-capable binary but does not yet cascade runtimes + memory. Run `iapeer update` once more — now the new binary cascades the rest. This is a one-time bootstrap; from 0.4.2 onward it is a single command. (A fresh install lands on a cascade-capable binary directly, so it never needs the double run.)
|
|
69
78
|
|
|
70
79
|
## Rollback
|
|
71
80
|
|
|
@@ -47,25 +47,34 @@ iapeer status
|
|
|
47
47
|
|
|
48
48
|
Разбор каждого шага onboard — в [02 — Быстрый старт](02-быстрый-старт.md); рантаймы и память подробно — в [09](09-рантаймы.md) и [11](11-расширения-и-память.md).
|
|
49
49
|
|
|
50
|
-
## Обновление
|
|
50
|
+
## Обновление
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
iapeer update
|
|
54
|
-
iapeer update
|
|
55
|
-
iapeer update
|
|
53
|
+
iapeer update # каскад: обновить ВЕСЬ стек — ядро + рантаймы + память
|
|
54
|
+
iapeer update --foundation-only # обновить только ядро
|
|
55
|
+
iapeer update 0.2.80 # запинить ЯДРО на конкретную версию (только ядро)
|
|
56
|
+
iapeer update --force # переустановить, даже если версия уже стоит
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
Голый `iapeer update` обновляет **весь хостовый стек** одной командой — ядро (фундамент), каждый установленный рантайм и провайдер памяти, — чтобы «обновлено» значило всю машину, а не только ядро. Ядро идёт первым; рантаймы и память следуют, только если ядро поднялось здоровым.
|
|
60
|
+
|
|
61
|
+
**1. Ядро (фундамент)** — и оно ПРЕРЫВАЕТ каскад при жёсткой ошибке (не обновлять рантаймы на сломанное ядро):
|
|
59
62
|
|
|
60
63
|
1. Узнаёт нужную версию из npm. Если установленная уже совпадает и нет `--force` — ничего не делает.
|
|
61
|
-
2. Скачивает и собирает новый
|
|
64
|
+
2. Скачивает и собирает новый бинарь через `npm pack`, а не `npx` (бинарь не пересобирает себя сам).
|
|
62
65
|
3. Перезапускает демон на новый бинарь.
|
|
63
|
-
4. Перезапускает launchd-джобы инфраструктуры, которыми владеет ядро (`com.iapeer.*` — роутеры `timer`, `watcher`, Telegram), переводя их на
|
|
64
|
-
5. Проверяет, что демон
|
|
66
|
+
4. Перезапускает launchd-джобы инфраструктуры, которыми владеет ядро (`com.iapeer.*` — роутеры `timer`, `watcher`, Telegram), переводя их на заменённый бинарь. Перезапускаются именно ДЖОБЫ, а не пакетный код рантаймов — это следующий шаг. Если демон на шаге 3 не поднялся, шаг пропускается.
|
|
67
|
+
5. Проверяет, что демон поднялся здоровым, — пробой, а не кодом возврата команды перезапуска: подключается к сокету демона и считает здоровым, только когда тот принимает соединение **дважды подряд** (одиночная проба прошла бы и у демона, который привязал сокет и тут же упал). На проверку 15 секунд; при провале команда говорит и советует откатиться.
|
|
68
|
+
|
|
69
|
+
**2. Рантаймы** — каждый установленный рантайм-пакет (telegram-runtime, notifier-runtime) проходит version-gate, переустановку, ре-провижн, и его пиры перезапускаются (через обычный stop/start — флот persistent-peer отклоняется, не форсится).
|
|
70
|
+
|
|
71
|
+
**3. Провайдер памяти** — если слот занят, запускается его собственный update-verb (провайдер владеет своим обновлением, версией слота и перезапуском memory-демона).
|
|
72
|
+
|
|
73
|
+
Рантаймы и память — **best-effort**: ошибка компонента сообщается, остальные всё равно отрабатывают; код возврата ненулевой, если что-то упало. Агентские пиры (Claude/Codex) подхватывают новую доктрину ядра лениво на следующем пробуждении.
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
`iapeer update --foundation-only` делает только шаг 1 (узкий случай). Запиненный `iapeer update <версия>` — **только ядро** by design: пин версии специфичен для ядра (откат глубже одного шага или фиксация проверенной версии).
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
> **Обновление с версии до каскада (< 0.4.2).** Каскад исполняет *установленный* бинарь, поэтому самый первый `iapeer update` со старой версии работает по СТАРОЙ (foundation-only) логике: он поднимает ядро до каскад-способного бинаря, но рантаймы + память пока не каскадит. Запусти `iapeer update` ещё раз — теперь новый бинарь докаскадит остальное. Это разовый bootstrap; начиная с 0.4.2 — одна команда. (Свежая установка сразу ставит каскад-способный бинарь, поэтому двойной прогон ей не нужен.)
|
|
69
78
|
|
|
70
79
|
## Откат
|
|
71
80
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type Runtime,
|
|
22
22
|
} from '../core/constants.ts'
|
|
23
23
|
import { IAPEER_VERSION } from '../core/version.ts'
|
|
24
|
-
import { recycleFoundationOwnedInfraJobs, updateIapeer, waitForDaemonHealthy } from '../update/index.ts'
|
|
24
|
+
import { cascadeTail, recycleFoundationOwnedInfraJobs, updateIapeer, waitForDaemonHealthy } from '../update/index.ts'
|
|
25
25
|
import { buildProcessAddress, buildSocketPath, parseSessionName } from '../core/socket.ts'
|
|
26
26
|
import { ensureGlobalIapScaffold } from '../storage/index.ts'
|
|
27
27
|
import { findPeer, readPeersIndex, removePeer, type PeerRecord } from '../registry/index.ts'
|
|
@@ -955,7 +955,10 @@ export function parseArgs(argv: string[]): { positionals: string[]; flags: Recor
|
|
|
955
955
|
* narrow terminal). `sig` = the invocation signature, `desc` = one prose line. */
|
|
956
956
|
const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
957
957
|
{ sig: 'install', desc: 'build binary + global scaffold + daemon plist (one bootstrap)' },
|
|
958
|
-
{
|
|
958
|
+
{
|
|
959
|
+
sig: 'update [version] [--force] [--foundation-only]',
|
|
960
|
+
desc: 'cascade-update the WHOLE stack (foundation + installed runtimes + memory) from npm + restart; --foundation-only = core only; a pinned <version> updates the core only',
|
|
961
|
+
},
|
|
959
962
|
{ sig: 'rollback', desc: 'revert to the previous binary (.prev) + restart daemon + recycle owned infra jobs' },
|
|
960
963
|
{
|
|
961
964
|
sig: 'uninstall [--dry-run] [--yes] [--remove-codesign-identity]',
|
|
@@ -1250,9 +1253,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1250
1253
|
// and the memory provider's install-time sweep (`--all`). NOT slot-gated here —
|
|
1251
1254
|
// an explicit operator/provider action; only the BIRTH-time hook is slot-gated.
|
|
1252
1255
|
const state = positionals[0]
|
|
1253
|
-
if (state !== 'off' && state !== 'on') return usage(
|
|
1256
|
+
if (state !== 'off' && state !== 'on') return argErr(errOut, 'native-memory needs a state — usage: iapeer native-memory <off|on> (--peer <p> | --all)')
|
|
1254
1257
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1255
|
-
if (flags.all !== true && !peerName) return
|
|
1258
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'native-memory needs a target — pass --peer <p> or --all')
|
|
1256
1259
|
const { applyNativeMemory } = await import('../launch/nativeMemory.ts')
|
|
1257
1260
|
const index = readPeersIndex({ env })
|
|
1258
1261
|
const targets = flags.all === true ? index.peers : index.peers.filter(p => p.personality === peerName)
|
|
@@ -1285,7 +1288,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1285
1288
|
// the read-only drift detector for verify pipelines — per-hook
|
|
1286
1289
|
// trusted/missing/drift, exit 1 on anything but full trust.
|
|
1287
1290
|
const target = positionals[0]
|
|
1288
|
-
if (!target) return
|
|
1291
|
+
if (!target) return argErr(errOut, 'trust-hooks needs a hooks file — usage: iapeer trust-hooks <hooks.json> [--check]')
|
|
1289
1292
|
const { preSeedCodexHooksTrust, checkCodexHooksTrust } = await import('../launch/codexHooksTrust.ts')
|
|
1290
1293
|
if (flags.check === true) {
|
|
1291
1294
|
let checks
|
|
@@ -1328,7 +1331,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1328
1331
|
// manifest), THEN deploy its declared peer-set (each → provision + per-peer
|
|
1329
1332
|
// self-config + auto-bootstrap). A runtime whose manifest declares no peers
|
|
1330
1333
|
// (telegram) is operator-add — use `iapeer create <human> --runtime telegram`.
|
|
1331
|
-
if (!positionals[0]) return usage(
|
|
1334
|
+
if (!positionals[0]) return argErr(errOut, 'install-runtime needs a runtime — usage: iapeer install-runtime <runtime> (e.g. telegram | notifier)')
|
|
1332
1335
|
const { onboardRuntime } = await import('../runtime/deploy.ts')
|
|
1333
1336
|
const r = await onboardRuntime({
|
|
1334
1337
|
runtime: positionals[0] as Runtime,
|
|
@@ -1356,7 +1359,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1356
1359
|
// restart the runtime's peers via the regular stop/start. The core's own
|
|
1357
1360
|
// `update` stays foundation-only — this is the runtimes' counterpart.
|
|
1358
1361
|
const all = flags.all === true
|
|
1359
|
-
if (!all && !positionals[0]) return
|
|
1362
|
+
if (!all && !positionals[0]) return argErr(errOut, 'update-runtime needs a runtime or --all — usage: iapeer update-runtime <runtime> | --all')
|
|
1360
1363
|
const { updateRuntime, updateAllRuntimes } = await import('../runtime/update.ts')
|
|
1361
1364
|
const results = all
|
|
1362
1365
|
? await updateAllRuntimes({ force: flags.force === true, env, warn: m => errOut(`warn: ${m}\n`) })
|
|
@@ -1403,7 +1406,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1403
1406
|
// cwd-INDEPENDENT: resolve a location (default ~/.iapeer/peers/<p> or --path),
|
|
1404
1407
|
// scaffold the folder (no-clobber), then init it. Operator-add for an infra
|
|
1405
1408
|
// human (telegram) or any agentic peer; provisions + auto-bootstraps infra.
|
|
1406
|
-
if (!positionals[0]) return
|
|
1409
|
+
if (!positionals[0]) return argErr(errOut, 'create needs a peer name — usage: iapeer create <personality> [--runtime r] [--path dir]')
|
|
1407
1410
|
const explicitRuntime = typeof flags.runtime === 'string' ? (flags.runtime as Runtime) : undefined
|
|
1408
1411
|
// FU5 — never SILENTLY default to claude when both agentic runtimes are
|
|
1409
1412
|
// installed. `--runtime` selects the DEFAULT (not the sole runtime); the
|
|
@@ -1567,7 +1570,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1567
1570
|
: positionals[0]
|
|
1568
1571
|
? [positionals[0]]
|
|
1569
1572
|
: null
|
|
1570
|
-
if (!peers) return
|
|
1573
|
+
if (!peers) return argErr(errOut, 'stop needs a peer or --all — usage: iapeer stop <peer> [runtime] | --all')
|
|
1571
1574
|
const outcomes = peers.flatMap(p => stopPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1572
1575
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1573
1576
|
return outcomes.some(o => o.action === 'refused-foreign-launchd') ? 1 : 0
|
|
@@ -1576,9 +1579,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1576
1579
|
// Fleet-switch enabler (codex-parity audit): add an agentic runtime to
|
|
1577
1580
|
// existing peer(s) — full codex birth chain per target, idempotent.
|
|
1578
1581
|
const rt = positionals[0]
|
|
1579
|
-
if (!rt) return usage(
|
|
1582
|
+
if (!rt) return argErr(errOut, 'add-runtime needs a runtime — usage: iapeer add-runtime <runtime> (--peer <p> | --all)')
|
|
1580
1583
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1581
|
-
if (flags.all !== true && !peerName) return
|
|
1584
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'add-runtime needs a target — pass --peer <p> or --all')
|
|
1582
1585
|
const outcomes = await addRuntime(rt, { peer: peerName, all: flags.all === true, env })
|
|
1583
1586
|
for (const o of outcomes) out(`${o.personality}: ${o.action}${o.detail ? ` — ${o.detail}` : ''}\n`)
|
|
1584
1587
|
return outcomes.some(o => o.action === 'failed') ? 1 : 0
|
|
@@ -1586,9 +1589,9 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1586
1589
|
case 'default-runtime': {
|
|
1587
1590
|
// The PRIMARY flip (routing/wake default) — the fleet-switch moment itself.
|
|
1588
1591
|
const rt = positionals[0]
|
|
1589
|
-
if (!rt) return usage(
|
|
1592
|
+
if (!rt) return argErr(errOut, 'default-runtime needs a runtime — usage: iapeer default-runtime <runtime> (--peer <p> | --all)')
|
|
1590
1593
|
const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
|
|
1591
|
-
if (flags.all !== true && !peerName) return
|
|
1594
|
+
if (flags.all !== true && !peerName) return argErr(errOut, 'default-runtime needs a target — pass --peer <p> or --all')
|
|
1592
1595
|
const outcomes = await defaultRuntime(rt, { peer: peerName, all: flags.all === true, env })
|
|
1593
1596
|
for (const o of outcomes) out(`${o.personality}: ${o.action}${o.detail ? ` — ${o.detail}` : ''}\n`)
|
|
1594
1597
|
return outcomes.some(o => o.action === 'failed') ? 1 : 0
|
|
@@ -1598,7 +1601,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1598
1601
|
// §new): the emergency lever for a hung/dead session, bypasses the peer.
|
|
1599
1602
|
// Source: operator CLI, or telegram-runtime's clean-/new detect (their bot
|
|
1600
1603
|
// shells `iapeer new <peer> <runtime>` — exit 0 ⟺ fresh session up+ready).
|
|
1601
|
-
if (!positionals[0]) return
|
|
1604
|
+
if (!positionals[0]) return argErr(errOut, 'new needs a peer name — usage: iapeer new <peer> [runtime]')
|
|
1602
1605
|
const o = await newPeer(positionals[0], positionals[1], { env })
|
|
1603
1606
|
if (o.action === 'fresh') {
|
|
1604
1607
|
out(`new: ${o.runtime}-${o.personality} fresh session up\n`)
|
|
@@ -1619,7 +1622,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1619
1622
|
: positionals[0]
|
|
1620
1623
|
? [positionals[0]]
|
|
1621
1624
|
: null
|
|
1622
|
-
if (!peers) return
|
|
1625
|
+
if (!peers) return argErr(errOut, 'start needs a peer or --all — usage: iapeer start <peer> [runtime] | --all')
|
|
1623
1626
|
const outcomes = peers.flatMap(p => startPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1624
1627
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}${o.reason ? ` — ${o.reason}` : ''}\n`)
|
|
1625
1628
|
return outcomes.some(o => o.action === 'refused-foreign-launchd') ? 1 : 0
|
|
@@ -1633,7 +1636,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1633
1636
|
: positionals[0]
|
|
1634
1637
|
? [positionals[0]]
|
|
1635
1638
|
: null
|
|
1636
|
-
if (!peers) return
|
|
1639
|
+
if (!peers) return argErr(errOut, 'refresh needs a peer or --all — usage: iapeer refresh <peer> [runtime] | --all')
|
|
1637
1640
|
const outcomes = peers.flatMap(p => refreshPeer(p, flags.all === true ? undefined : positionals[1], { env }))
|
|
1638
1641
|
for (const o of outcomes) out(`${o.personality} (${o.runtime}): ${o.action}\n`)
|
|
1639
1642
|
return 0
|
|
@@ -1642,7 +1645,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1642
1645
|
// Reap a registry record through the locked writer (the operator path over
|
|
1643
1646
|
// registry.removePeer). Idempotent on an absent peer (exit 0). Refuses a LIVE
|
|
1644
1647
|
// peer unless --force (orphaning a running session from routing is the risk).
|
|
1645
|
-
if (!positionals[0]) return
|
|
1648
|
+
if (!positionals[0]) return argErr(errOut, 'remove needs a peer name — usage: iapeer remove <peer> [--force]')
|
|
1646
1649
|
const o = await removePeerCli(positionals[0], { force: flags.force === true, env })
|
|
1647
1650
|
if (o.action === 'removed') {
|
|
1648
1651
|
out(`removed "${o.personality}" from the registry\n`)
|
|
@@ -1684,7 +1687,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1684
1687
|
const mf = flags['message-file']
|
|
1685
1688
|
message = mf === '-' ? readFileSync(0, 'utf8') : readFileSync(mf, 'utf8')
|
|
1686
1689
|
}
|
|
1687
|
-
if (!positionals[0] || message === null) return usage(
|
|
1690
|
+
if (!positionals[0] || message === null) return argErr(errOut, 'send needs a target and a message — usage: iapeer send <target> --message <text> (or --message-file <f|->)')
|
|
1688
1691
|
// --attachment is REPEATABLE; parseArgs collapses repeats (last-wins), so
|
|
1689
1692
|
// re-scan the raw rest argv to collect every attachment path (else files
|
|
1690
1693
|
// silently drop — a text-only smoke test would not catch it).
|
|
@@ -1716,49 +1719,72 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1716
1719
|
return 0
|
|
1717
1720
|
}
|
|
1718
1721
|
case 'update': {
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
|
|
1722
|
+
// CASCADE deploy (FU12): a bare `iapeer update` updates the WHOLE host stack —
|
|
1723
|
+
// foundation → every installed runtime → the memory provider — in one command.
|
|
1724
|
+
// Foundation goes FIRST and ABORTS the cascade on a hard failure (never update
|
|
1725
|
+
// runtimes onto a broken core). Runtimes + memory are BEST-EFFORT (reported, not
|
|
1726
|
+
// fatal). Two narrowings stay foundation-ONLY: `--foundation-only` (explicit) and
|
|
1727
|
+
// a pinned `update <version>` (a version pin is foundation-specific — downgrade /
|
|
1728
|
+
// recover). H4 holds throughout (foreign persistent-peer fleet is refused, never
|
|
1729
|
+
// forced — the runtime restart goes via the guarded stop/start). Cloud-only; the
|
|
1730
|
+
// first-ever install is `npx @agfpd/iapeer`.
|
|
1731
|
+
const pinned = positionals[0]
|
|
1732
|
+
const foundationOnly = flags['foundation-only'] === true
|
|
1733
|
+
const cascading = !foundationOnly && !pinned
|
|
1734
|
+
if (cascading) {
|
|
1735
|
+
// no-surprise heads-up: this restarts peers and can run minutes.
|
|
1736
|
+
out(
|
|
1737
|
+
'iapeer update — updating the WHOLE stack: foundation + installed runtimes + memory provider.\n' +
|
|
1738
|
+
'This rebuilds the daemon, restarts the telegram/notifier services and the memory daemon,\n' +
|
|
1739
|
+
'and can take a few minutes; agentic peers pick up the new code on their next wake.\n' +
|
|
1740
|
+
'(`iapeer update --foundation-only` updates just the core.)\n\n',
|
|
1741
|
+
)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// ── Foundation (the core; a hard failure aborts the cascade) ──
|
|
1745
|
+
const r = updateIapeer({ env, force: flags.force === true, targetVersion: pinned })
|
|
1746
|
+
let foundationHardFail = false
|
|
1726
1747
|
if (r.status === 'failed') {
|
|
1727
1748
|
errOut(`update failed: ${r.reason}\n`)
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
// 'updated': the binary is swapped. If the daemon was restarted, VERIFY it
|
|
1735
|
-
// actually came back up before declaring success — a new binary that fails to
|
|
1736
|
-
// boot must not read as a healthy deploy (it's the cue to roll back).
|
|
1737
|
-
if (r.daemon === 'restarted') {
|
|
1749
|
+
foundationHardFail = true
|
|
1750
|
+
} else if (r.status === 'already-latest') {
|
|
1751
|
+
out(`foundation: already at version ${r.from}\n`)
|
|
1752
|
+
} else if (r.daemon === 'restarted') {
|
|
1753
|
+
// 'updated' + daemon restarted: VERIFY it actually came back up (a binary that
|
|
1754
|
+
// fails to boot must not read as a healthy deploy — it's the cue to roll back).
|
|
1738
1755
|
const h = await waitForDaemonHealthy({ env })
|
|
1739
1756
|
if (!h.healthy) {
|
|
1740
|
-
errOut(`updated ${r.from} → ${r.to} but the daemon is NOT healthy after restart (${h.detail}).\n`
|
|
1741
|
-
|
|
1742
|
-
|
|
1757
|
+
errOut(`foundation: updated ${r.from} → ${r.to} but the daemon is NOT healthy after restart (${h.detail}).\nroll back now: iapeer rollback\n`)
|
|
1758
|
+
foundationHardFail = true
|
|
1759
|
+
} else {
|
|
1760
|
+
const note = infraRecycleNote(r.infra)
|
|
1761
|
+
;(infraRecycleFailed(r.infra) ? errOut : out)(`foundation: updated ${r.from} → ${r.to}; daemon restarted and healthy${note}\n`)
|
|
1743
1762
|
}
|
|
1763
|
+
} else {
|
|
1764
|
+
const daemonNote =
|
|
1765
|
+
r.daemon === 'not-loaded'
|
|
1766
|
+
? 'daemon not loaded — new binary will be used on next start'
|
|
1767
|
+
: r.daemon === 'failed'
|
|
1768
|
+
? `WARNING — ${r.reason}; roll back with: iapeer rollback`
|
|
1769
|
+
: String(r.daemon)
|
|
1770
|
+
if (r.daemon === 'failed') foundationHardFail = true
|
|
1744
1771
|
const note = infraRecycleNote(r.infra)
|
|
1745
|
-
|
|
1746
|
-
errOut(`updated ${r.from} → ${r.to}; daemon restarted and healthy${note}\n`)
|
|
1747
|
-
return 1
|
|
1748
|
-
}
|
|
1749
|
-
out(`updated ${r.from} → ${r.to}; daemon restarted and healthy${note}\n`)
|
|
1750
|
-
return 0
|
|
1772
|
+
;(r.daemon === 'failed' || infraRecycleFailed(r.infra) ? errOut : out)(`foundation: updated ${r.from} → ${r.to}; ${daemonNote}${note}\n`)
|
|
1751
1773
|
}
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1774
|
+
const foundationExit = foundationHardFail || infraRecycleFailed(r.infra) ? 1 : 0
|
|
1775
|
+
|
|
1776
|
+
// foundation-only / pinned / hard-fail → stop here (don't cascade onto a broken core).
|
|
1777
|
+
if (!cascading || foundationHardFail) return foundationExit
|
|
1778
|
+
|
|
1779
|
+
// ── Cascade tail: runtimes + memory provider (best-effort) ──
|
|
1780
|
+
const { updateAllRuntimes } = await import('../runtime/update.ts')
|
|
1781
|
+
const { updateMemoryProvider } = await import('../onboard/memory.ts')
|
|
1782
|
+
const tail = await cascadeTail({
|
|
1783
|
+
runtimes: () => updateAllRuntimes({ force: flags.force === true, env, warn: m => errOut(`warn: ${m}\n`) }),
|
|
1784
|
+
memory: () => updateMemoryProvider({ env }),
|
|
1785
|
+
out,
|
|
1786
|
+
})
|
|
1787
|
+
return foundationExit === 0 && !tail.failed ? 0 : 1
|
|
1762
1788
|
}
|
|
1763
1789
|
case 'rollback': {
|
|
1764
1790
|
// Recovery: restore the .prev binary kept by the last install, restart the
|
|
@@ -1909,7 +1935,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1909
1935
|
// Ф-F: the always-on infra entrypoint (telegram/notifier), held by launchd.
|
|
1910
1936
|
// The infra plist runs `iapeer run-infra <personality> <runtime>` (installed
|
|
1911
1937
|
// binary) instead of `bun launchdRun.ts`. cwd = the launchd WorkingDirectory.
|
|
1912
|
-
if (!positionals[0] || !positionals[1]) return usage(
|
|
1938
|
+
if (!positionals[0] || !positionals[1]) return argErr(errOut, 'run-infra needs a runtime and a personality — usage: iapeer run-infra <runtime> <personality> (internal launchd entrypoint)')
|
|
1913
1939
|
return await runAlwaysOn(positionals[0], positionals[1], process.cwd())
|
|
1914
1940
|
}
|
|
1915
1941
|
case 'self-done': {
|
|
@@ -1983,7 +2009,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1983
2009
|
case 'interrupt': {
|
|
1984
2010
|
// In-session control (Ф-E, clean-slash namespace): interrupt a stuck/raving
|
|
1985
2011
|
// turn (Escape). UNCONDITIONAL — acts on the live session.
|
|
1986
|
-
if (!positionals[0]) return
|
|
2012
|
+
if (!positionals[0]) return argErr(errOut, 'interrupt needs a peer name — usage: iapeer interrupt <peer> [runtime]')
|
|
1987
2013
|
const r = await routeControl(positionals[0], positionals[1], { name: verb })
|
|
1988
2014
|
if (!r.ok) {
|
|
1989
2015
|
errOut(`${verb}: ${r.error.message}\n`)
|
|
@@ -1996,7 +2022,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1996
2022
|
// Dialogue control: if the session is merely asleep
|
|
1997
2023
|
// after a clean idle-reap, resume the same dialogue and compact it; if there
|
|
1998
2024
|
// is no resumable dialogue, fail honestly instead of compacting a fresh one.
|
|
1999
|
-
if (!positionals[0]) return
|
|
2025
|
+
if (!positionals[0]) return argErr(errOut, 'compact needs a peer name — usage: iapeer compact <peer> [runtime]')
|
|
2000
2026
|
const o = await compactPeer(positionals[0], positionals[1], { env })
|
|
2001
2027
|
if (o.action === 'compacted') {
|
|
2002
2028
|
out(`compact → ${o.personality} (${o.runtime})${o.woke ? ' after resume' : ''}\n`)
|
|
@@ -2010,7 +2036,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2010
2036
|
// `connect telegram <peer> [--token <t>]`. The human owes only the token;
|
|
2011
2037
|
// alias/bot-add/interface/router-restart are resolved by the system. The
|
|
2012
2038
|
// FIRST message from the human to the bot activates the chat (platform rule).
|
|
2013
|
-
if (positionals[0] !== 'telegram' || !positionals[1]) return usage(
|
|
2039
|
+
if (positionals[0] !== 'telegram' || !positionals[1]) return argErr(errOut, 'connect needs "telegram <peer>" — usage: iapeer connect telegram <peer> [--token <t>] (e.g. iapeer connect telegram boris)')
|
|
2014
2040
|
const { connectTelegram } = await import('../connect/index.ts')
|
|
2015
2041
|
const r = await connectTelegram({
|
|
2016
2042
|
peer: positionals[1],
|
|
@@ -2040,7 +2066,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2040
2066
|
// per-runtime (claude project-scope IN the peer cwd / codex global) + enable +
|
|
2041
2067
|
// call the plugin's `setup` ONLY if its iapeer.json declares it. Idempotent and
|
|
2042
2068
|
// fleet-safe — claude is keyed by the peer's projectPath. `enable <plugin> [peer]`.
|
|
2043
|
-
if (!positionals[0]) return
|
|
2069
|
+
if (!positionals[0]) return argErr(errOut, 'enable needs a capability/plugin — usage: iapeer enable <plugin> [peer]')
|
|
2044
2070
|
const { enableCapability } = await import('../enable/index.ts')
|
|
2045
2071
|
const r = enableCapability({
|
|
2046
2072
|
plugin: positionals[0],
|
|
@@ -2055,7 +2081,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2055
2081
|
return r.runtimes.some(rt => rt.state === 'failed') || r.setup === 'failed' ? 1 : 0
|
|
2056
2082
|
}
|
|
2057
2083
|
case 'attach': {
|
|
2058
|
-
if (!positionals[0]) return
|
|
2084
|
+
if (!positionals[0]) return argErr(errOut, 'attach needs a peer name — usage: iapeer attach <peer> [runtime]')
|
|
2059
2085
|
const r = await attachPeer({ personality: positionals[0], runtime: positionals[1], env })
|
|
2060
2086
|
if (!r.ok) {
|
|
2061
2087
|
errOut(`attach: ${r.reason}\n`)
|
|
@@ -2131,7 +2157,14 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2131
2157
|
const id = resolveIdentity({ env })
|
|
2132
2158
|
const r = await folderLaunch({ cwd: process.cwd(), runtime: verb, env, cfg })
|
|
2133
2159
|
if (r.status === 'FAILED') {
|
|
2134
|
-
|
|
2160
|
+
// A bare token format-matches a runtime, so it dispatched here as `iapeer
|
|
2161
|
+
// <runtime>` (launch the cwd peer on that runtime). When that fails, the user
|
|
2162
|
+
// most often just MISTYPED a command — surface BOTH readings so the message is
|
|
2163
|
+
// actionable instead of a bare "runtime not declared" (FU13).
|
|
2164
|
+
errOut(
|
|
2165
|
+
`iapeer: "${verb}" — ${r.reason}\n` +
|
|
2166
|
+
' If you meant a command, run `iapeer help`. `iapeer <runtime>` launches a runtime declared for the current peer.\n',
|
|
2167
|
+
)
|
|
2135
2168
|
return 1
|
|
2136
2169
|
}
|
|
2137
2170
|
const identity = r.process_address ?? buildProcessAddress(verb, id.personality)
|
|
@@ -2143,6 +2176,7 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
2143
2176
|
out(`launched ${identity} (fresh)\n`)
|
|
2144
2177
|
return 0
|
|
2145
2178
|
}
|
|
2179
|
+
errOut(`iapeer: unknown verb "${verb ?? ''}"\n`)
|
|
2146
2180
|
return usage(errOut)
|
|
2147
2181
|
}
|
|
2148
2182
|
}
|
|
@@ -2157,6 +2191,15 @@ function usage(errOut: (s: string) => void): number {
|
|
|
2157
2191
|
return 2
|
|
2158
2192
|
}
|
|
2159
2193
|
|
|
2194
|
+
/** A TARGETED argument error (FU13): a one-line, addressed, actionable reason for THIS
|
|
2195
|
+
* verb — never the generic top-level help wall (which reads as "nothing happened" to a
|
|
2196
|
+
* user who just mistyped one verb). `message` should name what's missing + the verb's
|
|
2197
|
+
* own form, e.g. "connect telegram needs a peer — usage: iapeer connect telegram <peer>". */
|
|
2198
|
+
function argErr(errOut: (s: string) => void, message: string): number {
|
|
2199
|
+
errOut(`iapeer: ${message}\n`)
|
|
2200
|
+
return 2
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2160
2203
|
/** Interactive default-runtime picker (FU5) — only called on a real TTY when more
|
|
2161
2204
|
* than one agentic runtime is installed and no `--runtime` was given. Accepts a
|
|
2162
2205
|
* number or the runtime name; empty input takes the first (the listed default). */
|
package/src/onboard/index.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface OnboardOptions {
|
|
|
46
46
|
/** Restrict to these runtimes (default both). */
|
|
47
47
|
runtimes?: OnboardRuntime[]
|
|
48
48
|
env?: NodeJS.ProcessEnv
|
|
49
|
+
/** Injectable (tests). Default registerMarketplace. */
|
|
50
|
+
register?: (runtime: OnboardRuntime, env: NodeJS.ProcessEnv) => { ok: boolean; detail?: string }
|
|
51
|
+
/** Injectable (tests). Default isMarketplaceRegistered. */
|
|
52
|
+
isRegistered?: (runtime: OnboardRuntime, env: NodeJS.ProcessEnv) => boolean
|
|
53
|
+
/** Injectable (tests). Default Bun.sleepSync — the pause before the one transient retry. */
|
|
54
|
+
sleep?: (ms: number) => void
|
|
55
|
+
/** Delay (ms) before the single non-timeout retry. Default 2000. */
|
|
56
|
+
retryDelayMs?: number
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -192,13 +200,17 @@ export function refreshMarketplace(
|
|
|
192
200
|
export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
|
|
193
201
|
const env = opts.env ?? process.env
|
|
194
202
|
const runtimes = opts.runtimes ?? (['claude', 'codex'] as OnboardRuntime[])
|
|
203
|
+
const isRegistered = opts.isRegistered ?? isMarketplaceRegistered
|
|
204
|
+
const register = opts.register ?? registerMarketplace
|
|
205
|
+
const sleep = opts.sleep ?? ((ms: number) => Bun.sleepSync(ms))
|
|
206
|
+
const retryDelayMs = opts.retryDelayMs ?? 2000
|
|
195
207
|
const marketplaces: OnboardRuntimeResult[] = []
|
|
196
208
|
for (const runtime of runtimes) {
|
|
197
209
|
if (!isExecutable(runtimeBin(runtime, env), env)) {
|
|
198
210
|
marketplaces.push({ runtime, state: 'runtime-missing' })
|
|
199
211
|
continue
|
|
200
212
|
}
|
|
201
|
-
if (
|
|
213
|
+
if (isRegistered(runtime, env)) {
|
|
202
214
|
marketplaces.push({ runtime, state: 'already-registered' })
|
|
203
215
|
continue
|
|
204
216
|
}
|
|
@@ -206,13 +218,59 @@ export function onboardHost(opts: OnboardOptions = {}): OnboardResult {
|
|
|
206
218
|
marketplaces.push({ runtime, state: 'would-register' })
|
|
207
219
|
continue
|
|
208
220
|
}
|
|
209
|
-
|
|
210
|
-
marketplaces.push({ runtime, state: r.ok ? 'registered' : 'failed', detail: r.detail })
|
|
221
|
+
marketplaces.push(resolveMarketplaceAdd(runtime, env, register, isRegistered, sleep, retryDelayMs))
|
|
211
222
|
}
|
|
212
223
|
const noop = marketplaces.every(m => m.state !== 'registered')
|
|
213
224
|
return { marketplaces, noop }
|
|
214
225
|
}
|
|
215
226
|
|
|
227
|
+
/**
|
|
228
|
+
* One `marketplace add` with first-run-transient resilience (Arthur's fresh-run found a
|
|
229
|
+
* scary red "failed" on a step that actually self-heals — the failure is transient, the
|
|
230
|
+
* marketplace ends up registered). Layered:
|
|
231
|
+
* (a) SELF-HEAL: if the add reports failure but a re-check shows the marketplace IS
|
|
232
|
+
* present (it landed despite a non-zero tail / a race), report `registered`.
|
|
233
|
+
* (c) ONE retry after a short delay — only for a NON-timeout transient (a network/git
|
|
234
|
+
* blip on the GitHub clone). NEVER on a timeout: a wedged / macOS-launch-approval-
|
|
235
|
+
* parked binary would just re-wedge, doubling the wait.
|
|
236
|
+
* (b) on a timeout signature, report `failed` with the macOS-approval advisory; on any
|
|
237
|
+
* other persistent failure, a transient-retry hint. The marketplace is OPTIONAL for
|
|
238
|
+
* core function, so a clear actionable line beats a bare "failed".
|
|
239
|
+
*/
|
|
240
|
+
function resolveMarketplaceAdd(
|
|
241
|
+
runtime: OnboardRuntime,
|
|
242
|
+
env: NodeJS.ProcessEnv,
|
|
243
|
+
register: (r: OnboardRuntime, e: NodeJS.ProcessEnv) => { ok: boolean; detail?: string },
|
|
244
|
+
isRegistered: (r: OnboardRuntime, e: NodeJS.ProcessEnv) => boolean,
|
|
245
|
+
sleep: (ms: number) => void,
|
|
246
|
+
retryDelayMs: number,
|
|
247
|
+
): OnboardRuntimeResult {
|
|
248
|
+
const first = register(runtime, env)
|
|
249
|
+
if (first.ok) return { runtime, state: 'registered' }
|
|
250
|
+
// (a) self-heal — present despite the reported error?
|
|
251
|
+
if (isRegistered(runtime, env)) {
|
|
252
|
+
return { runtime, state: 'registered', detail: 'add reported an error, but the marketplace is present' }
|
|
253
|
+
}
|
|
254
|
+
const timedOut = /timed out/i.test(first.detail ?? '')
|
|
255
|
+
if (!timedOut) {
|
|
256
|
+
// (c) one retry for a non-timeout transient.
|
|
257
|
+
sleep(retryDelayMs)
|
|
258
|
+
const second = register(runtime, env)
|
|
259
|
+
if (second.ok || isRegistered(runtime, env)) return { runtime, state: 'registered', detail: 'succeeded on retry' }
|
|
260
|
+
return {
|
|
261
|
+
runtime,
|
|
262
|
+
state: 'failed',
|
|
263
|
+
detail: `${second.detail ?? first.detail} — transient (network/git)? re-run \`iapeer onboard\`; the marketplace is optional for core function`,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// (b) timeout signature → the known macOS launch-approval park.
|
|
267
|
+
return {
|
|
268
|
+
runtime,
|
|
269
|
+
state: 'failed',
|
|
270
|
+
detail: `${first.detail} — macOS may be verifying the runtime binary; approve it in System Settings → Privacy & Security, then re-run \`iapeer onboard\``,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
216
274
|
/**
|
|
217
275
|
* macOS Full Disk Access (TCC) advisory printed at the END of onboard. The runtime permission layer
|
|
218
276
|
* (`--add-dir` / `additionalDirectories` for claude, the sandbox flags for codex) governs what the
|
package/src/onboard/memory.ts
CHANGED
|
@@ -144,3 +144,74 @@ export async function onboardMemoryProvider(opts: MemoryOnboardOptions = {}): Pr
|
|
|
144
144
|
}
|
|
145
145
|
return { state: 'installed', provider: after }
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// updateMemoryProvider — the memory leg of the cascade `iapeer update` (FU12)
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export interface MemoryUpdateResult {
|
|
153
|
+
state:
|
|
154
|
+
| 'updated' // provider update ran and the slot version advanced
|
|
155
|
+
| 'already-latest' // ran but the slot version is unchanged
|
|
156
|
+
| 'no-slot' // no provider claimed — nothing to update
|
|
157
|
+
| 'skipped-unavailable' // package not published / no network — soft skip
|
|
158
|
+
| 'failed' // provider update exited non-zero
|
|
159
|
+
package?: string
|
|
160
|
+
from?: string
|
|
161
|
+
to?: string
|
|
162
|
+
detail?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The provider's CLI bin name = the unscoped package name (`@agfpd/iapeer-memory` →
|
|
166
|
+
* `iapeer-memory`). */
|
|
167
|
+
function memoryBinName(pkg: string): string {
|
|
168
|
+
return pkg.replace(/^@[^/]+\//, '')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function defaultRunMemoryUpdate(pkg: string, env: NodeJS.ProcessEnv): { status: number | null; unavailable: boolean } {
|
|
172
|
+
// Availability probe FIRST (cheap, bounded): unpublished / no network → soft-skip.
|
|
173
|
+
const probe = spawnSync('npm', ['view', `${pkg}@latest`, 'version'], { encoding: 'utf8', env, timeout: 60_000 })
|
|
174
|
+
if (probe.status !== 0) return { status: probe.status, unavailable: true }
|
|
175
|
+
// The provider ships a COMPILED bin on PATH (~/.local/bin/<bin>), NOT an npm package,
|
|
176
|
+
// so a bare `npx <bin>` / `npx <pkg>@latest` re-runs the STALE installed binary —
|
|
177
|
+
// verified by the provider: it reports the OLD version, "running FROM the installed
|
|
178
|
+
// binary", a SILENT non-update (fatal for the upgrade case). `npm exec
|
|
179
|
+
// --package=<pkg>@latest -- <bin> update` fetches latest + rebuilds, bypassing the PATH
|
|
180
|
+
// name-shadow. The provider OWNS update + slot-write + its own daemon (memoryd) restart.
|
|
181
|
+
const r = spawnSync(
|
|
182
|
+
'npm',
|
|
183
|
+
['exec', '--yes', `--package=${pkg}@latest`, '--', memoryBinName(pkg), 'update'],
|
|
184
|
+
{ stdio: 'inherit', env },
|
|
185
|
+
)
|
|
186
|
+
return { status: r.status, unavailable: false }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update the CLAIMED memory provider via ITS OWN npx update verb (slot contract: the
|
|
191
|
+
* provider owns install/update + slot-write + peer restart; the core only invokes +
|
|
192
|
+
* reads the slot). No slot → nothing to update. Reports from→to by reading the slot
|
|
193
|
+
* version before/after. BEST-EFFORT by the caller (the cascade never aborts on it).
|
|
194
|
+
*/
|
|
195
|
+
export function updateMemoryProvider(opts: {
|
|
196
|
+
env?: NodeJS.ProcessEnv
|
|
197
|
+
dryRun?: boolean
|
|
198
|
+
runUpdate?: (pkg: string, env: NodeJS.ProcessEnv) => { status: number | null; unavailable: boolean }
|
|
199
|
+
} = {}): MemoryUpdateResult {
|
|
200
|
+
const env = opts.env ?? process.env
|
|
201
|
+
const slot = readMemoryProvider(env)
|
|
202
|
+
if (!slot) return { state: 'no-slot', detail: 'no memory provider claimed — nothing to update' }
|
|
203
|
+
const pkg = slot.package
|
|
204
|
+
const from = slot.version
|
|
205
|
+
if (opts.dryRun) {
|
|
206
|
+
return { state: 'updated', package: pkg, from, detail: `would run: npm exec --package=${pkg}@latest -- ${memoryBinName(pkg)} update` }
|
|
207
|
+
}
|
|
208
|
+
const run = opts.runUpdate ?? defaultRunMemoryUpdate
|
|
209
|
+
const r = run(pkg, env)
|
|
210
|
+
if (r.unavailable) {
|
|
211
|
+
return { state: 'skipped-unavailable', package: pkg, from, detail: `${pkg}@latest not reachable (no network / unpublished)` }
|
|
212
|
+
}
|
|
213
|
+
if (r.status !== 0) return { state: 'failed', package: pkg, from, detail: `provider update exited ${r.status ?? 'null'}` }
|
|
214
|
+
const to = readMemoryProvider(env)?.version
|
|
215
|
+
if (from && to && from === to) return { state: 'already-latest', package: pkg, from, to }
|
|
216
|
+
return { state: 'updated', package: pkg, from, to }
|
|
217
|
+
}
|
package/src/update/index.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// `
|
|
2
|
-
// from npm (the cloud) and restart the daemon onto it. There is deliberately
|
|
3
|
-
// "deploy from a working tree" — every host, including the dev host, activates a
|
|
1
|
+
// `updateIapeer` — the FOUNDATION leg of the deploy: pull the latest published
|
|
2
|
+
// foundation from npm (the cloud) and restart the daemon onto it. There is deliberately
|
|
3
|
+
// NO "deploy from a working tree" — every host, including the dev host, activates a
|
|
4
4
|
// release the same way: `npm run release` (publish) → `iapeer update` (pull + restart).
|
|
5
5
|
//
|
|
6
|
+
// NB the `iapeer update` VERB cascades the whole stack (FU12): it runs this foundation
|
|
7
|
+
// leg FIRST (aborting on a hard failure), then `updateAllRuntimes` + the memory provider
|
|
8
|
+
// (best-effort) via cascadeTail (below). updateIapeer itself stays foundation-scoped —
|
|
9
|
+
// the cascade is composed in the CLI verb.
|
|
10
|
+
//
|
|
6
11
|
// Flow:
|
|
7
12
|
// 1. latest = `npm view @agfpd/iapeer version` (the cloud's truth)
|
|
8
13
|
// 2. installed == latest && !--force → "already latest" (no needless rebuild/restart)
|
|
@@ -43,6 +48,10 @@ import {
|
|
|
43
48
|
} from '../launch/launchd.ts'
|
|
44
49
|
import { cycleShadowJob, SHADOW_PERSONALITY, SHADOW_PLIST_LABEL } from '../shadow/install.ts'
|
|
45
50
|
import { defaultDaemonSocketPath } from '../daemon/index.ts'
|
|
51
|
+
// Type-only (erased at runtime → no import cycle): the cascade tail renders these
|
|
52
|
+
// shapes but receives the component-updaters as injected deps.
|
|
53
|
+
import type { UpdateRuntimeResult } from '../runtime/update.ts'
|
|
54
|
+
import type { MemoryUpdateResult } from '../onboard/memory.ts'
|
|
46
55
|
|
|
47
56
|
/** The npm package the foundation publishes / updates from. */
|
|
48
57
|
export const IAPEER_PACKAGE = '@agfpd/iapeer'
|
|
@@ -316,3 +325,42 @@ export function updateIapeer(deps: UpdateDeps = {}): UpdateResult {
|
|
|
316
325
|
reason: d.state === 'failed' ? `binary updated but daemon restart failed: ${d.detail ?? ''}`.trim() : undefined,
|
|
317
326
|
}
|
|
318
327
|
}
|
|
328
|
+
|
|
329
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
330
|
+
// cascadeTail — the runtimes + memory legs of the cascade `iapeer update` (FU12).
|
|
331
|
+
// Runs AFTER a healthy foundation update (the CLI owns that, abort-on-hard-fail);
|
|
332
|
+
// here we update every installed runtime then the memory provider, BEST-EFFORT — a
|
|
333
|
+
// component failure is reported, never aborts the rest. Pure orchestration over
|
|
334
|
+
// injected component-updaters (no value-import of runtime/onboard → no cycle), so it
|
|
335
|
+
// is hermetically testable. Returns whether anything failed (→ exit≠0).
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
export interface CascadeTailDeps {
|
|
339
|
+
/** = updateAllRuntimes (runtime/update.ts). */
|
|
340
|
+
runtimes: () => Promise<UpdateRuntimeResult[]>
|
|
341
|
+
/** = updateMemoryProvider (onboard/memory.ts). */
|
|
342
|
+
memory: () => MemoryUpdateResult | Promise<MemoryUpdateResult>
|
|
343
|
+
out: (s: string) => void
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function cascadeTail(deps: CascadeTailDeps): Promise<{ failed: boolean }> {
|
|
347
|
+
let failed = false
|
|
348
|
+
|
|
349
|
+
deps.out('runtimes:\n')
|
|
350
|
+
const rs = await deps.runtimes()
|
|
351
|
+
if (rs.length === 0) deps.out(' (none installed)\n')
|
|
352
|
+
for (const r of rs) {
|
|
353
|
+
const ver = r.from || r.to ? ` ${r.from ?? '?'} → ${r.to ?? '?'}` : ''
|
|
354
|
+
deps.out(` ${r.runtime}: ${r.state}${ver}${r.detail ? ` — ${r.detail}` : ''}\n`)
|
|
355
|
+
for (const p of r.restarted) deps.out(` restart ${p.personality}: ${p.state}${p.detail ? ` — ${p.detail}` : ''}\n`)
|
|
356
|
+
if (r.state === 'install-failed' || r.state === 'deploy-failed' || r.state === 'npm-unreachable') failed = true
|
|
357
|
+
if (r.restarted.some(p => p.state === 'failed')) failed = true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const m = await deps.memory()
|
|
361
|
+
const mver = m.from || m.to ? ` ${m.from ?? '?'} → ${m.to ?? '?'}` : ''
|
|
362
|
+
deps.out(`memory: ${m.state}${m.package ? ` (${m.package}${mver})` : ''}${m.detail ? ` — ${m.detail}` : ''}\n`)
|
|
363
|
+
if (m.state === 'failed') failed = true
|
|
364
|
+
|
|
365
|
+
return { failed }
|
|
366
|
+
}
|