@visa/cli 4.1.0-rc.99 → 5.0.0-rc.335

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 (85) hide show
  1. package/README.md +282 -156
  2. package/dist/cli.js +333 -532
  3. package/dist/managed-runtime/resolve-and-update.mjs +268 -0
  4. package/dist/managed-runtime/runtime-readiness.mjs +126 -0
  5. package/dist/managed-runtime/update-and-restart.mjs +1079 -0
  6. package/dist/mcp-apps/ucp-checkout.html +280 -0
  7. package/dist/mcp-server/index.js +234 -387
  8. package/dist/merchant-ucp-mcp/index.js +7 -0
  9. package/dist/skills/pair-visa-agent/RUNTIMES.md +56 -26
  10. package/dist/skills/pair-visa-agent/SKILL.md +319 -320
  11. package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
  12. package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
  13. package/dist/skills/visa-shopify-checkout/SKILL.md +122 -0
  14. package/dist/skills/visa-shopify-checkout/references/evidence-and-states.md +37 -0
  15. package/dist/skills/visa-ucp-shopping/SKILL.md +86 -0
  16. package/install.ps1 +10 -6
  17. package/install.sh +7 -2
  18. package/native/bin/darwin-arm64/visa-runtime-signer +0 -0
  19. package/native/bin/darwin-x64/visa-runtime-signer +0 -0
  20. package/native/bin/linux-arm64/visa-runtime-signer +0 -0
  21. package/native/bin/linux-x64/visa-runtime-signer +0 -0
  22. package/native/bin/win32-arm64/visa-runtime-signer.exe +0 -0
  23. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  24. package/native/bin/win32-x64/visa-runtime-signer.exe +0 -0
  25. package/package.json +23 -31
  26. package/server.json +3 -3
  27. package/dist/checkout-engine/adapters/generic.d.ts +0 -23
  28. package/dist/checkout-engine/adapters/generic.js +0 -216
  29. package/dist/checkout-engine/adapters/index.d.ts +0 -10
  30. package/dist/checkout-engine/adapters/index.js +0 -24
  31. package/dist/checkout-engine/adapters/shopify.d.ts +0 -31
  32. package/dist/checkout-engine/adapters/shopify.js +0 -423
  33. package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
  34. package/dist/checkout-engine/adapters/stripe-like.js +0 -21
  35. package/dist/checkout-engine/amount.d.ts +0 -15
  36. package/dist/checkout-engine/amount.js +0 -72
  37. package/dist/checkout-engine/browser-launch.d.ts +0 -46
  38. package/dist/checkout-engine/browser-launch.js +0 -81
  39. package/dist/checkout-engine/ceremony.d.ts +0 -64
  40. package/dist/checkout-engine/ceremony.js +0 -261
  41. package/dist/checkout-engine/cli-engine.d.ts +0 -227
  42. package/dist/checkout-engine/cli-engine.js +0 -779
  43. package/dist/checkout-engine/detect.d.ts +0 -61
  44. package/dist/checkout-engine/detect.js +0 -398
  45. package/dist/checkout-engine/evidence.d.ts +0 -25
  46. package/dist/checkout-engine/evidence.js +0 -104
  47. package/dist/checkout-engine/executor.d.ts +0 -176
  48. package/dist/checkout-engine/executor.js +0 -1325
  49. package/dist/checkout-engine/hosted-approval.d.ts +0 -187
  50. package/dist/checkout-engine/hosted-approval.js +0 -478
  51. package/dist/checkout-engine/index.d.ts +0 -6
  52. package/dist/checkout-engine/index.js +0 -8
  53. package/dist/checkout-engine/inline-target.d.ts +0 -13
  54. package/dist/checkout-engine/inline-target.js +0 -37
  55. package/dist/checkout-engine/instrument.d.ts +0 -61
  56. package/dist/checkout-engine/instrument.js +0 -87
  57. package/dist/checkout-engine/live-fill-approval.d.ts +0 -43
  58. package/dist/checkout-engine/live-fill-approval.js +0 -90
  59. package/dist/checkout-engine/mandate/card-mandate.d.ts +0 -121
  60. package/dist/checkout-engine/mandate/card-mandate.js +0 -227
  61. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +0 -142
  62. package/dist/checkout-engine/mandate/mandate-ledger.js +0 -338
  63. package/dist/checkout-engine/mandate.d.ts +0 -25
  64. package/dist/checkout-engine/mandate.js +0 -100
  65. package/dist/checkout-engine/outcome.d.ts +0 -30
  66. package/dist/checkout-engine/outcome.js +0 -225
  67. package/dist/checkout-engine/owner-only-file.d.ts +0 -19
  68. package/dist/checkout-engine/owner-only-file.js +0 -41
  69. package/dist/checkout-engine/package.json +0 -3
  70. package/dist/checkout-engine/receipt.d.ts +0 -81
  71. package/dist/checkout-engine/receipt.js +0 -109
  72. package/dist/checkout-engine/repo-env.d.ts +0 -11
  73. package/dist/checkout-engine/repo-env.js +0 -23
  74. package/dist/checkout-engine/trace-handles.d.ts +0 -8
  75. package/dist/checkout-engine/trace-handles.js +0 -12
  76. package/dist/checkout-engine/types.d.ts +0 -44
  77. package/dist/checkout-engine/types.js +0 -2
  78. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  79. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
  80. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -82
  81. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -180
  82. package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -179
  83. package/dist/checkout-engine/vgs-live-instrument.js +0 -296
  84. package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
  85. package/dist/checkout-engine/vic-confirmation.js +0 -39
@@ -0,0 +1,1079 @@
1
+ #!/usr/bin/env node
2
+ /** Supervisor-owned exact-RC deployment transaction for managed runtimes. */
3
+
4
+ import { createHash, randomUUID } from 'node:crypto'
5
+ import { execFileSync, execSync } from 'node:child_process'
6
+ import {
7
+ chmodSync,
8
+ closeSync,
9
+ existsSync,
10
+ fsyncSync,
11
+ lstatSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ readlinkSync,
17
+ realpathSync,
18
+ renameSync,
19
+ rmSync,
20
+ symlinkSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ } from 'node:fs'
24
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+ import {
27
+ assertSameRuntimeBuild,
28
+ assertSameShopifyRuntime,
29
+ commerceSkillArgs,
30
+ parseRuntimeReadiness,
31
+ resolveRuntimeUpdateConfig,
32
+ runtimeReadinessExecOptions,
33
+ } from './runtime-readiness.mjs'
34
+
35
+ const PREFIX = '[managed-runtime-update]'
36
+ const EXACT_RC_RE = /^\d+\.\d+\.\d+-rc\.\d+$/
37
+ const SRI_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/
38
+ const DEFAULT_COMMAND_TIMEOUT_MS = 120_000
39
+ const MAX_ROLLBACK_ATTEMPTS = 2
40
+ const COMMANDS_PER_TRANSACTION_BOUND = 12
41
+ const READINESS_PROBES_PER_TRANSACTION_BOUND = 3
42
+ const RELEASE_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/
43
+ const PENDING_TRANSACTION_FILE = '.pending-update.json'
44
+
45
+ function exactRcParts(version) {
46
+ const match = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(version)
47
+ if (!match) return null
48
+ const parts = match.slice(1).map(Number)
49
+ return parts.every(Number.isSafeInteger) ? parts : null
50
+ }
51
+
52
+ export function compareExactRc(left, right) {
53
+ const leftParts = exactRcParts(left)
54
+ const rightParts = exactRcParts(right)
55
+ if (!leftParts || !rightParts) {
56
+ throw new ManagedRuntimeUpdateError(
57
+ 'invalid_runtime_state',
58
+ 'managed release version is not an exact RC'
59
+ )
60
+ }
61
+ for (let index = 0; index < leftParts.length; index += 1) {
62
+ if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index]
63
+ }
64
+ return 0
65
+ }
66
+
67
+ export class ManagedRuntimeUpdateError extends Error {
68
+ constructor(code, message, options = {}) {
69
+ super(message, options)
70
+ this.name = 'ManagedRuntimeUpdateError'
71
+ this.code = code
72
+ }
73
+ }
74
+
75
+ function structuredLog(event, fields = {}, logger = console) {
76
+ logger.log(`${PREFIX} ${JSON.stringify({ event, ...fields })}`)
77
+ }
78
+
79
+ function safeInteger(value, fallback, name) {
80
+ if (value === undefined || value === '') return fallback
81
+ const parsed = Number(value)
82
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
83
+ throw new ManagedRuntimeUpdateError(
84
+ 'invalid_configuration',
85
+ `${name} must be a positive integer`
86
+ )
87
+ }
88
+ return parsed
89
+ }
90
+
91
+ export function resolveManagedUpdateConfig(env = process.env) {
92
+ const exactVersion = env.VISA_CLI_EXACT_VERSION?.trim() || ''
93
+ if (!EXACT_RC_RE.test(exactVersion) || !exactRcParts(exactVersion)) {
94
+ throw new ManagedRuntimeUpdateError(
95
+ 'exact_target_required',
96
+ 'VISA_CLI_EXACT_VERSION must be an exact RC such as 4.1.0-rc.253; floating dist-tags are refused'
97
+ )
98
+ }
99
+ const managedDir = env.VISA_MANAGED_RUNTIME_DIR?.trim() || ''
100
+ if (!managedDir || !isAbsolute(managedDir)) {
101
+ throw new ManagedRuntimeUpdateError(
102
+ 'unmanaged_runtime',
103
+ 'VISA_MANAGED_RUNTIME_DIR must be an absolute supervisor-owned directory'
104
+ )
105
+ }
106
+ const runtime = resolveRuntimeUpdateConfig(env)
107
+ if (!runtime.restartCommand || !runtime.readinessCommand) {
108
+ throw new ManagedRuntimeUpdateError(
109
+ 'unmanaged_runtime',
110
+ 'managed updates require RUNTIME_RESTART_COMMAND and RUNTIME_READINESS_COMMAND'
111
+ )
112
+ }
113
+ const expectedEnvironment = env.VISA_RUNTIME_EXPECTED_ENVIRONMENT?.trim() || ''
114
+ if (!/^(?:development|staging|production)$/.test(expectedEnvironment)) {
115
+ throw new ManagedRuntimeUpdateError(
116
+ 'expected_environment_required',
117
+ 'VISA_RUNTIME_EXPECTED_ENVIRONMENT must be development, staging, or production'
118
+ )
119
+ }
120
+ const resolvedManagedDir = resolve(managedDir)
121
+ if (existsSync(resolvedManagedDir) && realpathSync(resolvedManagedDir) !== resolvedManagedDir) {
122
+ throw new ManagedRuntimeUpdateError(
123
+ 'invalid_runtime_state',
124
+ 'VISA_MANAGED_RUNTIME_DIR and its parents must not contain symbolic links'
125
+ )
126
+ }
127
+ const commandTimeoutMs = safeInteger(
128
+ env.VISA_RUNTIME_UPDATE_TIMEOUT_MS,
129
+ DEFAULT_COMMAND_TIMEOUT_MS,
130
+ 'VISA_RUNTIME_UPDATE_TIMEOUT_MS'
131
+ )
132
+ return {
133
+ ...runtime,
134
+ exactVersion,
135
+ expectedEnvironment,
136
+ managedDir: existsSync(resolvedManagedDir)
137
+ ? realpathSync(resolvedManagedDir)
138
+ : resolvedManagedDir,
139
+ commandTimeoutMs,
140
+ lockLeaseMs:
141
+ commandTimeoutMs * COMMANDS_PER_TRANSACTION_BOUND +
142
+ runtime.readinessTimeoutMs * READINESS_PROBES_PER_TRANSACTION_BOUND,
143
+ }
144
+ }
145
+
146
+ function ensureCanonicalDirectory(path, label) {
147
+ mkdirSync(path, { recursive: true, mode: 0o700 })
148
+ if (lstatSync(path).isSymbolicLink() || realpathSync(path) !== resolve(path)) {
149
+ throw new ManagedRuntimeUpdateError(
150
+ 'invalid_runtime_state',
151
+ `${label} and its parents must not contain symbolic links`
152
+ )
153
+ }
154
+ }
155
+
156
+ function processIsAlive(pid, processKill = process.kill) {
157
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false
158
+ try {
159
+ processKill(pid, 0)
160
+ return true
161
+ } catch (error) {
162
+ return error?.code === 'EPERM'
163
+ }
164
+ }
165
+
166
+ export function acquireUpdateLock(managedDir, correlationId, options = {}) {
167
+ const now = options.now?.() ?? Date.now()
168
+ const leaseMs =
169
+ options.leaseMs ??
170
+ DEFAULT_COMMAND_TIMEOUT_MS * COMMANDS_PER_TRANSACTION_BOUND +
171
+ 60_000 * READINESS_PROBES_PER_TRANSACTION_BOUND
172
+ mkdirSync(managedDir, { recursive: true, mode: 0o700 })
173
+ ensureCanonicalDirectory(managedDir, 'VISA_MANAGED_RUNTIME_DIR')
174
+ const lockDir = join(managedDir, '.update-lock')
175
+ for (let attempt = 0; attempt < 3; attempt += 1) {
176
+ const candidate = join(managedDir, `.update-lock-candidate-${correlationId}-${attempt}`)
177
+ try {
178
+ mkdirSync(candidate, { mode: 0o700 })
179
+ try {
180
+ writeFileSync(
181
+ join(candidate, 'owner.json'),
182
+ JSON.stringify({
183
+ correlationId,
184
+ pid: process.pid,
185
+ createdAtMs: now,
186
+ expiresAtMs: now + leaseMs,
187
+ }),
188
+ { mode: 0o600, flag: 'wx' }
189
+ )
190
+ fsyncDirectory(candidate)
191
+ renameSync(candidate, lockDir)
192
+ fsyncDirectory(managedDir)
193
+ } catch (error) {
194
+ rmSync(candidate, { recursive: true, force: true })
195
+ throw error
196
+ }
197
+ return () => {
198
+ try {
199
+ const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'))
200
+ if (owner.correlationId === correlationId) {
201
+ rmSync(lockDir, { recursive: true, force: true })
202
+ }
203
+ } catch {
204
+ // Never remove a lock whose ownership can no longer be proven.
205
+ }
206
+ }
207
+ } catch (error) {
208
+ rmSync(candidate, { recursive: true, force: true })
209
+ if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error
210
+ let owner = null
211
+ try {
212
+ owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'))
213
+ } catch {
214
+ // An incomplete owner record is stale because owner creation is atomic under mkdir.
215
+ }
216
+ const active =
217
+ owner &&
218
+ Number.isSafeInteger(owner.expiresAtMs) &&
219
+ owner.expiresAtMs > now &&
220
+ processIsAlive(owner.pid, options.processKill)
221
+ if (active) {
222
+ throw new ManagedRuntimeUpdateError(
223
+ 'update_in_progress',
224
+ 'another supervisor-owned runtime update is already in progress'
225
+ )
226
+ }
227
+ const stale = join(managedDir, `.stale-update-lock-${correlationId}-${attempt}`)
228
+ try {
229
+ renameSync(lockDir, stale)
230
+ rmSync(stale, { recursive: true, force: true })
231
+ } catch (renameError) {
232
+ if (renameError?.code !== 'ENOENT') throw renameError
233
+ }
234
+ }
235
+ }
236
+ throw new ManagedRuntimeUpdateError('update_in_progress', 'could not acquire runtime update lock')
237
+ }
238
+
239
+ function parseJson(raw, label) {
240
+ try {
241
+ return JSON.parse(raw)
242
+ } catch (error) {
243
+ throw new ManagedRuntimeUpdateError(
244
+ 'invalid_package_metadata',
245
+ `${label} returned invalid JSON`,
246
+ {
247
+ cause: error,
248
+ }
249
+ )
250
+ }
251
+ }
252
+
253
+ function assertManagedChild(managedDir, candidate, label) {
254
+ const rel = relative(managedDir, candidate)
255
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
256
+ throw new ManagedRuntimeUpdateError(
257
+ 'invalid_runtime_state',
258
+ `${label} escaped the managed directory`
259
+ )
260
+ }
261
+ }
262
+
263
+ export function releaseTreeIntegrity(releaseDir) {
264
+ const hash = createHash('sha512')
265
+ const visit = (directory, prefix = '') => {
266
+ for (const name of readdirSync(directory).sort()) {
267
+ if (!prefix && name === 'deployment-receipt.json') continue
268
+ const absolute = join(directory, name)
269
+ const entry = lstatSync(absolute)
270
+ const path = prefix ? `${prefix}/${name}` : name
271
+ if (entry.isDirectory()) {
272
+ hash.update(`directory\0${path}\0${entry.mode & 0o777}\0`)
273
+ visit(absolute, path)
274
+ } else if (entry.isFile()) {
275
+ hash.update(`file\0${path}\0${entry.mode & 0o777}\0${entry.size}\0`)
276
+ hash.update(readFileSync(absolute))
277
+ } else if (entry.isSymbolicLink()) {
278
+ const target = readlinkSync(absolute)
279
+ const resolvedTarget = resolve(dirname(absolute), target)
280
+ assertManagedChild(releaseDir, resolvedTarget, `release symlink ${path}`)
281
+ assertManagedChild(releaseDir, realpathSync(absolute), `release symlink ${path}`)
282
+ hash.update(`symlink\0${path}\0${target}\0`)
283
+ } else {
284
+ throw new ManagedRuntimeUpdateError(
285
+ 'invalid_runtime_state',
286
+ `managed release contains unsupported filesystem entry ${path}`
287
+ )
288
+ }
289
+ }
290
+ }
291
+ visit(releaseDir)
292
+ return `sha512-${hash.digest('base64')}`
293
+ }
294
+
295
+ function verifyReleaseReceipt(releaseDir, receipt) {
296
+ if (!RELEASE_INTEGRITY_RE.test(receipt.releaseIntegrity || '')) {
297
+ throw new ManagedRuntimeUpdateError(
298
+ 'invalid_runtime_state',
299
+ 'managed deployment receipt omitted release integrity'
300
+ )
301
+ }
302
+ if (releaseTreeIntegrity(releaseDir) !== receipt.releaseIntegrity) {
303
+ throw new ManagedRuntimeUpdateError(
304
+ 'release_integrity_mismatch',
305
+ 'managed release bytes no longer match the known-good deployment receipt'
306
+ )
307
+ }
308
+ }
309
+
310
+ function fsyncDirectory(path) {
311
+ const fd = openSync(path, 'r')
312
+ try {
313
+ fsyncSync(fd)
314
+ } finally {
315
+ closeSync(fd)
316
+ }
317
+ }
318
+
319
+ function atomicWritePrivateJson(path, value, id = randomUUID()) {
320
+ const temp = join(dirname(path), `.${basename(path)}-${id}.tmp`)
321
+ let fd
322
+ try {
323
+ fd = openSync(temp, 'wx', 0o600)
324
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`)
325
+ fsyncSync(fd)
326
+ closeSync(fd)
327
+ fd = undefined
328
+ renameSync(temp, path)
329
+ chmodSync(path, 0o600)
330
+ fsyncDirectory(dirname(path))
331
+ } finally {
332
+ if (fd !== undefined) closeSync(fd)
333
+ rmSync(temp, { force: true })
334
+ }
335
+ }
336
+
337
+ export function readCurrentRelease(managedDir) {
338
+ const current = join(managedDir, 'current')
339
+ if (!existsSync(current)) return null
340
+ if (!lstatSync(current).isSymbolicLink()) {
341
+ throw new ManagedRuntimeUpdateError(
342
+ 'invalid_runtime_state',
343
+ 'current runtime pointer is not a symbolic link'
344
+ )
345
+ }
346
+ const target = realpathSync(resolve(dirname(current), readlinkSync(current)))
347
+ assertManagedChild(managedDir, target, 'current runtime link')
348
+ const receiptPath = join(target, 'deployment-receipt.json')
349
+ if (!existsSync(receiptPath)) {
350
+ throw new ManagedRuntimeUpdateError(
351
+ 'invalid_runtime_state',
352
+ 'current managed runtime has no deployment receipt and cannot be used as known-good'
353
+ )
354
+ }
355
+ const receipt = parseJson(readFileSync(receiptPath, 'utf8'), 'known-good deployment receipt')
356
+ const commerceReady = receipt.commerceReady ?? Boolean(receipt.shopifyRuntime)
357
+ if (
358
+ !EXACT_RC_RE.test(receipt.version) ||
359
+ receipt.releaseDir !== target ||
360
+ typeof receipt.targetEnvironment !== 'string' ||
361
+ typeof commerceReady !== 'boolean' ||
362
+ (commerceReady &&
363
+ (typeof receipt.shopifyRuntime !== 'object' || receipt.shopifyRuntime === null))
364
+ ) {
365
+ throw new ManagedRuntimeUpdateError(
366
+ 'invalid_runtime_state',
367
+ 'known-good deployment receipt is invalid'
368
+ )
369
+ }
370
+ verifyReleaseReceipt(target, receipt)
371
+ return {
372
+ releaseDir: target,
373
+ receipt: {
374
+ ...receipt,
375
+ commerceReady,
376
+ commerceActionRequired: receipt.commerceActionRequired ?? null,
377
+ },
378
+ }
379
+ }
380
+
381
+ function visaBinary(releaseDir) {
382
+ return join(releaseDir, 'node_modules', '.bin', 'visa')
383
+ }
384
+
385
+ function sha512Integrity(filePath) {
386
+ return `sha512-${createHash('sha512').update(readFileSync(filePath)).digest('base64')}`
387
+ }
388
+
389
+ export function stageExactRelease(config, dependencies = {}) {
390
+ const execFile = dependencies.execFileSync ?? execFileSync
391
+ const id = dependencies.randomUUID?.() ?? randomUUID()
392
+ const correlationId = config.correlationId ?? 'standalone-stage'
393
+ const observedNpm = (operation, args, options) => {
394
+ const startedAt = Date.now()
395
+ structuredLog(
396
+ 'external_request',
397
+ { correlationId, service: 'npm_registry', operation, targetVersion: config.exactVersion },
398
+ dependencies.logger
399
+ )
400
+ try {
401
+ const result = execFile('npm', args, options)
402
+ structuredLog(
403
+ 'external_response',
404
+ {
405
+ correlationId,
406
+ service: 'npm_registry',
407
+ operation,
408
+ ok: true,
409
+ durationMs: Date.now() - startedAt,
410
+ },
411
+ dependencies.logger
412
+ )
413
+ return result
414
+ } catch (error) {
415
+ structuredLog(
416
+ 'external_response',
417
+ {
418
+ correlationId,
419
+ service: 'npm_registry',
420
+ operation,
421
+ ok: false,
422
+ durationMs: Date.now() - startedAt,
423
+ },
424
+ dependencies.logger
425
+ )
426
+ throw error
427
+ }
428
+ }
429
+ const releasesDir = join(config.managedDir, 'releases')
430
+ const releaseDir = join(releasesDir, config.exactVersion)
431
+ ensureCanonicalDirectory(releasesDir, 'managed releases directory')
432
+
433
+ if (existsSync(releaseDir)) {
434
+ if (lstatSync(releaseDir).isSymbolicLink()) {
435
+ throw new ManagedRuntimeUpdateError(
436
+ 'invalid_runtime_state',
437
+ 'managed release directory must not be a symbolic link'
438
+ )
439
+ }
440
+ const receiptPath = join(releaseDir, 'deployment-receipt.json')
441
+ if (existsSync(receiptPath) && existsSync(visaBinary(releaseDir))) {
442
+ const installed = parseJson(
443
+ readFileSync(join(releaseDir, 'node_modules', '@visa', 'cli', 'package.json'), 'utf8'),
444
+ 'installed package manifest'
445
+ )
446
+ const receipt = parseJson(readFileSync(receiptPath, 'utf8'), 'deployment receipt')
447
+ if (
448
+ installed.version !== config.exactVersion ||
449
+ receipt.version !== config.exactVersion ||
450
+ receipt.releaseDir !== releaseDir
451
+ ) {
452
+ throw new ManagedRuntimeUpdateError(
453
+ 'invalid_runtime_state',
454
+ 'existing managed release does not match its deployment receipt'
455
+ )
456
+ }
457
+ verifyReleaseReceipt(releaseDir, receipt)
458
+ return releaseDir
459
+ }
460
+ // A prior attempt that never passed loaded-process health is not known-good.
461
+ const quarantine = join(releasesDir, `.quarantine-${config.exactVersion}-${id}`)
462
+ renameSync(releaseDir, quarantine)
463
+ }
464
+
465
+ const stagingDir = join(releasesDir, `.staging-${config.exactVersion}-${id}`)
466
+ mkdirSync(stagingDir, { recursive: true, mode: 0o700 })
467
+ try {
468
+ const packageSpec = `@visa/cli@${config.exactVersion}`
469
+ const metadata = parseJson(
470
+ observedNpm(
471
+ 'view_exact_metadata',
472
+ ['view', packageSpec, 'version', 'dist.integrity', '--json'],
473
+ {
474
+ encoding: 'utf8',
475
+ stdio: ['ignore', 'pipe', 'pipe'],
476
+ timeout: config.commandTimeoutMs,
477
+ }
478
+ ),
479
+ 'npm view'
480
+ )
481
+ const expectedIntegrity = metadata?.dist?.integrity ?? metadata?.['dist.integrity'] ?? ''
482
+ if (metadata.version !== config.exactVersion || !SRI_RE.test(expectedIntegrity)) {
483
+ throw new ManagedRuntimeUpdateError(
484
+ 'target_not_authorized',
485
+ 'registry metadata did not attest the exact requested RC and sha512 integrity'
486
+ )
487
+ }
488
+
489
+ const pack = parseJson(
490
+ observedNpm(
491
+ 'download_exact_tarball',
492
+ ['pack', packageSpec, '--json', '--pack-destination', stagingDir],
493
+ {
494
+ encoding: 'utf8',
495
+ stdio: ['ignore', 'pipe', 'pipe'],
496
+ timeout: config.commandTimeoutMs,
497
+ }
498
+ ),
499
+ 'npm pack'
500
+ )
501
+ const packed = Array.isArray(pack) ? pack[0] : null
502
+ if (
503
+ !packed ||
504
+ packed.version !== config.exactVersion ||
505
+ packed.integrity !== expectedIntegrity ||
506
+ basename(packed.filename) !== packed.filename
507
+ ) {
508
+ throw new ManagedRuntimeUpdateError(
509
+ 'package_integrity_mismatch',
510
+ 'packed artifact metadata did not match the exact authorized RC'
511
+ )
512
+ }
513
+ const tarball = join(stagingDir, packed.filename)
514
+ if (sha512Integrity(tarball) !== expectedIntegrity) {
515
+ throw new ManagedRuntimeUpdateError(
516
+ 'package_integrity_mismatch',
517
+ 'packed artifact bytes did not match the registry sha512 integrity'
518
+ )
519
+ }
520
+
521
+ try {
522
+ observedNpm(
523
+ 'install_staged_tarball',
524
+ ['install', '--prefix', stagingDir, '--omit=dev', tarball],
525
+ {
526
+ stdio: ['ignore', 'pipe', 'pipe'],
527
+ timeout: config.commandTimeoutMs,
528
+ }
529
+ )
530
+ } catch (error) {
531
+ throw new ManagedRuntimeUpdateError(
532
+ 'package_install_failed',
533
+ 'the exact verified package could not be installed into the staged release',
534
+ { cause: error }
535
+ )
536
+ }
537
+ const installed = parseJson(
538
+ readFileSync(join(stagingDir, 'node_modules', '@visa', 'cli', 'package.json'), 'utf8'),
539
+ 'installed package manifest'
540
+ )
541
+ if (installed.version !== config.exactVersion || !existsSync(visaBinary(stagingDir))) {
542
+ throw new ManagedRuntimeUpdateError(
543
+ 'installed_version_mismatch',
544
+ 'staged CLI did not expose the exact requested version'
545
+ )
546
+ }
547
+ rmSync(tarball, { force: true })
548
+ renameSync(stagingDir, releaseDir)
549
+ return releaseDir
550
+ } catch (error) {
551
+ rmSync(stagingDir, { recursive: true, force: true })
552
+ throw error
553
+ }
554
+ }
555
+
556
+ export function activateRelease(managedDir, releaseDir, id = randomUUID()) {
557
+ assertManagedChild(managedDir, releaseDir, 'release')
558
+ const current = join(managedDir, 'current')
559
+ const next = join(managedDir, `.current-${id}`)
560
+ symlinkSync(relative(managedDir, releaseDir), next, 'dir')
561
+ renameSync(next, current)
562
+ fsyncDirectory(managedDir)
563
+ }
564
+
565
+ function removeCurrentIfTarget(managedDir, releaseDir) {
566
+ const current = join(managedDir, 'current')
567
+ if (!existsSync(current) && !lstatExists(current)) return
568
+ if (!lstatSync(current).isSymbolicLink()) return
569
+ if (resolve(dirname(current), readlinkSync(current)) === releaseDir) {
570
+ unlinkSync(current)
571
+ fsyncDirectory(managedDir)
572
+ }
573
+ }
574
+
575
+ function lstatExists(path) {
576
+ try {
577
+ lstatSync(path)
578
+ return true
579
+ } catch (error) {
580
+ if (error?.code === 'ENOENT') return false
581
+ throw error
582
+ }
583
+ }
584
+
585
+ function pendingTransactionPath(managedDir) {
586
+ return join(managedDir, PENDING_TRANSACTION_FILE)
587
+ }
588
+
589
+ function writePendingTransaction(config, targetRelease, previous, id) {
590
+ atomicWritePrivateJson(
591
+ pendingTransactionPath(config.managedDir),
592
+ {
593
+ correlationId: config.correlationId,
594
+ targetRelease,
595
+ previousRelease: previous?.releaseDir ?? null,
596
+ createdAt: new Date().toISOString(),
597
+ },
598
+ id
599
+ )
600
+ }
601
+
602
+ function clearPendingTransaction(managedDir) {
603
+ const path = pendingTransactionPath(managedDir)
604
+ if (!lstatExists(path)) return
605
+ unlinkSync(path)
606
+ fsyncDirectory(managedDir)
607
+ }
608
+
609
+ export function recoverInterruptedActivation(managedDir, logger = console) {
610
+ const path = pendingTransactionPath(managedDir)
611
+ if (!lstatExists(path)) return false
612
+ const pending = parseJson(readFileSync(path, 'utf8'), 'pending managed update')
613
+ if (typeof pending.targetRelease !== 'string') {
614
+ throw new ManagedRuntimeUpdateError(
615
+ 'invalid_runtime_state',
616
+ 'pending managed update is invalid'
617
+ )
618
+ }
619
+ assertManagedChild(managedDir, pending.targetRelease, 'pending target release')
620
+ if (pending.previousRelease !== null && typeof pending.previousRelease !== 'string') {
621
+ throw new ManagedRuntimeUpdateError(
622
+ 'invalid_runtime_state',
623
+ 'pending previous release is invalid'
624
+ )
625
+ }
626
+ if (pending.previousRelease) {
627
+ assertManagedChild(managedDir, pending.previousRelease, 'pending previous release')
628
+ }
629
+
630
+ const targetReceipt = join(pending.targetRelease, 'deployment-receipt.json')
631
+ if (existsSync(targetReceipt)) {
632
+ clearPendingTransaction(managedDir)
633
+ return false
634
+ }
635
+
636
+ if (pending.previousRelease) {
637
+ const previousReceiptPath = join(pending.previousRelease, 'deployment-receipt.json')
638
+ const receipt = parseJson(
639
+ readFileSync(previousReceiptPath, 'utf8'),
640
+ 'previous deployment receipt'
641
+ )
642
+ verifyReleaseReceipt(pending.previousRelease, receipt)
643
+ activateRelease(managedDir, pending.previousRelease)
644
+ } else {
645
+ removeCurrentIfTarget(managedDir, pending.targetRelease)
646
+ }
647
+ clearPendingTransaction(managedDir)
648
+ structuredLog(
649
+ 'interrupted_activation_recovered',
650
+ {
651
+ correlationId: pending.correlationId || null,
652
+ restoredPrevious: Boolean(pending.previousRelease),
653
+ },
654
+ logger
655
+ )
656
+ return true
657
+ }
658
+
659
+ function runCommerceInstall(releaseDir, env, execFile, timeout) {
660
+ try {
661
+ execFile(visaBinary(releaseDir), commerceSkillArgs(env), {
662
+ stdio: ['ignore', 'pipe', 'pipe'],
663
+ timeout,
664
+ })
665
+ } catch (error) {
666
+ throw new ManagedRuntimeUpdateError(
667
+ 'rebuild_or_remount_required',
668
+ 'the managed runtime could not refresh its installer-owned commerce mount; rebuild or remount it',
669
+ { cause: error }
670
+ )
671
+ }
672
+ }
673
+
674
+ function certifyLocalBinary(releaseDir, execFile, timeout) {
675
+ const rawCapabilities = execFile(visaBinary(releaseDir), ['capabilities', '--format', 'json'], {
676
+ encoding: 'utf8',
677
+ stdio: ['ignore', 'pipe', 'pipe'],
678
+ timeout,
679
+ })
680
+ const capabilities = parseJson(rawCapabilities, 'visa capabilities')
681
+ const build = capabilities?.data?.build ?? capabilities?.build
682
+ if (typeof build?.version !== 'string' || typeof build?.targetEnvironment !== 'string') {
683
+ throw new ManagedRuntimeUpdateError(
684
+ 'local_readiness_failed',
685
+ 'local capabilities omitted build identity'
686
+ )
687
+ }
688
+
689
+ return {
690
+ version: build.version,
691
+ targetEnvironment: build.targetEnvironment,
692
+ minCompatibleVersion: build.minCompatibleVersion || null,
693
+ }
694
+ }
695
+
696
+ function certifyLocalCommerce(releaseDir, env, execFile, timeout) {
697
+ let rawCommerce
698
+ try {
699
+ rawCommerce = execFile(visaBinary(releaseDir), commerceSkillArgs(env, true), {
700
+ encoding: 'utf8',
701
+ stdio: ['ignore', 'pipe', 'pipe'],
702
+ timeout,
703
+ })
704
+ } catch (error) {
705
+ throw new ManagedRuntimeUpdateError(
706
+ 'commerce_runtime_not_ready',
707
+ 'local Shopify UCP runtime readiness command failed',
708
+ { cause: error }
709
+ )
710
+ }
711
+ const commerce = parseJson(rawCommerce, 'commerce readiness')
712
+ if (
713
+ commerce?.localBundleReady !== true ||
714
+ commerce?.shopifyRuntime?.status !== 'ready' ||
715
+ typeof commerce?.shopifyRuntime?.toolSchemaFingerprint !== 'string'
716
+ ) {
717
+ throw new ManagedRuntimeUpdateError(
718
+ 'commerce_runtime_not_ready',
719
+ 'local Shopify UCP runtime did not certify its required tools'
720
+ )
721
+ }
722
+ return commerce.shopifyRuntime
723
+ }
724
+
725
+ function assessLocalCommerce(releaseDir, env, execFile, timeout) {
726
+ try {
727
+ runCommerceInstall(releaseDir, env, execFile, timeout)
728
+ return {
729
+ commerceReady: true,
730
+ commerceActionRequired: null,
731
+ shopifyRuntime: certifyLocalCommerce(releaseDir, env, execFile, timeout),
732
+ }
733
+ } catch (error) {
734
+ const code =
735
+ error instanceof ManagedRuntimeUpdateError && error.code === 'rebuild_or_remount_required'
736
+ ? 'rebuild_or_remount_required'
737
+ : 'commerce_runtime_not_ready'
738
+ return {
739
+ commerceReady: false,
740
+ commerceActionRequired: code,
741
+ shopifyRuntime: null,
742
+ }
743
+ }
744
+ }
745
+
746
+ function assessLoadedCommerce(loaded, localCommerce) {
747
+ if (!localCommerce.commerceReady) return localCommerce
748
+ try {
749
+ assertSameShopifyRuntime(loaded.shopifyRuntime, localCommerce.shopifyRuntime)
750
+ return localCommerce
751
+ } catch {
752
+ return {
753
+ commerceReady: false,
754
+ commerceActionRequired: 'commerce_runtime_not_ready',
755
+ shopifyRuntime: null,
756
+ }
757
+ }
758
+ }
759
+
760
+ function restartSupervisor(config, dependencies, operation = 'restart') {
761
+ const execShell = dependencies.execSync ?? execSync
762
+ const startedAt = Date.now()
763
+ structuredLog(
764
+ 'supervisor_request',
765
+ { correlationId: config.correlationId, operation },
766
+ dependencies.logger
767
+ )
768
+ try {
769
+ execShell(config.restartCommand, {
770
+ stdio: ['ignore', 'pipe', 'pipe'],
771
+ timeout: config.commandTimeoutMs,
772
+ })
773
+ } catch (error) {
774
+ structuredLog(
775
+ 'supervisor_response',
776
+ {
777
+ correlationId: config.correlationId,
778
+ operation,
779
+ ok: false,
780
+ durationMs: Date.now() - startedAt,
781
+ },
782
+ dependencies.logger
783
+ )
784
+ throw new ManagedRuntimeUpdateError('restart_failed', 'runtime supervisor restart failed', {
785
+ cause: error,
786
+ })
787
+ }
788
+ structuredLog(
789
+ 'supervisor_response',
790
+ {
791
+ correlationId: config.correlationId,
792
+ operation,
793
+ ok: true,
794
+ durationMs: Date.now() - startedAt,
795
+ },
796
+ dependencies.logger
797
+ )
798
+ }
799
+
800
+ function restartAndReadLoaded(config, dependencies) {
801
+ const execShell = dependencies.execSync ?? execSync
802
+ restartSupervisor(config, dependencies)
803
+ let raw
804
+ const startedAt = Date.now()
805
+ structuredLog(
806
+ 'supervisor_request',
807
+ { correlationId: config.correlationId, operation: 'loaded_readiness' },
808
+ dependencies.logger
809
+ )
810
+ try {
811
+ raw = execShell(config.readinessCommand, runtimeReadinessExecOptions(config.readinessTimeoutMs))
812
+ } catch (error) {
813
+ structuredLog(
814
+ 'supervisor_response',
815
+ {
816
+ correlationId: config.correlationId,
817
+ operation: 'loaded_readiness',
818
+ ok: false,
819
+ durationMs: Date.now() - startedAt,
820
+ },
821
+ dependencies.logger
822
+ )
823
+ throw new ManagedRuntimeUpdateError(
824
+ 'loaded_readiness_failed',
825
+ 'loaded runtime readiness command failed or timed out',
826
+ { cause: error }
827
+ )
828
+ }
829
+ structuredLog(
830
+ 'supervisor_response',
831
+ {
832
+ correlationId: config.correlationId,
833
+ operation: 'loaded_readiness',
834
+ ok: true,
835
+ durationMs: Date.now() - startedAt,
836
+ },
837
+ dependencies.logger
838
+ )
839
+ return parseRuntimeReadiness(raw)
840
+ }
841
+
842
+ export function writeReceipt(releaseDir, receipt, id) {
843
+ const path = join(releaseDir, 'deployment-receipt.json')
844
+ atomicWritePrivateJson(path, receipt, id)
845
+ }
846
+
847
+ function rollbackKnownGood(config, previous, dependencies, correlationId) {
848
+ if (!previous) {
849
+ throw new ManagedRuntimeUpdateError(
850
+ 'rollback_unavailable',
851
+ 'fresh managed runtime had no known-good release to restore'
852
+ )
853
+ }
854
+ let lastError
855
+ for (let attempt = 1; attempt <= MAX_ROLLBACK_ATTEMPTS; attempt += 1) {
856
+ try {
857
+ verifyReleaseReceipt(previous.releaseDir, previous.receipt)
858
+ activateRelease(config.managedDir, previous.releaseDir)
859
+ const loaded = restartAndReadLoaded(config, dependencies)
860
+ assertSameRuntimeBuild(loaded, {
861
+ version: previous.receipt.version,
862
+ targetEnvironment: previous.receipt.targetEnvironment,
863
+ })
864
+ structuredLog(
865
+ 'rollback_succeeded',
866
+ { correlationId, attempt, version: loaded.version },
867
+ dependencies.logger
868
+ )
869
+ return loaded
870
+ } catch (error) {
871
+ lastError = error
872
+ }
873
+ }
874
+ throw new ManagedRuntimeUpdateError(
875
+ 'rollback_failed',
876
+ `known-good runtime could not be restored after ${MAX_ROLLBACK_ATTEMPTS} attempts`,
877
+ { cause: lastError }
878
+ )
879
+ }
880
+
881
+ export async function runManagedRuntimeUpdate(env = process.env, dependencies = {}) {
882
+ const baseConfig = resolveManagedUpdateConfig(env)
883
+ // Prefer a caller-supplied id so a convergence run's discovery logs and this
884
+ // transaction's logs join on one correlation id.
885
+ const correlationId = dependencies.correlationId ?? dependencies.randomUUID?.() ?? randomUUID()
886
+ const config = { ...baseConfig, correlationId }
887
+ const releaseLock = acquireUpdateLock(config.managedDir, correlationId, {
888
+ leaseMs: config.lockLeaseMs,
889
+ })
890
+ const startedAt = Date.now()
891
+ let previous
892
+ let activated = false
893
+ let targetRelease
894
+ try {
895
+ recoverInterruptedActivation(config.managedDir, dependencies.logger)
896
+ previous = readCurrentRelease(config.managedDir)
897
+ if (previous && previous.receipt.targetEnvironment !== config.expectedEnvironment) {
898
+ throw new ManagedRuntimeUpdateError(
899
+ 'target_environment_mismatch',
900
+ `known-good runtime targets ${previous.receipt.targetEnvironment}, expected ${config.expectedEnvironment}`
901
+ )
902
+ }
903
+ if (previous && compareExactRc(config.exactVersion, previous.receipt.version) < 0) {
904
+ throw new ManagedRuntimeUpdateError(
905
+ 'target_rollback_refused',
906
+ `exact target ${config.exactVersion} is older than known-good ${previous.receipt.version}`
907
+ )
908
+ }
909
+ structuredLog(
910
+ 'update_started',
911
+ {
912
+ correlationId,
913
+ targetVersion: config.exactVersion,
914
+ previousVersion: previous?.receipt.version ?? null,
915
+ },
916
+ dependencies.logger
917
+ )
918
+ targetRelease = stageExactRelease(config, dependencies)
919
+ const execFile = dependencies.execFileSync ?? execFileSync
920
+ const local = certifyLocalBinary(targetRelease, execFile, config.commandTimeoutMs)
921
+ if (local.version !== config.exactVersion) {
922
+ throw new ManagedRuntimeUpdateError(
923
+ 'installed_version_mismatch',
924
+ `staged CLI reported ${local.version} instead of ${config.exactVersion}`
925
+ )
926
+ }
927
+ if (local.targetEnvironment !== config.expectedEnvironment) {
928
+ throw new ManagedRuntimeUpdateError(
929
+ 'target_environment_mismatch',
930
+ `staged CLI targets ${local.targetEnvironment}, expected ${config.expectedEnvironment}`
931
+ )
932
+ }
933
+ const localCommerce = assessLocalCommerce(targetRelease, env, execFile, config.commandTimeoutMs)
934
+
935
+ const releaseIntegrity = releaseTreeIntegrity(targetRelease)
936
+ writePendingTransaction(config, targetRelease, previous, correlationId)
937
+ activateRelease(config.managedDir, targetRelease)
938
+ activated = true
939
+ const loaded = restartAndReadLoaded(config, dependencies)
940
+ assertSameRuntimeBuild(loaded, local)
941
+ const commerce = assessLoadedCommerce(loaded, localCommerce)
942
+ if (!commerce.commerceReady) {
943
+ structuredLog(
944
+ 'commerce_action_required',
945
+ {
946
+ correlationId,
947
+ version: local.version,
948
+ code: commerce.commerceActionRequired,
949
+ checkoutBlocked: true,
950
+ },
951
+ dependencies.logger
952
+ )
953
+ }
954
+
955
+ const receipt = {
956
+ ok: true,
957
+ binaryReady: true,
958
+ correlationId,
959
+ version: local.version,
960
+ targetEnvironment: local.targetEnvironment,
961
+ minCompatibleVersion: local.minCompatibleVersion,
962
+ releaseDir: targetRelease,
963
+ previousVersion: previous?.receipt.version ?? null,
964
+ releaseIntegrity,
965
+ commerceReady: commerce.commerceReady,
966
+ checkoutBlocked: !commerce.commerceReady,
967
+ commerceActionRequired: commerce.commerceActionRequired,
968
+ shopifyRuntime: commerce.commerceReady
969
+ ? {
970
+ serverName: 'shopify-ucp',
971
+ installedVersion: commerce.shopifyRuntime.installedVersion,
972
+ toolSchemaFingerprint: commerce.shopifyRuntime.toolSchemaFingerprint,
973
+ command: commerce.shopifyRuntime.command,
974
+ args: commerce.shopifyRuntime.args,
975
+ blockedTools: commerce.shopifyRuntime.blockedTools,
976
+ }
977
+ : null,
978
+ durationMs: Date.now() - startedAt,
979
+ }
980
+ try {
981
+ ;(dependencies.writeReceipt ?? writeReceipt)(targetRelease, receipt, correlationId)
982
+ } catch (error) {
983
+ throw new ManagedRuntimeUpdateError(
984
+ 'receipt_write_failed',
985
+ 'loaded runtime passed health but its durable deployment receipt could not be written',
986
+ { cause: error }
987
+ )
988
+ }
989
+ clearPendingTransaction(config.managedDir)
990
+ structuredLog(
991
+ 'update_succeeded',
992
+ {
993
+ correlationId,
994
+ version: receipt.version,
995
+ binaryReady: receipt.binaryReady,
996
+ targetEnvironment: receipt.targetEnvironment,
997
+ releaseIntegrity: receipt.releaseIntegrity,
998
+ commerceReady: receipt.commerceReady,
999
+ checkoutBlocked: receipt.checkoutBlocked,
1000
+ commerceActionRequired: receipt.commerceActionRequired,
1001
+ durationMs: receipt.durationMs,
1002
+ },
1003
+ dependencies.logger
1004
+ )
1005
+ return {
1006
+ ok: true,
1007
+ binaryReady: true,
1008
+ correlationId,
1009
+ version: receipt.version,
1010
+ targetEnvironment: receipt.targetEnvironment,
1011
+ minCompatibleVersion: receipt.minCompatibleVersion,
1012
+ previousVersion: receipt.previousVersion,
1013
+ releaseIntegrity: receipt.releaseIntegrity,
1014
+ commerceReady: receipt.commerceReady,
1015
+ checkoutBlocked: receipt.checkoutBlocked,
1016
+ commerceActionRequired: receipt.commerceActionRequired,
1017
+ durationMs: receipt.durationMs,
1018
+ shopifyRuntime: receipt.shopifyRuntime
1019
+ ? {
1020
+ serverName: receipt.shopifyRuntime.serverName,
1021
+ installedVersion: receipt.shopifyRuntime.installedVersion,
1022
+ toolSchemaFingerprint: receipt.shopifyRuntime.toolSchemaFingerprint,
1023
+ blockedTools: receipt.shopifyRuntime.blockedTools,
1024
+ }
1025
+ : null,
1026
+ }
1027
+ } catch (error) {
1028
+ const updateError =
1029
+ error instanceof ManagedRuntimeUpdateError
1030
+ ? error
1031
+ : new ManagedRuntimeUpdateError('loaded_readiness_mismatch', error.message, {
1032
+ cause: error,
1033
+ })
1034
+ structuredLog(
1035
+ 'update_failed',
1036
+ { correlationId, code: updateError.code, activated },
1037
+ dependencies.logger
1038
+ )
1039
+ if (activated) {
1040
+ try {
1041
+ if (previous) {
1042
+ rollbackKnownGood(config, previous, dependencies, correlationId)
1043
+ } else if (targetRelease) {
1044
+ removeCurrentIfTarget(config.managedDir, targetRelease)
1045
+ // A successful first activation has no managed known-good release to
1046
+ // restore. Restart after removing its pointer so the supervisor
1047
+ // cannot keep serving the now-uncommitted target process while
1048
+ // `current` says that no managed release is active.
1049
+ restartSupervisor(config, dependencies, 'deactivation_restart')
1050
+ }
1051
+ clearPendingTransaction(config.managedDir)
1052
+ } catch (rollbackError) {
1053
+ throw new ManagedRuntimeUpdateError(
1054
+ 'rollback_failed',
1055
+ `update failed with ${updateError.code}; the prior runtime could not be restored`,
1056
+ { cause: new AggregateError([updateError, rollbackError]) }
1057
+ )
1058
+ }
1059
+ }
1060
+ throw updateError
1061
+ } finally {
1062
+ releaseLock()
1063
+ }
1064
+ }
1065
+
1066
+ async function main() {
1067
+ try {
1068
+ const result = await runManagedRuntimeUpdate(process.env)
1069
+ process.stdout.write(`${JSON.stringify(result)}\n`)
1070
+ } catch (error) {
1071
+ const code = error instanceof ManagedRuntimeUpdateError ? error.code : 'update_failed'
1072
+ process.stderr.write(`${JSON.stringify({ ok: false, code, message: error.message })}\n`)
1073
+ process.exitCode = 1
1074
+ }
1075
+ }
1076
+
1077
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
1078
+ void main()
1079
+ }