@ouro.bot/cli 0.1.0-alpha.864 → 0.1.0-alpha.866
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/changelog.json +17 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +179 -24
- package/deploy/unraid/sanctuary-replay-gate.mjs +533 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/a2a/delegated-command.js +9 -3
- package/dist/a2a/replay-harness.js +147 -0
- package/dist/heart/awaiting/a2a-await-delivery.js +6 -0
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +71 -22
- package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +31 -1
- package/dist/heart/daemon/sanctuary-telegram-authority-service.js +25 -12
- package/dist/senses/a2a-entry.js +2 -0
- package/dist/senses/private-runtime.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.866",
|
|
6
|
+
"changes": [
|
|
7
|
+
"The Sanctuary lifecycle now relaunches a gateway that exits before readiness (backoff of 2, 10 and 30 seconds) within one 15-minute readiness deadline and, if every attempt fails, throws promptly with the gateway.log tail (token-shaped text masked before the tail is cut) instead of waiting out a fresh budget.",
|
|
8
|
+
"The Sanctuary Telegram authority retries getMe with backoff on network-class errors (fetch failed, ECONNRESET, ETIMEDOUT, 5xx) and still fails at once on auth errors such as 401.",
|
|
9
|
+
"The Sanctuary authority socket server handles client connection errors by logging them and skips writes to destroyed connections, so a client that disconnects mid-response no longer crashes the gateway with write EPIPE.",
|
|
10
|
+
"The Butler upgrade script checks that api.telegram.org is reachable before pausing host supervision and aborts early with a clear message if it is not; it skips the check when an upgrade journal exists (resume or rollback)."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.865",
|
|
15
|
+
"changes": [
|
|
16
|
+
"Added an operator-set replay window: while the host's replay gate holds it open for a replay peer, that peer's delegated-command owner notices and A2A await deliveries go to a local sink instead of the owner's Telegram chat, with every authorization check unchanged, a failed sink write refusing the command, and a nerves event for each notice sunk. The window counts only when its directory is root-owned and not group- or other-writable and the window file, opened once with O_NOFOLLOW and checked by descriptor, is a root-owned regular file nobody else can write, and a sink record satisfies an await's noticed check only for the same friend inside such a window.",
|
|
17
|
+
"Added the host-side replay gate (sanctuary-replay-gate.mjs) that provisions two replay peers and replays seven real requests over A2A, reading each outcome back from tool traces, the steward policy hash, the books ledger, the Sonarr queue, await archives and Telegram effect records; unreadable evidence now fails a case instead of skipping or passing it.",
|
|
18
|
+
"Made Sanctuary upgrades hold the new version uncommitted until the replay gate passes: a pass commits, pins the template and prunes images, a failed gate rolls back to the predecessor and exits 1, and the commit is refused unless the held upgrade is still the live one. Added --no-gate, --plant and verify --gate, plus a --hold-commit option on the root lifecycle because a completed upgrade cannot be rolled back."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
4
21
|
{
|
|
5
22
|
"version": "0.1.0-alpha.864",
|
|
6
23
|
"changes": [
|
|
@@ -29,11 +29,21 @@
|
|
|
29
29
|
// refuses without a rotated incoming-token. Run `upgrade` detached so an SSH drop
|
|
30
30
|
// cannot interrupt it: setsid nohup node <this> upgrade <version> > /var/log/ouro-upgrade.log 2>&1 &
|
|
31
31
|
//
|
|
32
|
-
//
|
|
32
|
+
// An `upgrade` ends with the replay gate (sanctuary-replay-gate.mjs): the new Butler is held UNCOMMITTED (the lifecycle
|
|
33
|
+
// keeps its journal and the predecessor's records, because a committed upgrade cannot be rolled back), the gate replays
|
|
34
|
+
// real requests over A2A and reads the outcome back from machine state, and only a pass commits, pins the template and
|
|
35
|
+
// prunes images. A failed gate runs `upgrade-rollback`, confirms the predecessor is healthy, and exits 1.
|
|
36
|
+
// --no-gate skip the gate and commit straight away (emergencies; logged loudly)
|
|
37
|
+
// --plant <case> make that gate case fail on purpose, proving the rollback end to end
|
|
38
|
+
// verify --gate run the gate against the live version without upgrading
|
|
39
|
+
//
|
|
40
|
+
// Usage: sanctuary-butler-upgrade.mjs <preflight|prepare|install|verify|upgrade> <version> [--rehearse <step>] [--no-gate] [--plant <case>]
|
|
41
|
+
// sanctuary-butler-upgrade.mjs verify [--gate]
|
|
33
42
|
|
|
34
|
-
import { execFileSync } from "node:child_process"
|
|
43
|
+
import { execFileSync, spawnSync } from "node:child_process"
|
|
35
44
|
import { createHash } from "node:crypto"
|
|
36
|
-
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync, statSync, chmodSync } from "node:fs"
|
|
45
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync, statSync, chmodSync, realpathSync } from "node:fs"
|
|
46
|
+
import { fileURLToPath } from "node:url"
|
|
37
47
|
import { tmpdir } from "node:os"
|
|
38
48
|
import { join } from "node:path"
|
|
39
49
|
|
|
@@ -61,6 +71,10 @@ const UPGRADE_JOURNAL = `${ROOT}/upgrade.json`
|
|
|
61
71
|
const MAINTENANCE = "/run/ouro-authority-maintenance"
|
|
62
72
|
// The stop hook's flag: every keeper watchdog version stands down while it exists.
|
|
63
73
|
const SHUTDOWN_FLAG = "/run/ouro-authority-shutdown"
|
|
74
|
+
const GATE_SCRIPT = `${ROOT}/package/deploy/unraid/sanctuary-replay-gate.mjs`
|
|
75
|
+
const GATE_PROVISION = `${BUNDLE}/state/replay-client/provision.json`
|
|
76
|
+
// The whole gate (every case, including the six-minute await wait) must finish well inside this; a hung gate is a failed gate.
|
|
77
|
+
export const GATE_TIMEOUT_MS = 45 * 60 * 1000
|
|
64
78
|
const UPGRADE_STEPS = ["stop", "switch", "resident", "migrate", "start"]
|
|
65
79
|
|
|
66
80
|
const sh = (file, args, opts = {}) => execFileSync(file, args, { encoding: "utf8", maxBuffer: 64 << 20, ...opts })
|
|
@@ -845,9 +859,86 @@ function lifecycleFailure(e) {
|
|
|
845
859
|
return `${detail || e.message}${last ? `\n last root failure: ${last}` : ""}`
|
|
846
860
|
}
|
|
847
861
|
|
|
848
|
-
|
|
862
|
+
/**
|
|
863
|
+
* The end of a gated upgrade, with every side effect injected so the decision is testable: a pass commits the held
|
|
864
|
+
* upgrade, pins the template and prunes; a fail rolls back to the predecessor and confirms it is healthy. Nothing is
|
|
865
|
+
* committed, pinned or pruned unless the gate passed.
|
|
866
|
+
*/
|
|
867
|
+
export function completeGatedUpgrade({ runGate, commit, rollback, confirmRollback, pin, prune, log = console.log }) {
|
|
868
|
+
const gate = runGate()
|
|
869
|
+
if (gate.ok) {
|
|
870
|
+
commit()
|
|
871
|
+
pin()
|
|
872
|
+
prune()
|
|
873
|
+
log("GATE PASS")
|
|
874
|
+
return { passed: true }
|
|
875
|
+
}
|
|
876
|
+
log(`gate failed: ${gate.detail ?? "see the case lines above"}`)
|
|
877
|
+
rollback()
|
|
878
|
+
const restored = confirmRollback()
|
|
879
|
+
log(`GATE FAIL — ROLLED BACK to ${restored}`)
|
|
880
|
+
return { passed: false, rolledBackTo: restored }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export function runReplayGate(plant, { spawn = spawnSync, exists = existsSync } = {}) {
|
|
884
|
+
if (!exists(GATE_SCRIPT)) return { ok: false, detail: `the replay gate is missing from the live package (${GATE_SCRIPT})` }
|
|
885
|
+
const result = spawn("/usr/local/bin/node", [GATE_SCRIPT, "run", ...(plant ? ["--plant", plant] : [])], { stdio: "inherit", timeout: GATE_TIMEOUT_MS, killSignal: "SIGKILL" })
|
|
886
|
+
return result.status === 0 ? { ok: true } : { ok: false, detail: `the replay gate exited ${result.status ?? result.signal}` }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The commit step runs the lifecycle's `upgrade` again, which would START A FRESH UNGATED UPGRADE if the keeper had
|
|
891
|
+
* rolled the held one back mid-gate. So commit only while the journal still exists and the new image is still live.
|
|
892
|
+
*/
|
|
893
|
+
export function heldUpgradeProblem({ journalExists, liveImage, expectedImage }) {
|
|
894
|
+
if (!journalExists) return "the held upgrade's journal is gone (the keeper or a boot rolled it back mid-gate)"
|
|
895
|
+
if (liveImage !== expectedImage) return `the live Butler is ${liveImage}, not the held ${expectedImage}`
|
|
896
|
+
return null
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function butlerState() {
|
|
900
|
+
try { return docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}}"], { stdio: ["ignore", "pipe", "ignore"] }).trim() } catch { return "? ?/?" }
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function waitForButler(imageRef, seconds) {
|
|
904
|
+
for (let waited = 0; waited <= seconds; waited += 10) {
|
|
905
|
+
const [liveImage, state] = butlerState().split(" ")
|
|
906
|
+
if (liveImage === imageRef && state === "running/healthy") return true
|
|
907
|
+
sh("/bin/sleep", ["10"])
|
|
908
|
+
}
|
|
909
|
+
return false
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// The new gateway asks Telegram who it is (getMe) before it reports ready. When the host cannot reach
|
|
913
|
+
// Telegram, that fails and the whole upgrade rolls back after host supervision was already paused
|
|
914
|
+
// (the 863 -> 864 upgrade). Any HTTP answer from the API host proves the network path; no token is sent.
|
|
915
|
+
export const TELEGRAM_API_URL = "https://api.telegram.org/"
|
|
916
|
+
export async function telegramApiReachable({ fetchImpl = globalThis.fetch, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), tries = 3, timeoutMs = 8000, retryDelayMs = 3000 } = {}) {
|
|
917
|
+
let detail = "no attempt made"
|
|
918
|
+
for (let attempt = 1; attempt <= tries; attempt += 1) {
|
|
919
|
+
try {
|
|
920
|
+
const response = await fetchImpl(TELEGRAM_API_URL, { method: "GET", signal: AbortSignal.timeout(timeoutMs) })
|
|
921
|
+
return { ok: true, detail: `HTTP ${response.status}` }
|
|
922
|
+
} catch (error) {
|
|
923
|
+
detail = error instanceof Error ? (error.cause instanceof Error ? `${error.message}: ${error.cause.message}` : error.message) : String(error)
|
|
924
|
+
if (attempt < tries) await sleep(retryDelayMs)
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return { ok: false, detail: `${detail} (${tries} attempts)` }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// A resume or rollback (an upgrade journal exists) must never be blocked by a network probe.
|
|
931
|
+
export async function checkTelegramBeforePause({ journalExists, probe = telegramApiReachable }) {
|
|
932
|
+
if (journalExists) return { skipped: true, ok: true, detail: "upgrade journal present" }
|
|
933
|
+
return { skipped: false, ...(await probe()) }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function upgrade(version, rehearse, { noGate = false, plant } = {}) {
|
|
849
937
|
say(`in-place upgrade to ${version}${rehearse ? ` — REHEARSAL: stop after "${rehearse}", then roll back` : ""}`)
|
|
850
938
|
if (rehearse && !UPGRADE_STEPS.includes(rehearse)) fail(`--rehearse takes one of: ${UPGRADE_STEPS.join(", ")}`)
|
|
939
|
+
const gated = !rehearse && !noGate
|
|
940
|
+
if (noGate && !rehearse) console.log("\n!! --no-gate: this upgrade will NOT be replay-gated or auto-rolled-back on a behavioural regression")
|
|
941
|
+
if (gated && !existsSync(GATE_PROVISION)) fail(`replay peers are not provisioned (${GATE_PROVISION}); run \`sanctuary-replay-gate.mjs provision\` first, or pass --no-gate`)
|
|
851
942
|
if (!existsSync(`${ROOT}/active.json`) || !existsSync(`${ROOT}/activation.json`)) fail("no installed authority; use prepare + install")
|
|
852
943
|
if (existsSync(JOURNAL)) fail("a template transaction is pending; resolve it first")
|
|
853
944
|
let id = imageId(version)
|
|
@@ -867,13 +958,18 @@ function upgrade(version, rehearse) {
|
|
|
867
958
|
ok("new package staged beside the live one and verified")
|
|
868
959
|
}
|
|
869
960
|
const lifecycle = `${ROOT}/incoming-package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js`
|
|
961
|
+
const priorImage = butlerState().split(" ")[0]
|
|
870
962
|
const jellyfinBefore = docker(["inspect", "jellyfin", "--format", "{{.Id}}|{{.Image}}|{{.RestartCount}}|{{.State.StartedAt}}"]).trim()
|
|
871
963
|
const policyBefore = existsSync(POLICY) ? sha12(POLICY) : fail("steward policy missing")
|
|
964
|
+
const telegram = await checkTelegramBeforePause({ journalExists: existsSync(UPGRADE_JOURNAL) })
|
|
965
|
+
if (!telegram.ok) fail(`api.telegram.org is not reachable from this host (${telegram.detail}); the new gateway could not pass its Telegram identity check. Nothing was paused or changed; fix the network and re-run.`)
|
|
966
|
+
if (telegram.skipped) console.log(" note: resuming a pending upgrade; Telegram reachability not re-checked")
|
|
967
|
+
else ok(`api.telegram.org reachable (${telegram.detail})`)
|
|
872
968
|
pauseSupervision()
|
|
873
969
|
let failure = null
|
|
874
970
|
try {
|
|
875
|
-
say(
|
|
876
|
-
ok(sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version), ...(rehearse ? ["--fail-after", rehearse] : [])], { stdio: ["ignore", "pipe", "pipe"] }).trim())
|
|
971
|
+
say(`lifecycle upgrade (stop → switch → resident → migrate → start${gated ? ", held uncommitted for the gate" : ""})`)
|
|
972
|
+
ok(sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version), ...(rehearse ? ["--fail-after", rehearse] : gated ? ["--hold-commit"] : [])], { stdio: ["ignore", "pipe", "pipe"] }).trim())
|
|
877
973
|
} catch (e) { failure = e }
|
|
878
974
|
// Only the lifecycle's own rehearsal stop is planned; anything else is a real failure,
|
|
879
975
|
// even during a rehearsal (the first 830 -> 835 rehearsal was mislabelled "as planned").
|
|
@@ -886,20 +982,58 @@ function upgrade(version, rehearse) {
|
|
|
886
982
|
resumeSupervision()
|
|
887
983
|
fail(`ROLLBACK FAILED: ${lifecycleFailure(e)}\nThe journal is kept; the next authority boot (or \`upgrade-rollback\`) retries it.`)
|
|
888
984
|
}
|
|
889
|
-
} else { pinTemplate(version); pruneButlerImages() }
|
|
985
|
+
} else if (!gated) { pinTemplate(version); pruneButlerImages() }
|
|
890
986
|
resumeSupervision()
|
|
891
987
|
say("preservation")
|
|
892
|
-
|
|
893
|
-
|
|
988
|
+
const preserved = []
|
|
989
|
+
const jellyfinNow = docker(["inspect", "jellyfin", "--format", "{{.Id}}|{{.Image}}|{{.RestartCount}}|{{.State.StartedAt}}"]).trim()
|
|
990
|
+
jellyfinNow === jellyfinBefore ? ok("jellyfin unchanged") : (gated && !failure ? (bad("JELLYFIN CHANGED"), preserved.push("jellyfin changed")) : fail("JELLYFIN CHANGED"))
|
|
991
|
+
sha12(POLICY) === policyBefore ? ok("steward policy unchanged") : (gated && !failure ? (bad("STEWARD POLICY CHANGED"), preserved.push("steward policy changed")) : fail("STEWARD POLICY CHANGED"))
|
|
894
992
|
const live = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}}"]).trim()
|
|
895
993
|
ok(`butler: ${live}`)
|
|
896
994
|
if (failure && !planned) fail("upgrade rolled back; the Butler is on its prior version (see above)")
|
|
897
|
-
|
|
995
|
+
if (gated && !failure) {
|
|
996
|
+
say(`replay gate${plant ? ` (planted failure: ${plant})` : ""}`)
|
|
997
|
+
const stillHeld = () => heldUpgradeProblem({ journalExists: existsSync(UPGRADE_JOURNAL), liveImage: butlerState().split(" ")[0], expectedImage: image(version) })
|
|
998
|
+
const outcome = completeGatedUpgrade({
|
|
999
|
+
runGate: () => {
|
|
1000
|
+
if (preserved.length) return { ok: false, detail: preserved.join("; ") }
|
|
1001
|
+
if (!waitForButler(image(version), 300)) return { ok: false, detail: `the new Butler did not become healthy on ${image(version)}` }
|
|
1002
|
+
const result = runReplayGate(plant)
|
|
1003
|
+
if (!result.ok) return result
|
|
1004
|
+
const problem = stillHeld()
|
|
1005
|
+
return problem ? { ok: false, detail: problem } : result
|
|
1006
|
+
},
|
|
1007
|
+
commit: () => {
|
|
1008
|
+
const problem = stillHeld()
|
|
1009
|
+
if (problem) throw new Error(`refusing to commit: ${problem}`)
|
|
1010
|
+
pauseSupervision()
|
|
1011
|
+
try { ok(`commit: ${sh("/usr/local/bin/node", [lifecycle, "upgrade", id, image(version)], { stdio: ["ignore", "pipe", "pipe"] }).trim()}`) }
|
|
1012
|
+
finally { resumeSupervision() }
|
|
1013
|
+
},
|
|
1014
|
+
rollback: () => {
|
|
1015
|
+
pauseSupervision()
|
|
1016
|
+
try { ok(`rollback: ${sh("/usr/local/bin/node", [lifecycle, "upgrade-rollback"], { stdio: ["ignore", "pipe", "pipe"] }).trim()}`) }
|
|
1017
|
+
catch (e) { fail(`ROLLBACK FAILED: ${lifecycleFailure(e)}\nThe journal is kept; the next authority boot (or \`upgrade-rollback\`) retries it.`) }
|
|
1018
|
+
finally { resumeSupervision() }
|
|
1019
|
+
},
|
|
1020
|
+
confirmRollback: () => {
|
|
1021
|
+
if (!waitForButler(priorImage, 300)) fail(`rolled back, but ${priorImage} is not running healthy; inspect \`docker logs ${CONTAINER}\``)
|
|
1022
|
+
return priorImage
|
|
1023
|
+
},
|
|
1024
|
+
pin: () => pinTemplate(version),
|
|
1025
|
+
prune: () => pruneButlerImages(),
|
|
1026
|
+
})
|
|
1027
|
+
if (!outcome.passed) process.exit(1)
|
|
1028
|
+
console.log(`\nUPGRADE done — ${version} live and gate-verified. Run verify.`)
|
|
1029
|
+
return
|
|
1030
|
+
}
|
|
1031
|
+
console.log(rehearse ? "\nREHEARSAL done — the upgrade reached the planned step and the rollback restored the predecessor." : `\nUPGRADE done — ${version} live (NOT gate-verified). Run verify.`)
|
|
898
1032
|
}
|
|
899
1033
|
|
|
900
1034
|
// ---- verify ---------------------------------------------------------------
|
|
901
1035
|
|
|
902
|
-
function verify() {
|
|
1036
|
+
function verify(withGate = false) {
|
|
903
1037
|
say("verify")
|
|
904
1038
|
const st = docker(["inspect", CONTAINER, "--format", "{{.Config.Image}} {{.State.Status}}/{{.State.Health.Status}} restarts={{.RestartCount}}"]).trim()
|
|
905
1039
|
ok(`butler: ${st}`)
|
|
@@ -908,6 +1042,11 @@ function verify() {
|
|
|
908
1042
|
existsSync(POLICY) ? ok(`steward policy ${sha12(POLICY)}`) : bad("steward policy missing")
|
|
909
1043
|
const jf = docker(["inspect", "jellyfin", "--format", "{{.State.Status}} restarts={{.RestartCount}}"]).trim()
|
|
910
1044
|
ok(`jellyfin ${jf}`)
|
|
1045
|
+
if (withGate) {
|
|
1046
|
+
say("replay gate (live version, no upgrade)")
|
|
1047
|
+
const gate = runReplayGate(undefined)
|
|
1048
|
+
gate.ok ? ok("replay gate passed") : bad(`replay gate failed: ${gate.detail}`)
|
|
1049
|
+
}
|
|
911
1050
|
process.exit(RED === 0 ? 0 : 1)
|
|
912
1051
|
}
|
|
913
1052
|
|
|
@@ -915,19 +1054,35 @@ function fail(m) { console.error(`\nREFUSING: ${m}`); process.exit(1) }
|
|
|
915
1054
|
|
|
916
1055
|
// ---- entry ----------------------------------------------------------------
|
|
917
1056
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1057
|
+
export function parseUpgradeArgs(argv) {
|
|
1058
|
+
const usage = "Usage: sanctuary-butler-upgrade.mjs <preflight|prepare|install|upgrade> <version> [--rehearse <step>] [--no-gate] [--plant <case>] | verify [--gate]"
|
|
1059
|
+
const [phase, ...rest] = argv
|
|
1060
|
+
if (!phase || !["preflight", "prepare", "install", "verify", "upgrade"].includes(phase)) throw new Error(usage)
|
|
1061
|
+
const parsed = { phase, version: undefined, rehearse: undefined, noGate: false, plant: undefined, gate: false }
|
|
1062
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1063
|
+
const arg = rest[i]
|
|
1064
|
+
if (arg === "--rehearse" && phase === "upgrade" && rest[i + 1]) parsed.rehearse = rest[++i]
|
|
1065
|
+
else if (arg === "--no-gate" && phase === "upgrade") parsed.noGate = true
|
|
1066
|
+
else if (arg === "--plant" && phase === "upgrade" && rest[i + 1]) parsed.plant = rest[++i]
|
|
1067
|
+
else if (arg === "--gate" && phase === "verify") parsed.gate = true
|
|
1068
|
+
else if (!arg.startsWith("--") && parsed.version === undefined && phase !== "verify") parsed.version = arg
|
|
1069
|
+
else throw new Error(usage)
|
|
1070
|
+
}
|
|
1071
|
+
if (parsed.noGate && parsed.plant) throw new Error("--plant needs the gate; it cannot be combined with --no-gate")
|
|
1072
|
+
if (parsed.rehearse && (parsed.noGate || parsed.plant)) throw new Error("--rehearse does not use the gate; do not combine it with --no-gate or --plant")
|
|
1073
|
+
if (phase !== "verify" && !/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(parsed.version || "")) throw new Error("a semver version is required, e.g. 0.1.0-alpha.829")
|
|
1074
|
+
return parsed
|
|
922
1075
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1076
|
+
|
|
1077
|
+
async function run(argv) {
|
|
1078
|
+
let args
|
|
1079
|
+
try { args = parseUpgradeArgs(argv) } catch (error) { console.error(error.message); process.exit(2) }
|
|
1080
|
+
requireRoot()
|
|
1081
|
+
if (args.phase === "preflight") preflight(args.version)
|
|
1082
|
+
else if (args.phase === "prepare") prepare(args.version)
|
|
1083
|
+
else if (args.phase === "install") install(args.version)
|
|
1084
|
+
else if (args.phase === "verify") verify(args.gate)
|
|
1085
|
+
else if (args.phase === "upgrade") await upgrade(args.version, args.rehearse, { noGate: args.noGate, plant: args.plant })
|
|
926
1086
|
}
|
|
927
|
-
requireRoot()
|
|
928
1087
|
|
|
929
|
-
if (
|
|
930
|
-
else if (phase === "prepare") prepare(version)
|
|
931
|
-
else if (phase === "install") install(version)
|
|
932
|
-
else if (phase === "verify") verify()
|
|
933
|
-
else if (phase === "upgrade") upgrade(version, rehearse)
|
|
1088
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) run(process.argv.slice(2)).catch((error) => { console.error(error); process.exit(1) })
|