@agfpd/iapeer 0.4.1 → 0.4.2
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 +17 -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 +17 -10
- package/package.json +1 -1
- package/src/cli/index.ts +64 -38
- package/src/onboard/memory.ts +71 -0
- package/src/update/index.ts +51 -3
|
@@ -47,25 +47,32 @@ 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).
|
|
65
72
|
|
|
66
|
-
|
|
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.
|
|
67
74
|
|
|
68
|
-
|
|
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).
|
|
69
76
|
|
|
70
77
|
## Rollback
|
|
71
78
|
|
|
@@ -47,25 +47,32 @@ 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-демона).
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
Рантаймы и память — **best-effort**: ошибка компонента сообщается, остальные всё равно отрабатывают; код возврата ненулевой, если что-то упало. Агентские пиры (Claude/Codex) подхватывают новую доктрину ядра лениво на следующем пробуждении.
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
`iapeer update --foundation-only` делает только шаг 1 (узкий случай). Запиненный `iapeer update <версия>` — **только ядро** by design: пин версии специфичен для ядра (откат глубже одного шага или фиксация проверенной версии).
|
|
69
76
|
|
|
70
77
|
## Откат
|
|
71
78
|
|
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]',
|
|
@@ -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
|
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
|
+
}
|