zl-code 0.0.30 → 0.0.32

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.
Files changed (91) hide show
  1. package/dist/bin.mjs +6271 -5269
  2. package/dist/client/apple-touch-icon.png +0 -0
  3. package/dist/client/assets/{DiffPanel-DPeVDy87.js → DiffPanel-zCbsoszm.js} +1 -1
  4. package/dist/client/assets/{FilePreviewPanel-CoSSgo5p.js → FilePreviewPanel-GEuC3j76.js} +1 -1
  5. package/dist/client/assets/{PreviewPanel-y0Pign2e.js → PreviewPanel-C9dhIGyh.js} +1 -1
  6. package/dist/client/assets/{angular-html-C7-uPMFl.js → angular-html-BMjNownc.js} +1 -1
  7. package/dist/client/assets/{angular-ts-xNhvCrgW.js → angular-ts-B5LPf24i.js} +1 -1
  8. package/dist/client/assets/{apl-Cbzu1Nmg.js → apl-Iiv22-rq.js} +1 -1
  9. package/dist/client/assets/{astro-BcXGqCfF.js → astro-C7X5I9rp.js} +1 -1
  10. package/dist/client/assets/{blade-DXoD1tq3.js → blade-ClfIyMfn.js} +1 -1
  11. package/dist/client/assets/{c-ww9DT4H9.js → c-BQ8eShnN.js} +1 -1
  12. package/dist/client/assets/{cobol-Uf0pxp3c.js → cobol-DqNqjz_S.js} +1 -1
  13. package/dist/client/assets/{coffee-CNtx8SYp.js → coffee-Dfy3eYCK.js} +1 -1
  14. package/dist/client/assets/{cpp-CNtAvTB0.js → cpp-C5wPYC_c.js} +1 -1
  15. package/dist/client/assets/{crystal-B_GNKS-f.js → crystal-C_a9rG7e.js} +1 -1
  16. package/dist/client/assets/{css-EjGGwsxM.js → css-D4w5bo68.js} +1 -1
  17. package/dist/client/assets/{edge-FdFiwuuE.js → edge-DvZwYTRV.js} +1 -1
  18. package/dist/client/assets/{elixir-DDsnq8Pt.js → elixir-D-VlWr-X.js} +1 -1
  19. package/dist/client/assets/{elm-ZpE4zkBa.js → elm-DWj16Mpb.js} +1 -1
  20. package/dist/client/assets/{erb-B-Z8312X.js → erb-DlxOzz2-.js} +1 -1
  21. package/dist/client/assets/{fileCommentAnnotations-BBQH2v2a.js → fileCommentAnnotations-ClcpOWQa.js} +1 -1
  22. package/dist/client/assets/{git-rebase-CGEmx0LI.js → git-rebase-BBxaCC9q.js} +1 -1
  23. package/dist/client/assets/{glimmer-js-COOoKt3i.js → glimmer-js-DkqomO9t.js} +1 -1
  24. package/dist/client/assets/{glimmer-ts-D1F8OsDX.js → glimmer-ts-DqP3lMYF.js} +1 -1
  25. package/dist/client/assets/{glsl-DJH58HGE.js → glsl-CTIHfVFl.js} +1 -1
  26. package/dist/client/assets/{graphql-DiNe2tpe.js → graphql-D12g55j9.js} +1 -1
  27. package/dist/client/assets/{hack-Dekeh7e5.js → hack-DiEhbY4L.js} +1 -1
  28. package/dist/client/assets/{haml-BVSoeu2x.js → haml-k10ejFQT.js} +1 -1
  29. package/dist/client/assets/{handlebars-k1nk8eCR.js → handlebars-Bp3Y28q9.js} +1 -1
  30. package/dist/client/assets/{html-DL9dLLUO.js → html-CUWUgIeB.js} +1 -1
  31. package/dist/client/assets/{html-derivative-CUKIwoBz.js → html-derivative-DfyM2SPX.js} +1 -1
  32. package/dist/client/assets/{http-imAz0kdM.js → http-DPJUcqBH.js} +1 -1
  33. package/dist/client/assets/{hurl-DmtXySiF.js → hurl-DEgmYbvA.js} +1 -1
  34. package/dist/client/assets/index-CvcF6-DK.js +577 -0
  35. package/dist/client/assets/{java-C_iicb65.js → java-C8F05rcf.js} +1 -1
  36. package/dist/client/assets/{javascript-BB2uj7le.js → javascript-DouaYy_g.js} +1 -1
  37. package/dist/client/assets/{jinja-CN975xAC.js → jinja-Be9nMfz4.js} +1 -1
  38. package/dist/client/assets/{jison-DgcPoUOY.js → jison-DudveEHD.js} +1 -1
  39. package/dist/client/assets/{json-Ddi2d9VD.js → json-Cs3qd19t.js} +1 -1
  40. package/dist/client/assets/{jsx-DYU0gI8H.js → jsx-5HPw0X3q.js} +1 -1
  41. package/dist/client/assets/{julia-DhqSTpmP.js → julia-BoQ-G9_W.js} +1 -1
  42. package/dist/client/assets/{just-h3se1pga.js → just-Cbl3z7sB.js} +1 -1
  43. package/dist/client/assets/{latex-BS-rXA6Q.js → latex-CTaz-xa9.js} +1 -1
  44. package/dist/client/assets/{liquid-DHdObD9D.js → liquid-nTT4KJIV.js} +1 -1
  45. package/dist/client/assets/{lua-BGB4Nt27.js → lua-Dvbu0Xf8.js} +1 -1
  46. package/dist/client/assets/{marko-Bx2nm-_y.js → marko-HSTFVa6O.js} +1 -1
  47. package/dist/client/assets/{mdc-1G1Pxvit.js → mdc-hyhTtO2H.js} +1 -1
  48. package/dist/client/assets/{nginx-BmU1aBWa.js → nginx-xeSRWU27.js} +1 -1
  49. package/dist/client/assets/{nim-45VoVczs.js → nim-D8r6vzf8.js} +1 -1
  50. package/dist/client/assets/{perl-DGeP60Eh.js → perl-CJlDsa7P.js} +1 -1
  51. package/dist/client/assets/{php-BOvK9gQB.js → php-akslWydZ.js} +1 -1
  52. package/dist/client/assets/{pug-C3v1lq7N.js → pug-Cc2NlxNM.js} +1 -1
  53. package/dist/client/assets/{qml-D4WEnAWk.js → qml-CSw3No2z.js} +1 -1
  54. package/dist/client/assets/{r-chkD_kWf.js → r-pY2PXIBb.js} +1 -1
  55. package/dist/client/assets/{razor-D5zg9w6m.js → razor-0_ry7Rob.js} +1 -1
  56. package/dist/client/assets/{regexp-Br_6RjDB.js → regexp-2vomjM_W.js} +1 -1
  57. package/dist/client/assets/{rst-BozCtIg9.js → rst-C-cZW4l2.js} +1 -1
  58. package/dist/client/assets/{ruby-CGjgsblc.js → ruby-BxaEq5Zq.js} +1 -1
  59. package/dist/client/assets/{sas-CVxWda8Q.js → sas-96FB6PFP.js} +1 -1
  60. package/dist/client/assets/{scss-DeFuqIK_.js → scss-Ct-gnIxF.js} +1 -1
  61. package/dist/client/assets/{shellscript-C6Q7qkUD.js → shellscript-ypyzeT3L.js} +1 -1
  62. package/dist/client/assets/{shellsession-WLo6A5mg.js → shellsession-DgirLgWe.js} +1 -1
  63. package/dist/client/assets/{soy-BcVC4qYp.js → soy-BVOIU10T.js} +1 -1
  64. package/dist/client/assets/{sql-ZjNem-IK.js → sql-GXzok57n.js} +1 -1
  65. package/dist/client/assets/{stata-BZ73C6JZ.js → stata-H-DFe69L.js} +1 -1
  66. package/dist/client/assets/{surrealql-CR43JrBJ.js → surrealql-3Y5zorm-.js} +1 -1
  67. package/dist/client/assets/{svelte-BpXqMiBr.js → svelte-CuLFybtz.js} +1 -1
  68. package/dist/client/assets/{templ-DSw5UMTq.js → templ-C-ht3RD5.js} +1 -1
  69. package/dist/client/assets/{tex-DaHKjnWo.js → tex-B3tZ6fPP.js} +1 -1
  70. package/dist/client/assets/textarea-CVmVd8xn.js +44 -0
  71. package/dist/client/assets/{ts-tags-SZ8IZlNT.js → ts-tags-Dn_kS980.js} +1 -1
  72. package/dist/client/assets/{tsx-CwwA7B6A.js → tsx-CYfxZJqp.js} +1 -1
  73. package/dist/client/assets/{twig-B_ebiDih.js → twig-CE2h-NqH.js} +1 -1
  74. package/dist/client/assets/{typescript-DO3j7XZx.js → typescript-Bufm6oeP.js} +1 -1
  75. package/dist/client/assets/{vue-B8Sj9crw.js → vue-BpYFSTwh.js} +1 -1
  76. package/dist/client/assets/{vue-html-DOid4MmJ.js → vue-html-C4Ma9CEV.js} +1 -1
  77. package/dist/client/assets/{vue-vine-CZ-OLWJd.js → vue-vine-C5Uds-em.js} +1 -1
  78. package/dist/client/assets/{xml-CQ7Xk9af.js → xml-D2xJwq9a.js} +1 -1
  79. package/dist/client/assets/{xsl-CNh6rw4f.js → xsl-DrXThzkt.js} +1 -1
  80. package/dist/client/assets/{yaml-BkM14TZe.js → yaml-Dnr4uuaT.js} +1 -1
  81. package/dist/client/favicon-16x16.png +0 -0
  82. package/dist/client/favicon-32x32.png +0 -0
  83. package/dist/client/favicon.ico +0 -0
  84. package/dist/client/index.html +5 -5
  85. package/dist/client/zettai-labs-z-mark.svg +1 -0
  86. package/dist/npm-service-runtime.sh +808 -0
  87. package/dist/zl-code-server-npm.service +29 -0
  88. package/dist/zl-code-update-supervisor.service +20 -0
  89. package/package.json +1 -1
  90. package/dist/client/assets/index-CxcYRXaB.js +0 -577
  91. package/dist/client/assets/textarea-nMqSOy-S.js +0 -44
@@ -0,0 +1,808 @@
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
5
+ SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
6
+ REPO_DIR="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
7
+
8
+ RUNTIME_ROOT="${ZL_CODE_NPM_SERVICE_ROOT:-${HOME}/.local/share/zl-code}"
9
+ PRODUCTION_HOME="${ZL_CODE_NPM_SERVICE_HOME:-${HOME}/.zettai-labs-code}"
10
+ OPERATOR_STATE="${ZL_CODE_NPM_SERVICE_STATE_DIR:-${HOME}/.local/state/zl-code/npm-service}"
11
+ UNIT_PATH="${ZL_CODE_NPM_SERVICE_UNIT_PATH:-${HOME}/.config/systemd/user/zl-code-server.service}"
12
+ DEFAULT_UNIT_TEMPLATE="${REPO_DIR}/infra/systemd/zl-code-server-npm.service"
13
+ [[ -f "${SCRIPT_DIR}/zl-code-server-npm.service" ]] && DEFAULT_UNIT_TEMPLATE="${SCRIPT_DIR}/zl-code-server-npm.service"
14
+ UNIT_TEMPLATE="${ZL_CODE_NPM_SERVICE_UNIT_TEMPLATE:-${DEFAULT_UNIT_TEMPLATE}}"
15
+ SUPERVISOR_UNIT_PATH="${ZL_CODE_UPDATE_SUPERVISOR_UNIT_PATH:-${HOME}/.config/systemd/user/zl-code-update-supervisor.service}"
16
+ DEFAULT_SUPERVISOR_UNIT_TEMPLATE="${REPO_DIR}/infra/systemd/zl-code-update-supervisor.service"
17
+ [[ -f "${SCRIPT_DIR}/zl-code-update-supervisor.service" ]] && DEFAULT_SUPERVISOR_UNIT_TEMPLATE="${SCRIPT_DIR}/zl-code-update-supervisor.service"
18
+ SUPERVISOR_UNIT_TEMPLATE="${ZL_CODE_UPDATE_SUPERVISOR_UNIT_TEMPLATE:-${DEFAULT_SUPERVISOR_UNIT_TEMPLATE}}"
19
+ SUPERVISOR_SERVICE_NAME="${ZL_CODE_UPDATE_SUPERVISOR_SERVICE_NAME:-zl-code-update-supervisor.service}"
20
+ NODE_BIN="${ZL_CODE_NPM_SERVICE_NODE_BIN:-${HOME}/.local/share/mise/installs/node/24.19.0/bin/node}"
21
+ NPM_BIN="${ZL_CODE_NPM_SERVICE_NPM_BIN:-${HOME}/.local/share/mise/installs/node/24.19.0/bin/npm}"
22
+ SERVICE_NAME="${ZL_CODE_NPM_SERVICE_NAME:-zl-code-server.service}"
23
+ LOCAL_URL="${ZL_CODE_NPM_SERVICE_LOCAL_URL:-http://127.0.0.1:14773/}"
24
+ PUBLIC_URL="${ZL_CODE_NPM_SERVICE_PUBLIC_URL:-https://code.zettailabs.com/}"
25
+ RESTART_STATE_FILE="${ZL_CODE_NPM_SERVICE_RESTART_STATE_FILE:-${PRODUCTION_HOME}/userdata/server-restart.json}"
26
+ DRAIN_SIGNAL="${ZL_CODE_NPM_SERVICE_DRAIN_SIGNAL:-SIGUSR1}"
27
+ DRAIN_ACK_TIMEOUT_SECONDS="${ZL_CODE_NPM_SERVICE_DRAIN_ACK_TIMEOUT_SECONDS:-10}"
28
+ DRAIN_QUIESCE_TIMEOUT_SECONDS="${ZL_CODE_NPM_SERVICE_DRAIN_QUIESCE_TIMEOUT_SECONDS:-10}"
29
+ LOCK_FILE="${OPERATOR_STATE}/operation.lock"
30
+ ROLLBACK_DIR="${OPERATOR_STATE}/rollback"
31
+ TRANSACTION_DIR="${OPERATOR_STATE}/transaction"
32
+ LOCK_HELD=false
33
+ SUPERVISED_MODE=false
34
+ ACTIVATION_BOUNDARY_CAPTURED=false
35
+ ACTIVATION_CUTOVER_STARTED=false
36
+ ACTIVATION_ROLLBACK_COMPLETED=false
37
+ SUPERVISED_ROLLBACK_VERSION=""
38
+
39
+ usage() {
40
+ cat <<'EOF'
41
+ Manage the versioned npm runtime for the local Zettai Labs Code service.
42
+
43
+ Usage:
44
+ scripts/npm-service-runtime.sh status
45
+ scripts/npm-service-runtime.sh prepare <version> --no-cutover
46
+ scripts/npm-service-runtime.sh backup
47
+ scripts/npm-service-runtime.sh activate <version> --backup-dir <path> --confirm-cutover
48
+ scripts/npm-service-runtime.sh verify <version>
49
+ scripts/npm-service-runtime.sh rollback --to previous|git --confirm-cutover
50
+ scripts/npm-service-runtime.sh supervised-update <version> --confirm-supervised
51
+ scripts/npm-service-runtime.sh install-supervisor --confirm-install
52
+
53
+ `prepare` and `backup` never reload or restart systemd. `activate` and
54
+ `rollback` refuse to run without the literal --confirm-cutover flag.
55
+ EOF
56
+ }
57
+
58
+ fail() {
59
+ echo "error: $*" >&2
60
+ exit 1
61
+ }
62
+
63
+ log() {
64
+ printf '\n==> %s\n' "$*"
65
+ }
66
+
67
+ require_command() {
68
+ command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
69
+ }
70
+
71
+ require_file() {
72
+ [[ -f "$1" ]] || fail "required file not found: $1"
73
+ }
74
+
75
+ require_executable() {
76
+ [[ -x "$1" ]] || fail "required executable not found: $1"
77
+ }
78
+
79
+ acquire_lock() {
80
+ [[ "$LOCK_HELD" == true ]] && return
81
+ mkdir -p "$OPERATOR_STATE"
82
+ chmod 700 "$OPERATOR_STATE"
83
+ exec 9>"$LOCK_FILE"
84
+ flock -n 9 || fail "another npm service operation is running"
85
+ LOCK_HELD=true
86
+ }
87
+
88
+ emit_supervisor_phase() {
89
+ [[ "$SUPERVISED_MODE" == true ]] || return 0
90
+ echo "supervisor_phase=$1"
91
+ }
92
+
93
+ validate_version() {
94
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]] ||
95
+ fail "invalid exact npm version: $1"
96
+ }
97
+
98
+ validate_operation_id() {
99
+ [[ "$1" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]] ||
100
+ fail "invalid supervised update operation id"
101
+ }
102
+
103
+ release_dir() {
104
+ printf '%s/releases/%s\n' "$RUNTIME_ROOT" "$1"
105
+ }
106
+
107
+ package_entry() {
108
+ printf '%s/node_modules/zl-code/dist/bin.mjs\n' "$(release_dir "$1")"
109
+ }
110
+
111
+ atomic_symlink() {
112
+ local target=$1
113
+ local link_path=$2
114
+ local temporary="${link_path}.tmp.$$"
115
+ mkdir -p "$(dirname "$link_path")"
116
+ rm -f "$temporary"
117
+ ln -s "$target" "$temporary"
118
+ mv -Tf "$temporary" "$link_path"
119
+ }
120
+
121
+ read_current_release_identity() {
122
+ local package_path="${RUNTIME_ROOT}/current/node_modules/zl-code/package.json"
123
+ if [[ ! -f "$package_path" ]]; then
124
+ printf 'release:absent\n'
125
+ return
126
+ fi
127
+ printf 'release:%s\n' "$($NODE_BIN -e 'const p=require(process.argv[1]); process.stdout.write(String(p.version ?? ""))' "$package_path")"
128
+ }
129
+
130
+ write_restart_state() {
131
+ local restart_id=$1
132
+ local initiator=$2
133
+ local current_sha=$3
134
+ local target_sha=$4
135
+ local requested_at=$5
136
+ mkdir -p "$(dirname "$RESTART_STATE_FILE")"
137
+ "$NODE_BIN" - "$RESTART_STATE_FILE" "$restart_id" "$initiator" "$current_sha" "$target_sha" "$requested_at" <<'NODE'
138
+ const fs = require("node:fs");
139
+ const [file, restartId, initiator, currentSha, targetSha, requestedAt] = process.argv.slice(2);
140
+ const state = {
141
+ version: 1,
142
+ reason: "update",
143
+ restartId,
144
+ initiator,
145
+ currentSha,
146
+ targetSha,
147
+ requestedAt,
148
+ };
149
+ fs.writeFileSync(file, `${JSON.stringify(state)}\n`);
150
+ NODE
151
+ }
152
+
153
+ patch_restart_state() {
154
+ local patch=$1
155
+ "$NODE_BIN" - "$RESTART_STATE_FILE" "$patch" <<'NODE'
156
+ const fs = require("node:fs");
157
+ const [file, rawPatch] = process.argv.slice(2);
158
+ const patch = JSON.parse(rawPatch);
159
+ const current = JSON.parse(fs.readFileSync(file, "utf8"));
160
+ fs.writeFileSync(file, `${JSON.stringify({ ...current, ...patch })}\n`);
161
+ NODE
162
+ }
163
+
164
+ read_restart_state_field() {
165
+ local field=$1
166
+ "$NODE_BIN" - "$RESTART_STATE_FILE" "$field" <<'NODE'
167
+ const fs = require("node:fs");
168
+ const [file, field] = process.argv.slice(2);
169
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
170
+ const value = state[field];
171
+ if (value === undefined || value === null) {
172
+ process.exit(1);
173
+ }
174
+ process.stdout.write(String(value));
175
+ NODE
176
+ }
177
+
178
+ await_restart_state_field() {
179
+ local field=$1
180
+ local timeout_seconds=$2
181
+ local attempt=1
182
+ while [[ "$attempt" -le "$timeout_seconds" ]]; do
183
+ if read_restart_state_field "$field" >/dev/null; then
184
+ return 0
185
+ fi
186
+ sleep 1
187
+ attempt=$((attempt + 1))
188
+ done
189
+ return 1
190
+ }
191
+
192
+ verify_release() {
193
+ local version=$1
194
+ local directory
195
+ local entry
196
+ local reported
197
+ directory="$(release_dir "$version")"
198
+ entry="$(package_entry "$version")"
199
+ require_file "$directory/node_modules/zl-code/package.json"
200
+ require_file "$entry"
201
+ reported="$($NODE_BIN -e 'const p=require(process.argv[1]); process.stdout.write(p.name+"@"+p.version)' "$directory/node_modules/zl-code/package.json")"
202
+ [[ "$reported" == "zl-code@${version}" ]] || fail "release identity mismatch: $reported"
203
+ reported="$($NODE_BIN "$entry" --version)"
204
+ [[ "$reported" == "zl-code v${version}" ]] || fail "CLI identity mismatch: $reported"
205
+ }
206
+
207
+ prepare_release() {
208
+ local version=$1
209
+ local no_cutover=$2
210
+ local directory
211
+ local temporary
212
+ [[ "$no_cutover" == true ]] || fail "prepare requires --no-cutover"
213
+ validate_version "$version"
214
+ acquire_lock
215
+ require_executable "$NODE_BIN"
216
+ require_executable "$NPM_BIN"
217
+ require_command mv
218
+ export PATH="$(dirname "$NODE_BIN"):${PATH}"
219
+
220
+ directory="$(release_dir "$version")"
221
+ mkdir -p "$RUNTIME_ROOT/releases"
222
+ chmod 700 "$RUNTIME_ROOT" "$RUNTIME_ROOT/releases"
223
+
224
+ if [[ ! -d "$directory" ]]; then
225
+ temporary="${RUNTIME_ROOT}/releases/.${version}.prepare.$$"
226
+ [[ ! -e "$temporary" ]] || fail "temporary release path already exists: $temporary"
227
+ mkdir -p "$temporary"
228
+ chmod 700 "$temporary"
229
+ log "Installing exact zl-code@${version} without changing production"
230
+ "$NPM_BIN" install --prefix "$temporary" --omit=dev --no-audit --no-fund \
231
+ --package-lock=false "zl-code@${version}"
232
+ mv "$temporary" "$directory"
233
+ fi
234
+
235
+ verify_release "$version"
236
+ if [[ "${ZL_CODE_NPM_SERVICE_SKIP_SIGNATURE_AUDIT:-false}" != true ]]; then
237
+ log "Verifying npm registry signatures and attestations"
238
+ "$NPM_BIN" audit signatures --prefix "$directory"
239
+ fi
240
+ if [[ "${ZL_CODE_NPM_SERVICE_SKIP_PTY_SMOKE:-false}" != true ]]; then
241
+ log "Verifying the installed native PTY"
242
+ "$NODE_BIN" --input-type=module - "$directory" <<'NODE'
243
+ import { createRequire } from "node:module";
244
+ const require = createRequire(import.meta.url);
245
+ const root = process.argv[2];
246
+ const pty = require(`${root}/node_modules/node-pty`);
247
+ await new Promise((resolve, reject) => {
248
+ const child = pty.spawn("/bin/sh", ["-lc", "printf zl-code-pty-ok"], {
249
+ cols: 80,
250
+ rows: 24,
251
+ cwd: root,
252
+ env: process.env,
253
+ });
254
+ let output = "";
255
+ child.onData((chunk) => {
256
+ output += chunk;
257
+ });
258
+ child.onExit(({ exitCode }) => {
259
+ if (exitCode === 0 && output.includes("zl-code-pty-ok")) resolve();
260
+ else reject(new Error(`PTY smoke failed with exit ${exitCode}: ${output}`));
261
+ });
262
+ });
263
+ NODE
264
+ fi
265
+
266
+ atomic_symlink "$directory" "$RUNTIME_ROOT/candidate"
267
+ echo "candidate=$(readlink -f "$RUNTIME_ROOT/candidate")"
268
+ if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
269
+ echo "current=$(readlink -f "$RUNTIME_ROOT/current")"
270
+ else
271
+ echo "current=absent"
272
+ fi
273
+ echo "cutover=not-performed"
274
+ }
275
+
276
+ sqlite_quick_check() {
277
+ local database_path=$1
278
+ "$NODE_BIN" - "$database_path" <<'NODE'
279
+ const { DatabaseSync } = require("node:sqlite");
280
+ const database = new DatabaseSync(process.argv[2], { readOnly: true });
281
+ const rows = database.prepare("PRAGMA quick_check").all();
282
+ database.close();
283
+ if (rows.length !== 1 || rows[0].quick_check !== "ok") {
284
+ console.error(JSON.stringify(rows));
285
+ process.exit(1);
286
+ }
287
+ console.log("quick_check=ok");
288
+ NODE
289
+ }
290
+
291
+ create_backup() {
292
+ local source_database="${PRODUCTION_HOME}/userdata/state.sqlite"
293
+ local timestamp
294
+ local destination
295
+ local boundary_archive
296
+ acquire_lock
297
+ require_executable "$NODE_BIN"
298
+ require_file "$source_database"
299
+ require_file "$PRODUCTION_HOME/userdata/environment-id"
300
+ require_file "$PRODUCTION_HOME/userdata/server-runtime.json"
301
+ require_command sha256sum
302
+ require_command tar
303
+
304
+ timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
305
+ destination="${OPERATOR_STATE}/backups/${timestamp}"
306
+ boundary_archive="${destination}/runtime-boundary.tar"
307
+ mkdir -p "$destination"
308
+ chmod 700 "$destination"
309
+
310
+ log "Creating an online SQLite backup"
311
+ "$NODE_BIN" --input-type=module - "$source_database" "$destination/state.sqlite" <<'NODE'
312
+ import { DatabaseSync, backup } from "node:sqlite";
313
+ const source = new DatabaseSync(process.argv[2], { readOnly: true });
314
+ try {
315
+ await backup(source, process.argv[3]);
316
+ } finally {
317
+ source.close();
318
+ }
319
+ NODE
320
+ sqlite_quick_check "$destination/state.sqlite"
321
+
322
+ log "Archiving the bounded configuration and identity boundary"
323
+ tar --create --file "$boundary_archive" --directory "$PRODUCTION_HOME" \
324
+ userdata/config userdata/secrets userdata/environment-id userdata/server-runtime.json
325
+ chmod 600 "$destination/state.sqlite" "$boundary_archive"
326
+ if [[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]]; then
327
+ cp -L "$UNIT_PATH" "$destination/zl-code-server.service.before-cutover"
328
+ chmod 600 "$destination/zl-code-server.service.before-cutover"
329
+ fi
330
+ sha256sum "$destination/state.sqlite" "$boundary_archive" >"$destination/SHA256SUMS"
331
+ chmod 600 "$destination/SHA256SUMS"
332
+ {
333
+ echo "created_at=$timestamp"
334
+ echo "source_home=$PRODUCTION_HOME"
335
+ echo "sqlite_backup=state.sqlite"
336
+ echo "boundary_archive=runtime-boundary.tar"
337
+ echo "quick_check=ok"
338
+ } >"$destination/manifest"
339
+ chmod 600 "$destination/manifest"
340
+ echo "backup_dir=$destination"
341
+ }
342
+
343
+ save_git_rollback_unit() {
344
+ mkdir -p "$ROLLBACK_DIR/git"
345
+ chmod 700 "$ROLLBACK_DIR" "$ROLLBACK_DIR/git"
346
+ if [[ ! -f "$ROLLBACK_DIR/git/zl-code-server.service" ]]; then
347
+ [[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]] || fail "current service unit is missing: $UNIT_PATH"
348
+ cp -L "$UNIT_PATH" "$ROLLBACK_DIR/git/zl-code-server.service"
349
+ chmod 600 "$ROLLBACK_DIR/git/zl-code-server.service"
350
+ if [[ -L "$UNIT_PATH" ]]; then
351
+ readlink "$UNIT_PATH" >"$ROLLBACK_DIR/git/symlink-target"
352
+ fi
353
+ fi
354
+ }
355
+
356
+ capture_current_pointer() {
357
+ rm -f "$ROLLBACK_DIR/pre-activation-current-target" "$ROLLBACK_DIR/pre-activation-current-absent"
358
+ if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
359
+ readlink -f "$RUNTIME_ROOT/current" >"$ROLLBACK_DIR/pre-activation-current-target"
360
+ else
361
+ : >"$ROLLBACK_DIR/pre-activation-current-absent"
362
+ fi
363
+ }
364
+
365
+ capture_activation_boundary() {
366
+ rm -rf "$TRANSACTION_DIR"
367
+ mkdir -p "$TRANSACTION_DIR"
368
+ chmod 700 "$TRANSACTION_DIR"
369
+ [[ -e "$UNIT_PATH" || -L "$UNIT_PATH" ]] || fail "current service unit is missing: $UNIT_PATH"
370
+ cp -L "$UNIT_PATH" "$TRANSACTION_DIR/zl-code-server.service"
371
+ chmod 600 "$TRANSACTION_DIR/zl-code-server.service"
372
+ if [[ -L "$UNIT_PATH" ]]; then
373
+ readlink "$UNIT_PATH" >"$TRANSACTION_DIR/unit-symlink-target"
374
+ fi
375
+ if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
376
+ readlink -f "$RUNTIME_ROOT/current" >"$TRANSACTION_DIR/current-target"
377
+ else
378
+ : >"$TRANSACTION_DIR/current-absent"
379
+ fi
380
+ ACTIVATION_BOUNDARY_CAPTURED=true
381
+ }
382
+
383
+ install_npm_unit() {
384
+ local temporary="${UNIT_PATH}.tmp.$$"
385
+ require_file "$UNIT_TEMPLATE"
386
+ mkdir -p "$(dirname "$UNIT_PATH")"
387
+ install -m 600 "$UNIT_TEMPLATE" "$temporary"
388
+ mv -Tf "$temporary" "$UNIT_PATH"
389
+ }
390
+
391
+ restore_git_unit_and_pointer() {
392
+ local temporary="${UNIT_PATH}.rollback.$$"
393
+ require_file "$ROLLBACK_DIR/git/zl-code-server.service"
394
+ if [[ -f "$ROLLBACK_DIR/git/symlink-target" ]]; then
395
+ rm -f "$temporary"
396
+ ln -s "$(cat "$ROLLBACK_DIR/git/symlink-target")" "$temporary"
397
+ else
398
+ install -m 600 "$ROLLBACK_DIR/git/zl-code-server.service" "$temporary"
399
+ fi
400
+ mv -Tf "$temporary" "$UNIT_PATH"
401
+
402
+ if [[ -f "$ROLLBACK_DIR/pre-activation-current-target" ]]; then
403
+ atomic_symlink "$(cat "$ROLLBACK_DIR/pre-activation-current-target")" "$RUNTIME_ROOT/current"
404
+ elif [[ -f "$ROLLBACK_DIR/pre-activation-current-absent" ]]; then
405
+ rm -f "$RUNTIME_ROOT/current"
406
+ fi
407
+ }
408
+
409
+ restore_activation_boundary() {
410
+ local temporary="${UNIT_PATH}.rollback.$$"
411
+ require_file "$TRANSACTION_DIR/zl-code-server.service"
412
+ if [[ -f "$TRANSACTION_DIR/unit-symlink-target" ]]; then
413
+ rm -f "$temporary"
414
+ ln -s "$(cat "$TRANSACTION_DIR/unit-symlink-target")" "$temporary"
415
+ else
416
+ install -m 600 "$TRANSACTION_DIR/zl-code-server.service" "$temporary"
417
+ fi
418
+ mv -Tf "$temporary" "$UNIT_PATH"
419
+
420
+ if [[ -f "$TRANSACTION_DIR/current-target" ]]; then
421
+ atomic_symlink "$(cat "$TRANSACTION_DIR/current-target")" "$RUNTIME_ROOT/current"
422
+ elif [[ -f "$TRANSACTION_DIR/current-absent" ]]; then
423
+ rm -f "$RUNTIME_ROOT/current"
424
+ fi
425
+ }
426
+
427
+ supervised_exit_cleanup() {
428
+ local status=$?
429
+ trap - EXIT TERM INT
430
+ if [[ "$status" -ne 0 && "$ACTIVATION_CUTOVER_STARTED" == true && "$ACTIVATION_ROLLBACK_COMPLETED" != true ]]; then
431
+ emit_supervisor_phase "rolling_back"
432
+ if [[ "$ACTIVATION_BOUNDARY_CAPTURED" == true ]]; then
433
+ if restore_activation_boundary &&
434
+ systemctl --user daemon-reload &&
435
+ systemctl --user restart "$SERVICE_NAME" &&
436
+ systemctl --user is-active --quiet "$SERVICE_NAME" &&
437
+ wait_for_url "$LOCAL_URL"; then
438
+ ACTIVATION_ROLLBACK_COMPLETED=true
439
+ if [[ -n "$SUPERVISED_ROLLBACK_VERSION" ]]; then
440
+ echo "rollback_version=$SUPERVISED_ROLLBACK_VERSION"
441
+ fi
442
+ else
443
+ echo "error: cleanup could not verify the preceding service runtime" >&2
444
+ fi
445
+ fi
446
+ fi
447
+ exit "$status"
448
+ }
449
+
450
+ wait_for_url() {
451
+ local url=$1
452
+ local attempts=${ZL_CODE_NPM_SERVICE_HEALTH_ATTEMPTS:-30}
453
+ local attempt
454
+ for ((attempt = 1; attempt <= attempts; attempt += 1)); do
455
+ if curl --fail --silent --show-error --max-time 3 --output /dev/null "$url"; then
456
+ echo "health=ok attempt=$attempt url=$url"
457
+ return 0
458
+ fi
459
+ sleep 1
460
+ done
461
+ return 1
462
+ }
463
+
464
+ verify_runtime() {
465
+ local version=$1
466
+ local current_target
467
+ local main_pid
468
+ local command_line
469
+ if ! (validate_version "$version"); then
470
+ return 1
471
+ fi
472
+ if ! (verify_release "$version"); then
473
+ return 1
474
+ fi
475
+ current_target="$(readlink -f "$RUNTIME_ROOT/current")"
476
+ [[ "$current_target" == "$(release_dir "$version")" ]] ||
477
+ {
478
+ echo "error: current pointer does not select $(release_dir "$version"): $current_target" >&2
479
+ return 1
480
+ }
481
+ systemctl --user is-active --quiet "$SERVICE_NAME" || {
482
+ echo "error: $SERVICE_NAME is not active" >&2
483
+ return 1
484
+ }
485
+
486
+ if [[ "${ZL_CODE_NPM_SERVICE_SKIP_PROCESS_CHECK:-false}" != true ]]; then
487
+ main_pid="$(systemctl --user show "$SERVICE_NAME" -p MainPID --value)"
488
+ [[ "$main_pid" =~ ^[1-9][0-9]*$ ]] || {
489
+ echo "error: service has no live MainPID" >&2
490
+ return 1
491
+ }
492
+ command_line="$(tr '\0' ' ' <"/proc/${main_pid}/cmdline")"
493
+ [[ "$command_line" == *"$NODE_BIN"* ]] || {
494
+ echo "error: service is not using pinned Node: $command_line" >&2
495
+ return 1
496
+ }
497
+ [[ "$command_line" == *"${RUNTIME_ROOT}/current/node_modules/zl-code/dist/bin.mjs"* ]] ||
498
+ {
499
+ echo "error: service is not using the current npm runtime: $command_line" >&2
500
+ return 1
501
+ }
502
+ fi
503
+
504
+ wait_for_url "$LOCAL_URL" || {
505
+ echo "error: local endpoint did not become healthy: $LOCAL_URL" >&2
506
+ return 1
507
+ }
508
+ wait_for_url "$PUBLIC_URL" || {
509
+ echo "error: public endpoint did not become healthy: $PUBLIC_URL" >&2
510
+ return 1
511
+ }
512
+ echo "version=zl-code v${version}"
513
+ echo "current=$current_target"
514
+ echo "service=active"
515
+ }
516
+
517
+ activate_release() {
518
+ local version=$1
519
+ local backup_dir=$2
520
+ local confirmed=$3
521
+ local directory
522
+ local restart_id
523
+ local restart_requested_at
524
+ local current_release_identity
525
+ local current_version
526
+ local target_release_identity
527
+ local activation_status
528
+ [[ "$confirmed" == true ]] || fail "activation requires --confirm-cutover"
529
+ validate_version "$version"
530
+ [[ -n "$backup_dir" ]] || fail "activation requires --backup-dir"
531
+ require_file "$backup_dir/manifest"
532
+ require_file "$backup_dir/state.sqlite"
533
+ require_file "$backup_dir/runtime-boundary.tar"
534
+ grep -Fxq "quick_check=ok" "$backup_dir/manifest" || fail "backup was not quick_check verified"
535
+ acquire_lock
536
+ require_command curl
537
+ require_command install
538
+ require_command systemctl
539
+ require_executable "$NODE_BIN"
540
+ verify_release "$version"
541
+ sqlite_quick_check "$backup_dir/state.sqlite"
542
+ directory="$(release_dir "$version")"
543
+ [[ "$(readlink -f "$RUNTIME_ROOT/candidate")" == "$directory" ]] ||
544
+ fail "candidate pointer does not select $directory"
545
+ current_release_identity="$(read_current_release_identity)"
546
+ current_version="${current_release_identity#release:}"
547
+ SUPERVISED_ROLLBACK_VERSION="$current_version"
548
+ target_release_identity="release:${version}"
549
+
550
+ save_git_rollback_unit
551
+ capture_current_pointer
552
+ capture_activation_boundary
553
+ if [[ -e "$RUNTIME_ROOT/current" || -L "$RUNTIME_ROOT/current" ]]; then
554
+ atomic_symlink "$(readlink -f "$RUNTIME_ROOT/current")" "$RUNTIME_ROOT/previous"
555
+ fi
556
+
557
+ restart_requested_at="$(date --iso-8601=seconds)"
558
+ if [[ -n "${ZL_CODE_UPDATE_OPERATION_ID:-}" ]]; then
559
+ validate_operation_id "$ZL_CODE_UPDATE_OPERATION_ID"
560
+ restart_id="$ZL_CODE_UPDATE_OPERATION_ID"
561
+ else
562
+ restart_id="$(printf '%s-%s' "$(date -u +%Y%m%dT%H%M%SZ)" "$version")"
563
+ fi
564
+ write_restart_state \
565
+ "$restart_id" \
566
+ "${SCRIPT_PATH} activate" \
567
+ "$current_release_identity" \
568
+ "$target_release_identity" \
569
+ "$restart_requested_at"
570
+ echo "restart_id=$restart_id"
571
+ echo "restart_state_file=$RESTART_STATE_FILE"
572
+ echo "current_version=$current_version"
573
+ echo "target_version=$version"
574
+ echo "validated_at=$restart_requested_at"
575
+
576
+ emit_supervisor_phase "draining"
577
+ log "Requesting graceful update drain"
578
+ ACTIVATION_CUTOVER_STARTED=true
579
+ systemctl --user kill --signal="$DRAIN_SIGNAL" "$SERVICE_NAME"
580
+ if ! await_restart_state_field "drainStartedAt" "$DRAIN_ACK_TIMEOUT_SECONDS"; then
581
+ restore_activation_boundary
582
+ systemctl --user daemon-reload
583
+ systemctl --user restart "$SERVICE_NAME"
584
+ systemctl --user is-active --quiet "$SERVICE_NAME"
585
+ wait_for_url "$LOCAL_URL" || fail "service did not recover after the drain acknowledgement timeout"
586
+ ACTIVATION_ROLLBACK_COMPLETED=true
587
+ echo "rollback_version=$current_version"
588
+ fail "service did not acknowledge update drain within ${DRAIN_ACK_TIMEOUT_SECONDS}s"
589
+ fi
590
+ echo "drain=acknowledged"
591
+ if await_restart_state_field "quiesceCompletedAt" "$DRAIN_QUIESCE_TIMEOUT_SECONDS"; then
592
+ echo "drain_quiesce=completed"
593
+ else
594
+ echo "drain_quiesce=timeout"
595
+ fi
596
+
597
+ set +e
598
+ (
599
+ set -Eeuo pipefail
600
+ emit_supervisor_phase "activating"
601
+ log "Activating zl-code@${version}"
602
+ atomic_symlink "$directory" "$RUNTIME_ROOT/current"
603
+ install_npm_unit
604
+ systemctl --user daemon-reload
605
+ systemctl --user restart "$SERVICE_NAME"
606
+ emit_supervisor_phase "verifying"
607
+ verify_runtime "$version"
608
+ patch_restart_state "{\"healthyAt\":\"$(date --iso-8601=seconds)\"}"
609
+ )
610
+ activation_status=$?
611
+ set -e
612
+
613
+ if [[ "$activation_status" -ne 0 ]]; then
614
+ emit_supervisor_phase "rolling_back"
615
+ echo "error: npm activation failed; restoring the immediately preceding service runtime" >&2
616
+ restore_activation_boundary
617
+ systemctl --user daemon-reload
618
+ systemctl --user restart "$SERVICE_NAME"
619
+ systemctl --user is-active --quiet "$SERVICE_NAME"
620
+ wait_for_url "$LOCAL_URL" || fail "activation failed and the preceding service runtime did not recover"
621
+ ACTIVATION_ROLLBACK_COMPLETED=true
622
+ echo "rollback_version=$current_version"
623
+ fail "activation failed and the preceding service runtime was restored"
624
+ fi
625
+ echo "backup_dir=$backup_dir"
626
+ echo "rollback=git-unit-retained"
627
+ if [[ -f "$RESTART_STATE_FILE" ]]; then
628
+ echo "restart_state=$(cat "$RESTART_STATE_FILE")"
629
+ fi
630
+ }
631
+
632
+ supervised_update() {
633
+ local version=$1
634
+ local confirmed=$2
635
+ local backup_dir
636
+ local backup_output
637
+ [[ "$confirmed" == true ]] || fail "supervised update requires --confirm-supervised"
638
+ SUPERVISED_MODE=true
639
+ trap supervised_exit_cleanup EXIT
640
+ trap 'exit 124' TERM INT
641
+ acquire_lock
642
+
643
+ emit_supervisor_phase "preparing"
644
+ prepare_release "$version" true
645
+ emit_supervisor_phase "backing_up"
646
+ backup_output="$(create_backup)"
647
+ printf '%s\n' "$backup_output" >&2
648
+ backup_dir="$(printf '%s\n' "$backup_output" | sed -n 's/^backup_dir=//p' | tail -n 1)"
649
+ [[ -n "$backup_dir" ]] || fail "supervised update did not produce a backup path"
650
+ emit_supervisor_phase "draining"
651
+ activate_release "$version" "$backup_dir" true
652
+ emit_supervisor_phase "succeeded"
653
+ trap - EXIT TERM INT
654
+ }
655
+
656
+ install_update_supervisor() {
657
+ local confirmed=$1
658
+ local temporary="${SUPERVISOR_UNIT_PATH}.tmp.$$"
659
+ [[ "$confirmed" == true ]] || fail "supervisor installation requires --confirm-install"
660
+ acquire_lock
661
+ require_command install
662
+ require_command systemctl
663
+ require_file "$SUPERVISOR_UNIT_TEMPLATE"
664
+ require_file "$RUNTIME_ROOT/current/node_modules/zl-code/dist/bin.mjs"
665
+
666
+ mkdir -p "$(dirname "$SUPERVISOR_UNIT_PATH")"
667
+ install -m 600 "$SUPERVISOR_UNIT_TEMPLATE" "$temporary"
668
+ mv -Tf "$temporary" "$SUPERVISOR_UNIT_PATH"
669
+ systemctl --user daemon-reload
670
+ systemctl --user enable --now "$SUPERVISOR_SERVICE_NAME"
671
+ systemctl --user is-active --quiet "$SUPERVISOR_SERVICE_NAME"
672
+ echo "supervisor_unit=$SUPERVISOR_UNIT_PATH"
673
+ echo "supervisor_service=active"
674
+ }
675
+
676
+ rollback_runtime() {
677
+ local target=$1
678
+ local confirmed=$2
679
+ local previous_target
680
+ [[ "$confirmed" == true ]] || fail "rollback requires --confirm-cutover"
681
+ [[ "$target" == previous || "$target" == git ]] || fail "rollback target must be previous or git"
682
+ acquire_lock
683
+ require_command curl
684
+ require_command systemctl
685
+
686
+ if [[ "$target" == git ]]; then
687
+ log "Restoring the retained git-backed service unit"
688
+ restore_git_unit_and_pointer
689
+ systemctl --user daemon-reload
690
+ systemctl --user restart "$SERVICE_NAME"
691
+ systemctl --user is-active --quiet "$SERVICE_NAME"
692
+ wait_for_url "$LOCAL_URL" || fail "git-backed service did not become healthy"
693
+ echo "rollback=git"
694
+ return
695
+ fi
696
+
697
+ [[ -e "$RUNTIME_ROOT/previous" || -L "$RUNTIME_ROOT/previous" ]] ||
698
+ fail "previous npm pointer is absent"
699
+ previous_target="$(readlink -f "$RUNTIME_ROOT/previous")"
700
+ [[ -d "$previous_target" ]] || fail "previous npm release is missing: $previous_target"
701
+ atomic_symlink "$previous_target" "$RUNTIME_ROOT/current"
702
+ systemctl --user restart "$SERVICE_NAME"
703
+ systemctl --user is-active --quiet "$SERVICE_NAME"
704
+ wait_for_url "$LOCAL_URL" || fail "previous npm release did not become healthy"
705
+ echo "rollback=previous"
706
+ echo "current=$previous_target"
707
+ }
708
+
709
+ show_status() {
710
+ echo "service=$SERVICE_NAME"
711
+ systemctl --user show "$SERVICE_NAME" -p ActiveState -p SubState -p MainPID -p FragmentPath -p WorkingDirectory --no-pager
712
+ for pointer in candidate current previous; do
713
+ if [[ -e "$RUNTIME_ROOT/$pointer" || -L "$RUNTIME_ROOT/$pointer" ]]; then
714
+ echo "$pointer=$(readlink -f "$RUNTIME_ROOT/$pointer")"
715
+ else
716
+ echo "$pointer=absent"
717
+ fi
718
+ done
719
+ echo "unit=$(readlink -f "$UNIT_PATH")"
720
+ echo "node=$($NODE_BIN --version)"
721
+ }
722
+
723
+ command_name=${1:-}
724
+ shift || true
725
+ case "$command_name" in
726
+ status)
727
+ [[ $# -eq 0 ]] || fail "status accepts no arguments"
728
+ show_status
729
+ ;;
730
+ prepare)
731
+ version=${1:-}
732
+ shift || true
733
+ no_cutover=false
734
+ [[ ${1:-} == --no-cutover ]] && no_cutover=true && shift
735
+ [[ $# -eq 0 ]] || fail "unexpected prepare arguments: $*"
736
+ prepare_release "$version" "$no_cutover"
737
+ ;;
738
+ backup)
739
+ [[ $# -eq 0 ]] || fail "backup accepts no arguments"
740
+ create_backup
741
+ ;;
742
+ activate)
743
+ version=${1:-}
744
+ shift || true
745
+ backup_dir=""
746
+ confirmed=false
747
+ while [[ $# -gt 0 ]]; do
748
+ case "$1" in
749
+ --backup-dir)
750
+ [[ $# -ge 2 ]] || fail "--backup-dir requires a path"
751
+ backup_dir=$2
752
+ shift 2
753
+ ;;
754
+ --confirm-cutover)
755
+ confirmed=true
756
+ shift
757
+ ;;
758
+ *) fail "unexpected activate argument: $1" ;;
759
+ esac
760
+ done
761
+ activate_release "$version" "$backup_dir" "$confirmed"
762
+ ;;
763
+ verify)
764
+ version=${1:-}
765
+ shift || true
766
+ [[ $# -eq 0 ]] || fail "verify accepts exactly one version"
767
+ verify_runtime "$version"
768
+ ;;
769
+ rollback)
770
+ target=""
771
+ confirmed=false
772
+ while [[ $# -gt 0 ]]; do
773
+ case "$1" in
774
+ --to)
775
+ [[ $# -ge 2 ]] || fail "--to requires previous or git"
776
+ target=$2
777
+ shift 2
778
+ ;;
779
+ --confirm-cutover)
780
+ confirmed=true
781
+ shift
782
+ ;;
783
+ *) fail "unexpected rollback argument: $1" ;;
784
+ esac
785
+ done
786
+ rollback_runtime "$target" "$confirmed"
787
+ ;;
788
+ supervised-update)
789
+ version=${1:-}
790
+ confirmation=${2:-}
791
+ [[ $# -eq 2 ]] || fail "supervised-update requires <version> --confirm-supervised"
792
+ [[ "$confirmation" == "--confirm-supervised" ]] || fail "supervised update requires --confirm-supervised"
793
+ supervised_update "$version" true
794
+ ;;
795
+ install-supervisor)
796
+ confirmation=${1:-}
797
+ [[ $# -eq 1 ]] || fail "install-supervisor requires --confirm-install"
798
+ [[ "$confirmation" == "--confirm-install" ]] || fail "supervisor installation requires --confirm-install"
799
+ install_update_supervisor true
800
+ ;;
801
+ help | --help | -h)
802
+ usage
803
+ ;;
804
+ *)
805
+ usage >&2
806
+ exit 2
807
+ ;;
808
+ esac