@mysten-incubation/devstack 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-integrations/vite/index.mjs +1 -0
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +24 -6
- package/dist/cli/wirings/apply.mjs.map +1 -1
- package/dist/cli/wirings/dump-deployment.mjs +2 -1
- package/dist/cli/wirings/dump-deployment.mjs.map +1 -1
- package/dist/cli/wirings/identity.mjs +2 -1
- package/dist/cli/wirings/identity.mjs.map +1 -1
- package/dist/cli/wirings/snapshot.mjs +2 -1
- package/dist/cli/wirings/snapshot.mjs.map +1 -1
- package/dist/cli/wirings/up.mjs +25 -29
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs +52 -10
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/plugins/package/codegen.mjs +4 -4
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/dep-resolution.mjs +19 -9
- package/dist/plugins/package/dep-resolution.mjs.map +1 -1
- package/dist/plugins/package/index.d.mts +3 -3
- package/dist/plugins/package/index.mjs +9 -5
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/package/mode-known.mjs +2 -2
- package/dist/plugins/package/mode-known.mjs.map +1 -1
- package/dist/plugins/package/mode-local.mjs +4 -4
- package/dist/plugins/package/mode-local.mjs.map +1 -1
- package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/plugins/sui/move/index.mjs +10 -6
- package/dist/plugins/sui/move/index.mjs.map +1 -1
- package/dist/plugins/sui/routable.mjs +3 -3
- package/dist/plugins/sui/routable.mjs.map +1 -1
- package/dist/plugins/wallet/codegen.d.mts +14 -10
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/origin-policy.mjs +43 -3
- package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
- package/dist/plugins/wallet/service.mjs +9 -7
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/substrate/cross-process.mjs +2 -1
- package/dist/substrate/cross-process.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/roster.mjs +5 -2
- package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
- package/dist/surfaces/cli/commands/supervisor-presence.mjs +4 -2
- package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
- package/dist/surfaces/cli/errors.mjs +12 -2
- package/dist/surfaces/cli/errors.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/cli/wirings/snapshot.ts"],"sourcesContent":["// `devstack snapshot save / restore / delete` verb wirings.\n//\n// `save` is live-aware: when a supervisor owns the stack, publish a\n// `snapshot.capture` command and read the structured result from the\n// command-channel reply payload (no tail-fiber). `restore` and\n// `delete` are direct/offline only; they refuse to run when a\n// supervisor is live.\n\nimport { dirname } from 'node:path';\n\nimport { Effect, Exit, FileSystem, Logger } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport { mintRandomSuffix } from '../../substrate/runtime/random-suffix.ts';\nimport {\n\tcommandChannelPaths,\n\tmakeCommandChannelPublisher,\n\tmakeProjectionRef,\n\ttype SupervisedStack,\n} from '../../substrate/runtime/index.ts';\nimport { superviseStackWithProductionBoot } from '../../orchestrators/boot.ts';\nimport {\n\tcomputeSnapshotGraphInputFromStack,\n\tSnapshotOrchestratorService,\n\ttype SnapshotMetadata,\n} from '../../orchestrators/snapshot/index.ts';\nimport { CliInternalError, CliUnavailableError } from '../../surfaces/cli/index.ts';\nimport { probeSupervisorPresence } from '../../surfaces/cli/commands/index.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { makeConfigLoader, resolveConfigPath } from './config-loader.ts';\nimport {\n\tensureNoLiveSupervisor,\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { buildDirectSnapshotLayers, buildVerbLayers } from '../../orchestrators/layers.ts';\nimport { provideFileSystem } from './provide-file-system.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst LIVE_SNAPSHOT_CAPTURE_TIMEOUT_MILLIS = 60 * 60 * 1000;\n\nconst mintCliSnapshotId = (): string => `snap-${Date.now()}-${mintRandomSuffix(8)}`;\n\n/** Structured payload that the supervisor's command-channel bridge\n * attaches to the ack/error reply for `snapshot.capture`. Mirrors the\n * shape produced by `cli/wirings/up.ts:snapshotCaptureAckFromEvent`. */\ninterface SnapshotCaptureAckPayload {\n\treadonly kind: 'captured' | 'failed' | 'skipped';\n\treadonly snapshotId?: string;\n\treadonly name?: string;\n\treadonly summary?: string;\n\treadonly reason?: string;\n}\n\nconst isCapturePayload = (value: unknown): value is SnapshotCaptureAckPayload => {\n\tif (typeof value !== 'object' || value === null) return false;\n\tconst kind = (value as { readonly kind?: unknown }).kind;\n\treturn kind === 'captured' || kind === 'failed' || kind === 'skipped';\n};\n\n/** Publish `snapshot.capture` to a live supervisor and surface its\n * structured ack/error payload. The reply payload carries the\n * captured metadata (success) or failure summary (failure) — no\n * side-channel event tail required. Returns `null` if no supervisor\n * is live so the caller can fall through to the direct path. */\nexport const runSnapshotCaptureAgainstLiveSupervisor = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string } | null, unknown> =>\n\tEffect.gen(function* () {\n\t\tconst presence = yield* probeSupervisorPresence(identity.rosterFile).pipe(\n\t\t\tEffect.catch(() => Effect.succeed({ live: false, pid: null, hostname: null })),\n\t\t);\n\t\tif (!presence.live) return null;\n\n\t\tconst snapshotId = args.snapshotId ?? mintCliSnapshotId();\n\t\treturn yield* Effect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst publisher = yield* makeCommandChannelPublisher(\n\t\t\t\t\tcommandChannelPaths(identity.stackRoot),\n\t\t\t\t);\n\t\t\t\tconst published = yield* publisher.publish({\n\t\t\t\t\ttag: 'snapshot.capture',\n\t\t\t\t\tsnapshotId,\n\t\t\t\t\t...(args.name === undefined ? {} : { name: args.name }),\n\t\t\t\t});\n\t\t\t\tconst reply = yield* publisher.awaitCompletion(published, {\n\t\t\t\t\ttimeoutMillis: LIVE_SNAPSHOT_CAPTURE_TIMEOUT_MILLIS,\n\t\t\t\t});\n\n\t\t\t\tif (reply.ok) {\n\t\t\t\t\tconst payload = isCapturePayload(reply.payload) ? reply.payload : null;\n\t\t\t\t\tif (payload?.kind === 'captured' && typeof payload.snapshotId === 'string') {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tsnapshotId: payload.snapshotId,\n\t\t\t\t\t\t\tname: payload.name ?? args.name ?? payload.snapshotId,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\t// Older supervisor without structured payload (or a peer mock\n\t\t\t\t\t// in tests): synthesize from the request — the supervisor\n\t\t\t\t\t// already accepted our snapshotId, so it's authoritative.\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsnapshotId,\n\t\t\t\t\t\tname: args.name ?? snapshotId,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tconst payload = isCapturePayload(reply.payload) ? reply.payload : null;\n\t\t\t\tif (payload?.kind === 'skipped') {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\t\tservice: 'snapshot capture',\n\t\t\t\t\t\t\tmessage: 'another snapshot capture is already running',\n\t\t\t\t\t\t\thint: 'wait for the current snapshot to finish and try again',\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (payload?.kind === 'failed') {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\tmessage: `snapshot capture failed: ${payload.summary ?? reply.message}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// No structured payload — surface the raw reply.message.\n\t\t\t\tif (/already running/i.test(reply.message)) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\t\tservice: 'snapshot capture',\n\t\t\t\t\t\t\tmessage: 'another snapshot capture is already running',\n\t\t\t\t\t\t\thint: 'wait for the current snapshot to finish and try again',\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\tservice: 'devstack supervisor',\n\t\t\t\t\t\tmessage: reply.message,\n\t\t\t\t\t\thint: 'check the attached `devstack up` session and try again',\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\t});\n\nexport const runSnapshotRestoreDirect = (\n\tidentity: ResolvedIdentity,\n\tsnapshotId: string,\n): Effect.Effect<void, unknown> => {\n\tconst program = Effect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t// Offline restore: there is NO live supervisor (ensured below), so no\n\t\t// plugin contributes a live identity slice. Call `restore` with no\n\t\t// `participants` — `runRestore` reads this as \"no live stack\" and\n\t\t// skips ONLY the cross-plugin contribution guard (the snapshot's own\n\t\t// recorded identity has nothing live to disagree with). The runtime\n\t\t// guard (app/stack/network) and the snapshot-side emptiness refusal\n\t\t// still fire. This is the same empty-participants startup restore\n\t\t// contract interrupted-restore recovery uses.\n\t\tyield* provideFileSystem(fs, snapshot.restore({ id: snapshotId }));\n\t});\n\tconst restored = program.pipe(\n\t\tEffect.provide(\n\t\t\tbuildDirectSnapshotLayers({\n\t\t\t\tidentity: identityValueFor(identity),\n\t\t\t\truntimeRoot: identity.runtimeRoot,\n\t\t\t}),\n\t\t),\n\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t);\n\treturn ensureNoLiveSupervisor(\n\t\tidentity,\n\t\t'shut down the attached `devstack up` session before restoring a snapshot',\n\t).pipe(Effect.andThen(restored));\n};\n\nexport const runSnapshotDeleteDirect = (\n\tidentity: ResolvedIdentity,\n\tsnapshotId: string,\n): Effect.Effect<void, unknown> => {\n\tconst program = Effect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tyield* provideFileSystem(fs, snapshot.delete(snapshotId));\n\t});\n\treturn program.pipe(\n\t\tEffect.provide(\n\t\t\tbuildDirectSnapshotLayers({\n\t\t\t\tidentity: identityValueFor(identity),\n\t\t\t\truntimeRoot: identity.runtimeRoot,\n\t\t\t}),\n\t\t),\n\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t);\n};\n\nconst runSnapshotCaptureDirect = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string; readonly configPath?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\tconst loaded = yield* loader.load(args.configPath);\n\t\treturn yield* runSnapshotCaptureDirectLoaded(identity, loaded, args);\n\t});\n};\n\nexport const runSnapshotCaptureDirectLoaded = (\n\tidentity: ResolvedIdentity,\n\tloaded: LoadedConfig,\n\targs: { readonly snapshotId?: string; readonly name?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> =>\n\tEffect.gen(function* () {\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\t\tconst substrateLayers = buildVerbLayers({\n\t\t\tidentity: identityValue,\n\t\t\tstack,\n\t\t\tappRoot,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t});\n\n\t\tconst program = Effect.gen(function* () {\n\t\t\tconst state = yield* makeProjectionRef();\n\t\t\tlet captureExit: Exit.Exit<void, unknown> = Exit.succeed(undefined);\n\t\t\tconst capturedMeta: { current: SnapshotMetadata | null } = { current: null };\n\t\t\t// The contribution dispatcher + post-acquire hook + built-in\n\t\t\t// plugin-context extension are assembled in ONE place\n\t\t\t// (`orchestrators/boot.ts superviseStackWithProductionBoot`); this\n\t\t\t// one-shot verb supplies only the per-caller inputs (`extras`,\n\t\t\t// `lifetime: 'one-shot'`, and the capture `withinScope` hook).\n\t\t\t// Result semantics are unchanged: the hook still tees the capture\n\t\t\t// `Exit` + metadata into the closure cells read below.\n\t\t\tyield* superviseStackWithProductionBoot(\n\t\t\t\t{ _tag: 'Stack', members: stack.members, options: stack.options },\n\t\t\t\tidentityValue,\n\t\t\t\tstate,\n\t\t\t\t{\n\t\t\t\t\textras: stack.options.extras,\n\t\t\t\t\tlifetime: 'one-shot',\n\t\t\t\t\t// Offline one-shot capture: the bounce gather → stop → commit →\n\t\t\t\t\t// retag → hard-rm runs here; NO `resume` is injected because\n\t\t\t\t\t// this supervise scope is `one-shot` (it closes right after) —\n\t\t\t\t\t// the NEXT boot is the resume. Direct call to the orchestrator\n\t\t\t\t\t// (the former `captureSnapshot` command-primitive is gone).\n\t\t\t\t\twithinScope: () =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\t\t\t\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t\t\t\t\t\tconst graphInput = yield* computeSnapshotGraphInputFromStack({\n\t\t\t\t\t\t\t\tstack,\n\t\t\t\t\t\t\t\tdevstackVersion: readDevstackVersion({ fallback: '0.0.0' }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\treturn yield* snapshot\n\t\t\t\t\t\t\t\t.capture({\n\t\t\t\t\t\t\t\t\t...(args.snapshotId === undefined ? {} : { id: args.snapshotId }),\n\t\t\t\t\t\t\t\t\t...(args.name === undefined ? {} : { label: args.name }),\n\t\t\t\t\t\t\t\t\tgraphInput,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.pipe(Effect.provideService(FileSystem.FileSystem, fs));\n\t\t\t\t\t\t}).pipe(\n\t\t\t\t\t\t\tEffect.tap((meta) =>\n\t\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\t\tcapturedMeta.current = meta;\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\tEffect.exit,\n\t\t\t\t\t\t\tEffect.tap((exit) =>\n\t\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\t\tcaptureExit = exit;\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (Exit.isFailure(captureExit)) {\n\t\t\t\tyield* Effect.failCause(captureExit.cause);\n\t\t\t}\n\t\t\tif (capturedMeta.current === null) {\n\t\t\t\treturn yield* Effect.die('snapshot capture completed without metadata');\n\t\t\t}\n\t\t\tconst meta = capturedMeta.current;\n\t\t\treturn { snapshotId: meta.id, name: meta.label ?? meta.id };\n\t\t});\n\n\t\treturn yield* ensureNoLiveSupervisor(\n\t\t\teffectiveIdentity,\n\t\t\t'shut down the attached `devstack up` session before saving a snapshot',\n\t\t).pipe(\n\t\t\tEffect.andThen(\n\t\t\t\tprogram.pipe(\n\t\t\t\t\tEffect.provide(substrateLayers),\n\t\t\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t});\n\nexport const runSnapshotCaptureLiveAware = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string; readonly configPath?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> =>\n\tEffect.gen(function* () {\n\t\tif (args.configPath === undefined && resolveConfigPath(undefined) === null) {\n\t\t\tconst live = yield* runSnapshotCaptureAgainstLiveSupervisor(identity, args);\n\t\t\tif (live !== null) return live;\n\t\t\treturn yield* runSnapshotCaptureDirect(identity, args);\n\t\t}\n\t\tconst loaded = yield* makeConfigLoader().load(args.configPath);\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst live = yield* runSnapshotCaptureAgainstLiveSupervisor(effectiveIdentity, args);\n\t\tif (live !== null) return live;\n\t\treturn yield* runSnapshotCaptureDirectLoaded(effectiveIdentity, loaded, args);\n\t});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/cli/wirings/snapshot.ts"],"sourcesContent":["// `devstack snapshot save / restore / delete` verb wirings.\n//\n// `save` is live-aware: when a supervisor owns the stack, publish a\n// `snapshot.capture` command and read the structured result from the\n// command-channel reply payload (no tail-fiber). `restore` and\n// `delete` are direct/offline only; they refuse to run when a\n// supervisor is live.\n\nimport { dirname } from 'node:path';\n\nimport { Effect, Exit, FileSystem, Logger } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport { mintRandomSuffix } from '../../substrate/runtime/random-suffix.ts';\nimport {\n\tcommandChannelPaths,\n\tmakeCommandChannelPublisher,\n\tmakeProjectionRef,\n\ttype SupervisedStack,\n} from '../../substrate/runtime/index.ts';\nimport { superviseStackWithProductionBoot } from '../../orchestrators/boot.ts';\nimport {\n\tcomputeSnapshotGraphInputFromStack,\n\tSnapshotOrchestratorService,\n\ttype SnapshotMetadata,\n} from '../../orchestrators/snapshot/index.ts';\nimport { CliInternalError, CliUnavailableError } from '../../surfaces/cli/index.ts';\nimport { probeSupervisorPresence } from '../../surfaces/cli/commands/index.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { makeConfigLoader, resolveConfigPath } from './config-loader.ts';\nimport {\n\tensureNoLiveSupervisor,\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { buildDirectSnapshotLayers, buildVerbLayers } from '../../orchestrators/layers.ts';\nimport { provideFileSystem } from './provide-file-system.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst LIVE_SNAPSHOT_CAPTURE_TIMEOUT_MILLIS = 60 * 60 * 1000;\n\nconst mintCliSnapshotId = (): string => `snap-${Date.now()}-${mintRandomSuffix(8)}`;\n\n/** Structured payload that the supervisor's command-channel bridge\n * attaches to the ack/error reply for `snapshot.capture`. Mirrors the\n * shape produced by `cli/wirings/up.ts:snapshotCaptureAckFromEvent`. */\ninterface SnapshotCaptureAckPayload {\n\treadonly kind: 'captured' | 'failed' | 'skipped';\n\treadonly snapshotId?: string;\n\treadonly name?: string;\n\treadonly summary?: string;\n\treadonly reason?: string;\n}\n\nconst isCapturePayload = (value: unknown): value is SnapshotCaptureAckPayload => {\n\tif (typeof value !== 'object' || value === null) return false;\n\tconst kind = (value as { readonly kind?: unknown }).kind;\n\treturn kind === 'captured' || kind === 'failed' || kind === 'skipped';\n};\n\n/** Publish `snapshot.capture` to a live supervisor and surface its\n * structured ack/error payload. The reply payload carries the\n * captured metadata (success) or failure summary (failure) — no\n * side-channel event tail required. Returns `null` if no supervisor\n * is live so the caller can fall through to the direct path. */\nexport const runSnapshotCaptureAgainstLiveSupervisor = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string } | null, unknown> =>\n\tEffect.gen(function* () {\n\t\tconst presence = yield* probeSupervisorPresence(identity.rosterFile).pipe(\n\t\t\tEffect.catch(() =>\n\t\t\t\tEffect.succeed({ live: false, pid: null, hostname: null, graphInputId: null }),\n\t\t\t),\n\t\t);\n\t\tif (!presence.live) return null;\n\n\t\tconst snapshotId = args.snapshotId ?? mintCliSnapshotId();\n\t\treturn yield* Effect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst publisher = yield* makeCommandChannelPublisher(\n\t\t\t\t\tcommandChannelPaths(identity.stackRoot),\n\t\t\t\t);\n\t\t\t\tconst published = yield* publisher.publish({\n\t\t\t\t\ttag: 'snapshot.capture',\n\t\t\t\t\tsnapshotId,\n\t\t\t\t\t...(args.name === undefined ? {} : { name: args.name }),\n\t\t\t\t});\n\t\t\t\tconst reply = yield* publisher.awaitCompletion(published, {\n\t\t\t\t\ttimeoutMillis: LIVE_SNAPSHOT_CAPTURE_TIMEOUT_MILLIS,\n\t\t\t\t});\n\n\t\t\t\tif (reply.ok) {\n\t\t\t\t\tconst payload = isCapturePayload(reply.payload) ? reply.payload : null;\n\t\t\t\t\tif (payload?.kind === 'captured' && typeof payload.snapshotId === 'string') {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tsnapshotId: payload.snapshotId,\n\t\t\t\t\t\t\tname: payload.name ?? args.name ?? payload.snapshotId,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\t// Older supervisor without structured payload (or a peer mock\n\t\t\t\t\t// in tests): synthesize from the request — the supervisor\n\t\t\t\t\t// already accepted our snapshotId, so it's authoritative.\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsnapshotId,\n\t\t\t\t\t\tname: args.name ?? snapshotId,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tconst payload = isCapturePayload(reply.payload) ? reply.payload : null;\n\t\t\t\tif (payload?.kind === 'skipped') {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\t\tservice: 'snapshot capture',\n\t\t\t\t\t\t\tmessage: 'another snapshot capture is already running',\n\t\t\t\t\t\t\thint: 'wait for the current snapshot to finish and try again',\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (payload?.kind === 'failed') {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\tmessage: `snapshot capture failed: ${payload.summary ?? reply.message}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// No structured payload — surface the raw reply.message.\n\t\t\t\tif (/already running/i.test(reply.message)) {\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\t\tservice: 'snapshot capture',\n\t\t\t\t\t\t\tmessage: 'another snapshot capture is already running',\n\t\t\t\t\t\t\thint: 'wait for the current snapshot to finish and try again',\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\t\tservice: 'devstack supervisor',\n\t\t\t\t\t\tmessage: reply.message,\n\t\t\t\t\t\thint: 'check the attached `devstack up` session and try again',\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}),\n\t\t);\n\t});\n\nexport const runSnapshotRestoreDirect = (\n\tidentity: ResolvedIdentity,\n\tsnapshotId: string,\n): Effect.Effect<void, unknown> => {\n\tconst program = Effect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t// Offline restore: there is NO live supervisor (ensured below), so no\n\t\t// plugin contributes a live identity slice. Call `restore` with no\n\t\t// `participants` — `runRestore` reads this as \"no live stack\" and\n\t\t// skips ONLY the cross-plugin contribution guard (the snapshot's own\n\t\t// recorded identity has nothing live to disagree with). The runtime\n\t\t// guard (app/stack/network) and the snapshot-side emptiness refusal\n\t\t// still fire. This is the same empty-participants startup restore\n\t\t// contract interrupted-restore recovery uses.\n\t\tyield* provideFileSystem(fs, snapshot.restore({ id: snapshotId }));\n\t});\n\tconst restored = program.pipe(\n\t\tEffect.provide(\n\t\t\tbuildDirectSnapshotLayers({\n\t\t\t\tidentity: identityValueFor(identity),\n\t\t\t\truntimeRoot: identity.runtimeRoot,\n\t\t\t}),\n\t\t),\n\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t);\n\treturn ensureNoLiveSupervisor(\n\t\tidentity,\n\t\t'shut down the attached `devstack up` session before restoring a snapshot',\n\t).pipe(Effect.andThen(restored));\n};\n\nexport const runSnapshotDeleteDirect = (\n\tidentity: ResolvedIdentity,\n\tsnapshotId: string,\n): Effect.Effect<void, unknown> => {\n\tconst program = Effect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tyield* provideFileSystem(fs, snapshot.delete(snapshotId));\n\t});\n\treturn program.pipe(\n\t\tEffect.provide(\n\t\t\tbuildDirectSnapshotLayers({\n\t\t\t\tidentity: identityValueFor(identity),\n\t\t\t\truntimeRoot: identity.runtimeRoot,\n\t\t\t}),\n\t\t),\n\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t);\n};\n\nconst runSnapshotCaptureDirect = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string; readonly configPath?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\tconst loaded = yield* loader.load(args.configPath);\n\t\treturn yield* runSnapshotCaptureDirectLoaded(identity, loaded, args);\n\t});\n};\n\nexport const runSnapshotCaptureDirectLoaded = (\n\tidentity: ResolvedIdentity,\n\tloaded: LoadedConfig,\n\targs: { readonly snapshotId?: string; readonly name?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> =>\n\tEffect.gen(function* () {\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\t\tconst substrateLayers = buildVerbLayers({\n\t\t\tidentity: identityValue,\n\t\t\tstack,\n\t\t\tappRoot,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t});\n\n\t\tconst program = Effect.gen(function* () {\n\t\t\tconst state = yield* makeProjectionRef();\n\t\t\tlet captureExit: Exit.Exit<void, unknown> = Exit.succeed(undefined);\n\t\t\tconst capturedMeta: { current: SnapshotMetadata | null } = { current: null };\n\t\t\t// The contribution dispatcher + post-acquire hook + built-in\n\t\t\t// plugin-context extension are assembled in ONE place\n\t\t\t// (`orchestrators/boot.ts superviseStackWithProductionBoot`); this\n\t\t\t// one-shot verb supplies only the per-caller inputs (`extras`,\n\t\t\t// `lifetime: 'one-shot'`, and the capture `withinScope` hook).\n\t\t\t// Result semantics are unchanged: the hook still tees the capture\n\t\t\t// `Exit` + metadata into the closure cells read below.\n\t\t\tyield* superviseStackWithProductionBoot(\n\t\t\t\t{ _tag: 'Stack', members: stack.members, options: stack.options },\n\t\t\t\tidentityValue,\n\t\t\t\tstate,\n\t\t\t\t{\n\t\t\t\t\textras: stack.options.extras,\n\t\t\t\t\tlifetime: 'one-shot',\n\t\t\t\t\t// Offline one-shot capture: the bounce gather → stop → commit →\n\t\t\t\t\t// retag → hard-rm runs here; NO `resume` is injected because\n\t\t\t\t\t// this supervise scope is `one-shot` (it closes right after) —\n\t\t\t\t\t// the NEXT boot is the resume. Direct call to the orchestrator\n\t\t\t\t\t// (the former `captureSnapshot` command-primitive is gone).\n\t\t\t\t\twithinScope: () =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\t\t\t\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t\t\t\t\t\tconst graphInput = yield* computeSnapshotGraphInputFromStack({\n\t\t\t\t\t\t\t\tstack,\n\t\t\t\t\t\t\t\tdevstackVersion: readDevstackVersion({ fallback: '0.0.0' }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\treturn yield* snapshot\n\t\t\t\t\t\t\t\t.capture({\n\t\t\t\t\t\t\t\t\t...(args.snapshotId === undefined ? {} : { id: args.snapshotId }),\n\t\t\t\t\t\t\t\t\t...(args.name === undefined ? {} : { label: args.name }),\n\t\t\t\t\t\t\t\t\tgraphInput,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.pipe(Effect.provideService(FileSystem.FileSystem, fs));\n\t\t\t\t\t\t}).pipe(\n\t\t\t\t\t\t\tEffect.tap((meta) =>\n\t\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\t\tcapturedMeta.current = meta;\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\tEffect.exit,\n\t\t\t\t\t\t\tEffect.tap((exit) =>\n\t\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\t\tcaptureExit = exit;\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (Exit.isFailure(captureExit)) {\n\t\t\t\tyield* Effect.failCause(captureExit.cause);\n\t\t\t}\n\t\t\tif (capturedMeta.current === null) {\n\t\t\t\treturn yield* Effect.die('snapshot capture completed without metadata');\n\t\t\t}\n\t\t\tconst meta = capturedMeta.current;\n\t\t\treturn { snapshotId: meta.id, name: meta.label ?? meta.id };\n\t\t});\n\n\t\treturn yield* ensureNoLiveSupervisor(\n\t\t\teffectiveIdentity,\n\t\t\t'shut down the attached `devstack up` session before saving a snapshot',\n\t\t).pipe(\n\t\t\tEffect.andThen(\n\t\t\t\tprogram.pipe(\n\t\t\t\t\tEffect.provide(substrateLayers),\n\t\t\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\t\t),\n\t\t\t),\n\t\t);\n\t});\n\nexport const runSnapshotCaptureLiveAware = (\n\tidentity: ResolvedIdentity,\n\targs: { readonly snapshotId?: string; readonly name?: string; readonly configPath?: string },\n): Effect.Effect<{ readonly snapshotId: string; readonly name: string }, unknown> =>\n\tEffect.gen(function* () {\n\t\tif (args.configPath === undefined && resolveConfigPath(undefined) === null) {\n\t\t\tconst live = yield* runSnapshotCaptureAgainstLiveSupervisor(identity, args);\n\t\t\tif (live !== null) return live;\n\t\t\treturn yield* runSnapshotCaptureDirect(identity, args);\n\t\t}\n\t\tconst loaded = yield* makeConfigLoader().load(args.configPath);\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst live = yield* runSnapshotCaptureAgainstLiveSupervisor(effectiveIdentity, args);\n\t\tif (live !== null) return live;\n\t\treturn yield* runSnapshotCaptureDirectLoaded(effectiveIdentity, loaded, args);\n\t});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,uCAAuC,OAAU;AAEvD,MAAM,0BAAkC,QAAQ,KAAK,IAAI,EAAE,GAAG,iBAAiB,CAAC;AAahF,MAAM,oBAAoB,UAAuD;CAChF,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,OAAQ,MAAsC;CACpD,OAAO,SAAS,cAAc,SAAS,YAAY,SAAS;AAC7D;;;;;;AAOA,MAAa,2CACZ,UACA,SAEA,OAAO,IAAI,aAAa;CAMvB,IAAI,EAAC,OALmB,wBAAwB,SAAS,UAAU,CAAC,CAAC,KACpE,OAAO,YACN,OAAO,QAAQ;EAAE,MAAM;EAAO,KAAK;EAAM,UAAU;EAAM,cAAc;CAAK,CAAC,CAC9E,CACD,EAAA,CACc,MAAM,OAAO;CAE3B,MAAM,aAAa,KAAK,cAAc,kBAAkB;CACxD,OAAO,OAAO,OAAO,OACpB,OAAO,IAAI,aAAa;EACvB,MAAM,YAAY,OAAO,4BACxB,oBAAoB,SAAS,SAAS,CACvC;EACA,MAAM,YAAY,OAAO,UAAU,QAAQ;GAC1C,KAAK;GACL;GACA,GAAI,KAAK,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK;EACtD,CAAC;EACD,MAAM,QAAQ,OAAO,UAAU,gBAAgB,WAAW,EACzD,eAAe,qCAChB,CAAC;EAED,IAAI,MAAM,IAAI;GACb,MAAM,UAAU,iBAAiB,MAAM,OAAO,IAAI,MAAM,UAAU;GAClE,IAAI,SAAS,SAAS,cAAc,OAAO,QAAQ,eAAe,UACjE,OAAO;IACN,YAAY,QAAQ;IACpB,MAAM,QAAQ,QAAQ,KAAK,QAAQ,QAAQ;GAC5C;GAKD,OAAO;IACN;IACA,MAAM,KAAK,QAAQ;GACpB;EACD;EAEA,MAAM,UAAU,iBAAiB,MAAM,OAAO,IAAI,MAAM,UAAU;EAClE,IAAI,SAAS,SAAS,WACrB,OAAO,OAAO,OAAO,KACpB,IAAI,oBAAoB;GACvB,SAAS;GACT,SAAS;GACT,MAAM;EACP,CAAC,CACF;EAED,IAAI,SAAS,SAAS,UACrB,OAAO,OAAO,OAAO,KACpB,IAAI,iBAAiB,EACpB,SAAS,4BAA4B,QAAQ,WAAW,MAAM,UAC/D,CAAC,CACF;EAGD,IAAI,mBAAmB,KAAK,MAAM,OAAO,GACxC,OAAO,OAAO,OAAO,KACpB,IAAI,oBAAoB;GACvB,SAAS;GACT,SAAS;GACT,MAAM;EACP,CAAC,CACF;EAED,OAAO,OAAO,OAAO,KACpB,IAAI,oBAAoB;GACvB,SAAS;GACT,SAAS,MAAM;GACf,MAAM;EACP,CAAC,CACF;CACD,CAAC,CACF;AACD,CAAC;AAEF,MAAa,4BACZ,UACA,eACkC;CAclC,MAAM,WAbU,OAAO,IAAI,aAAa;EACvC,MAAM,WAAW,OAAO;EAUxB,OAAO,kBAAkB,OATP,WAAW,YASA,SAAS,QAAQ,EAAE,IAAI,WAAW,CAAC,CAAC;CAClE,CACuB,CAAC,CAAC,KACxB,OAAO,QACN,0BAA0B;EACzB,UAAU,iBAAiB,QAAQ;EACnC,aAAa,SAAS;CACvB,CAAC,CACF,GACA,OAAO,QAAQ,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CACtD;CACA,OAAO,uBACN,UACA,0EACD,CAAC,CAAC,KAAK,OAAO,QAAQ,QAAQ,CAAC;AAChC;AAEA,MAAa,2BACZ,UACA,eACkC;CAMlC,OALgB,OAAO,IAAI,aAAa;EACvC,MAAM,WAAW,OAAO;EAExB,OAAO,kBAAkB,OADP,WAAW,YACA,SAAS,OAAO,UAAU,CAAC;CACzD,CACa,CAAC,CAAC,KACd,OAAO,QACN,0BAA0B;EACzB,UAAU,iBAAiB,QAAQ;EACnC,aAAa,SAAS;CACvB,CAAC,CACF,GACA,OAAO,QAAQ,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CACtD;AACD;AAEA,MAAM,4BACL,UACA,SACoF;CACpF,MAAM,SAAS,iBAAiB;CAChC,OAAO,OAAO,IAAI,aAAa;EAE9B,OAAO,OAAO,+BAA+B,UAAU,OADjC,OAAO,KAAK,KAAK,UAAU,GACc,IAAI;CACpE,CAAC;AACF;AAEA,MAAa,kCACZ,UACA,QACA,SAEA,OAAO,IAAI,aAAa;CACvB,MAAM,QAAS,OAA+D;CAC9E,MAAM,oBAAoB,yBAAyB,UAAU,KAAK;CAClE,MAAM,gBAA0B,iBAAiB,iBAAiB;CAElE,MAAM,kBAAkB,gBAAgB;EACvC,UAAU;EACV;EACA,SAJe,QAAQ,OAAO,kBAIxB;EACN,aAAa,kBAAkB;CAChC,CAAC;CAED,MAAM,UAAU,OAAO,IAAI,aAAa;EACvC,MAAM,QAAQ,OAAO,kBAAkB;EACvC,IAAI,cAAwC,KAAK,QAAQ,KAAA,CAAS;EAClE,MAAM,eAAqD,EAAE,SAAS,KAAK;EAQ3E,OAAO,iCACN;GAAE,MAAM;GAAS,SAAS,MAAM;GAAS,SAAS,MAAM;EAAQ,GAChE,eACA,OACA;GACC,QAAQ,MAAM,QAAQ;GACtB,UAAU;GAMV,mBACC,OAAO,IAAI,aAAa;IACvB,MAAM,WAAW,OAAO;IACxB,MAAM,KAAK,OAAO,WAAW;IAC7B,MAAM,aAAa,OAAO,mCAAmC;KAC5D;KACA,iBAAiB,oBAAoB,EAAE,UAAU,QAAQ,CAAC;IAC3D,CAAC;IACD,OAAO,OAAO,SACZ,QAAQ;KACR,GAAI,KAAK,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI,KAAK,WAAW;KAC/D,GAAI,KAAK,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,KAAK,KAAK;KACtD;IACD,CAAC,CAAC,CACD,KAAK,OAAO,eAAe,WAAW,YAAY,EAAE,CAAC;GACxD,CAAC,CAAC,CAAC,KACF,OAAO,KAAK,SACX,OAAO,WAAW;IACjB,aAAa,UAAU;GACxB,CAAC,CACF,GACA,OAAO,QACP,OAAO,MACP,OAAO,KAAK,SACX,OAAO,WAAW;IACjB,cAAc;GACf,CAAC,CACF,GACA,OAAO,MACR;EACF,CACD;EACA,IAAI,KAAK,UAAU,WAAW,GAC7B,OAAO,OAAO,UAAU,YAAY,KAAK;EAE1C,IAAI,aAAa,YAAY,MAC5B,OAAO,OAAO,OAAO,IAAI,6CAA6C;EAEvE,MAAM,OAAO,aAAa;EAC1B,OAAO;GAAE,YAAY,KAAK;GAAI,MAAM,KAAK,SAAS,KAAK;EAAG;CAC3D,CAAC;CAED,OAAO,OAAO,uBACb,mBACA,uEACD,CAAC,CAAC,KACD,OAAO,QACN,QAAQ,KACP,OAAO,QAAQ,eAAe,GAC9B,OAAO,QAAQ,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CACtD,CACD,CACD;AACD,CAAC;AAEF,MAAa,+BACZ,UACA,SAEA,OAAO,IAAI,aAAa;CACvB,IAAI,KAAK,eAAe,KAAA,KAAa,kBAAkB,KAAA,CAAS,MAAM,MAAM;EAC3E,MAAM,OAAO,OAAO,wCAAwC,UAAU,IAAI;EAC1E,IAAI,SAAS,MAAM,OAAO;EAC1B,OAAO,OAAO,yBAAyB,UAAU,IAAI;CACtD;CACA,MAAM,SAAS,OAAO,iBAAiB,CAAC,CAAC,KAAK,KAAK,UAAU;CAC7D,MAAM,QAAS,OAA+D;CAC9E,MAAM,oBAAoB,yBAAyB,UAAU,KAAK;CAClE,MAAM,OAAO,OAAO,wCAAwC,mBAAmB,IAAI;CACnF,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,OAAO,+BAA+B,mBAAmB,QAAQ,IAAI;AAC7E,CAAC"}
|
package/dist/cli/wirings/up.mjs
CHANGED
|
@@ -81,7 +81,7 @@ const makeSnapshotCommandHandler = (params) => Effect.gen(function* () {
|
|
|
81
81
|
});
|
|
82
82
|
const installLiveSupervisorRoster = (params) => Effect.gen(function* () {
|
|
83
83
|
const paths = rosterPathsFor(params.stackRoot);
|
|
84
|
-
if (!(yield* claim(paths)).soleHolder) {
|
|
84
|
+
if (!(yield* claim(paths, "normal", void 0, { graphInputId: params.graphInputId })).soleHolder) {
|
|
85
85
|
yield* release(paths).pipe(Effect.catch(() => Effect.void));
|
|
86
86
|
return yield* Effect.fail(new CliSupervisorLiveError({
|
|
87
87
|
app: params.app,
|
|
@@ -122,43 +122,38 @@ const buildUpBootBundle = (input) => {
|
|
|
122
122
|
const snapshot = yield* SnapshotOrchestratorService;
|
|
123
123
|
const fs = yield* FileSystem.FileSystem;
|
|
124
124
|
const stackPaths = yield* StackPathsService;
|
|
125
|
+
const graphInput = yield* computeGraphInput;
|
|
125
126
|
yield* recoverInterruptedRestore({
|
|
126
127
|
liveRoot: stackPaths.stackRoot,
|
|
127
|
-
restoreSnapshot: (id) =>
|
|
128
|
+
restoreSnapshot: (id) => snapshot.restore({
|
|
128
129
|
id,
|
|
129
|
-
currentGraphInput,
|
|
130
|
+
currentGraphInput: graphInput,
|
|
130
131
|
graphInputMismatchPolicy: "warn"
|
|
131
|
-
})
|
|
132
|
+
})
|
|
132
133
|
});
|
|
133
134
|
if (snapshotCache !== void 0) if (snapshotCache.existingSnapshotId === void 0) yield* Effect.logWarning(`snapshot cache ${snapshotCache.name} was not found; boot will refresh it`);
|
|
134
|
-
else {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
yield* provideFileSystem(fs, snapshot.restore({
|
|
139
|
-
id: snapshotCache.existingSnapshotId,
|
|
140
|
-
currentGraphInput: graphInput,
|
|
141
|
-
graphInputMismatchPolicy: "block"
|
|
142
|
-
}));
|
|
143
|
-
} else {
|
|
144
|
-
refreshSnapshotCache = true;
|
|
145
|
-
yield* Effect.logWarning(`snapshot cache ${snapshotCache.name} graph input is stale or unreadable; boot will refresh it`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
else if (startFromSnapshot !== void 0) {
|
|
149
|
-
const graphInput = yield* computeGraphInput;
|
|
150
|
-
if (startFromSnapshot.stalePolicy === "clean-start") if (((yield* provideFileSystem(fs, snapshot.list)).find((snapshotEntry) => snapshotEntry.id === startFromSnapshot.id)?.metadata?.graphInput.graphInputId ?? null) !== graphInput.graphInputId) yield* Effect.logWarning(`snapshot ${startFromSnapshot.id} graph input is stale; clean-start policy skipped restore`);
|
|
151
|
-
else yield* provideFileSystem(fs, snapshot.restore({
|
|
152
|
-
id: startFromSnapshot.id,
|
|
135
|
+
else if (((yield* provideFileSystem(fs, snapshot.list)).find((snapshotEntry) => snapshotEntry.id === snapshotCache.existingSnapshotId)?.metadata?.graphInput.graphInputId ?? null) === graphInput.graphInputId) {
|
|
136
|
+
refreshSnapshotCache = false;
|
|
137
|
+
yield* provideFileSystem(fs, snapshot.restore({
|
|
138
|
+
id: snapshotCache.existingSnapshotId,
|
|
153
139
|
currentGraphInput: graphInput,
|
|
154
140
|
graphInputMismatchPolicy: "block"
|
|
155
141
|
}));
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
graphInputMismatchPolicy: startFromSnapshot.stalePolicy
|
|
160
|
-
}));
|
|
142
|
+
} else {
|
|
143
|
+
refreshSnapshotCache = true;
|
|
144
|
+
yield* Effect.logWarning(`snapshot cache ${snapshotCache.name} graph input is stale or unreadable; boot will refresh it`);
|
|
161
145
|
}
|
|
146
|
+
else if (startFromSnapshot !== void 0) if (startFromSnapshot.stalePolicy === "clean-start") if (((yield* provideFileSystem(fs, snapshot.list)).find((snapshotEntry) => snapshotEntry.id === startFromSnapshot.id)?.metadata?.graphInput.graphInputId ?? null) !== graphInput.graphInputId) yield* Effect.logWarning(`snapshot ${startFromSnapshot.id} graph input is stale; clean-start policy skipped restore`);
|
|
147
|
+
else yield* provideFileSystem(fs, snapshot.restore({
|
|
148
|
+
id: startFromSnapshot.id,
|
|
149
|
+
currentGraphInput: graphInput,
|
|
150
|
+
graphInputMismatchPolicy: "block"
|
|
151
|
+
}));
|
|
152
|
+
else yield* provideFileSystem(fs, snapshot.restore({
|
|
153
|
+
id: startFromSnapshot.id,
|
|
154
|
+
currentGraphInput: graphInput,
|
|
155
|
+
graphInputMismatchPolicy: startFromSnapshot.stalePolicy
|
|
156
|
+
}));
|
|
162
157
|
const commandChannel = yield* installCommandChannelBridge({
|
|
163
158
|
stackRoot: stackPaths.stackRoot,
|
|
164
159
|
handle: h.supervisor
|
|
@@ -166,7 +161,8 @@ const buildUpBootBundle = (input) => {
|
|
|
166
161
|
yield* installLiveSupervisorRoster({
|
|
167
162
|
stackRoot: stackPaths.stackRoot,
|
|
168
163
|
app: String(identityValue.app),
|
|
169
|
-
stack: String(identityValue.stack)
|
|
164
|
+
stack: String(identityValue.stack),
|
|
165
|
+
graphInputId: graphInput.graphInputId
|
|
170
166
|
});
|
|
171
167
|
const rendererEvents = yield* Queue.unbounded();
|
|
172
168
|
const renderer = makeTuiSurface({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"up.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/up.ts"],"sourcesContent":["// `devstack up` verb wiring.\n//\n// Wires the substrate Layer stack, supervisor, attached renderer, and\n// in-process TUI command queue. The Effect runs as the outer Node fiber\n// so SIGINT reaches scope finalizers (see surfaces/cli/index.ts\n// architecture invariant).\n//\n// Logger layer: `Logger.layer([])` (silent). `up` owns the TUI, which is\n// the operator-facing output channel; routing structured `Effect.log*`\n// records to stderr would scribble on top of the live ink renderer in\n// human mode and would duplicate the structured event stream the plain\n// renderer already emits in non-TTY mode. The sibling `apply` verb (no\n// TUI) uses `Logger.consolePretty()` because its consumer is CI.\n\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, FileSystem, Queue, Scope, Stream } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport type { EngineEvent } from '../../substrate/events.ts';\nimport { StackPathsService } from '../../substrate/runtime/paths.ts';\nimport {\n\tclaim,\n\theartbeatFiber,\n\trelease,\n\tstartFileWatcher,\n\ttype SupervisedStack,\n\ttype SupervisorCommandHandler,\n} from '../../substrate/runtime/index.ts';\nimport {\n\tcollectLifecyclePruneInventory,\n\tdefaultLifecyclePruneSelection,\n\tDEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\trunLifecyclePrune,\n} from '../../orchestrators/lifecycle-prune/index.ts';\nimport {\n\tcomputeSnapshotGraphInputFromStack,\n\trecoverInterruptedRestore,\n\tSnapshotOrchestratorService,\n} from '../../orchestrators/snapshot/index.ts';\nimport type { Stack } from '../../api/define-devstack.ts';\nimport {\n\trunStackWithBoot,\n\ttype CommandHandlerFactory,\n\ttype RunStackBootBag,\n} from '../../api/run-stack-internal.ts';\nimport type { BootError } from '../../api/run-stack.ts';\nimport {\n\ttype CliError,\n\tCliInternalError,\n\tCliSnapshotNotFoundError,\n\tCliSupervisorLiveError,\n\tCliUsageError,\n\ttype GlobalFlags,\n} from '../../surfaces/cli/index.ts';\nimport type { CommandResult } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport { makeTuiSurface } from '../../surfaces/tui/index.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeQueueCommandPublisher, resolveUpRendererMode } from '../up-lifecycle.ts';\nimport { makeConfigLoader } from './config-loader.ts';\nimport { makeSnapshotReader } from '../snapshot-reader.ts';\nimport {\n\tfindCliSupervisorLiveError,\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { installCommandChannelBridge } from './up-ipc.ts';\nimport { provideFileSystem } from './provide-file-system.ts';\nimport { deriveContributions } from './codegen.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst rosterPathsFor = (stackRoot: string) => ({\n\tstackLockFile: resolvePath(stackRoot, 'stack.lock'),\n\trosterFile: resolvePath(stackRoot, 'roster.json'),\n});\n\n// -----------------------------------------------------------------------------\n// Supervisor command handler — snapshot/wipe/prune injected dispatch\n// -----------------------------------------------------------------------------\n\n/** Build the snapshot/wipe/prune `commandHandler` as a FACTORY the seam\n * resolves against its LIVE substrate (see `CommandHandlerFactory`). The\n * factory yields the seam's `SnapshotOrchestratorService` + `FileSystem` —\n * the SAME orchestrator instance the supervisor's contribution dispatcher\n * registers live participants on, so an operator `snapshot save` captures\n * real chain/blob/db state, not an empty set — and returns a handler\n * (R = `never`, as the command loop requires) that closes over them\n * directly. No Deferred hand-off: the seam runs this factory before\n * `startSupervisor` consumes the handler. */\nconst makeSnapshotCommandHandler = (params: {\n\treadonly runtimeRoot: string;\n\treadonly stack: SupervisedStack;\n\treadonly devstackVersion: string;\n}): CommandHandlerFactory =>\n\tEffect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst computeGraphInput = computeSnapshotGraphInputFromStack({\n\t\t\tstack: params.stack,\n\t\t\tdevstackVersion: params.devstackVersion,\n\t\t});\n\t\tconst handler: SupervisorCommandHandler = (cmd) => {\n\t\t\tswitch (cmd.tag) {\n\t\t\t\tcase 'snapshot.capture':\n\t\t\t\t\t// The L3 capture is the bounce's gather → stop → commit → retag\n\t\t\t\t\t// → hard-rm half; the RESUME (recreate + wait-write-ready) is the\n\t\t\t\t\t// command-loop's converge after this handler succeeds (it owns the\n\t\t\t\t\t// graph), mirroring restore. So no `resume` is injected here.\n\t\t\t\t\treturn provideFileSystem(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\t\tEffect.flatMap((graphInput) =>\n\t\t\t\t\t\t\t\tsnapshot.capture({\n\t\t\t\t\t\t\t\t\tid: cmd.snapshotId,\n\t\t\t\t\t\t\t\t\t...(cmd.name === undefined ? {} : { label: cmd.name }),\n\t\t\t\t\t\t\t\t\tgraphInput,\n\t\t\t\t\t\t\t\t\t...(cmd.replaceExisting === true ? { replaceExistingLabel: true } : {}),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((meta) => [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttag: 'snapshot.captured',\n\t\t\t\t\t\t\t\tsnapshotId: meta.id,\n\t\t\t\t\t\t\t\t...(meta.label === null ? {} : { name: meta.label }),\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\tcase 'snapshot.restore':\n\t\t\t\t\treturn provideFileSystem(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\t\tEffect.flatMap((currentGraphInput) =>\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: cmd.snapshotId,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((meta) => [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttag: 'snapshot.restored',\n\t\t\t\t\t\t\t\tsnapshotId: meta.id,\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\tcase 'snapshot.list':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.list).pipe(Effect.as([]));\n\t\t\t\tcase 'snapshot.delete':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.delete(cmd.snapshotId)).pipe(Effect.as([]));\n\t\t\t\tcase 'wipe.requested':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.wipe({})).pipe(Effect.as([]));\n\t\t\t\tcase 'prune.requested':\n\t\t\t\t\t// Route to the same orchestrator the offline `devstack prune`\n\t\t\t\t\t// verb uses (`runLifecyclePrune`), NOT the snapshot-orchestrator\n\t\t\t\t\t// prune (which only cleans the snapshot catalog and would leave\n\t\t\t\t\t// stale containers/networks/volumes/images behind for an attached\n\t\t\t\t\t// supervisor — silent under-prune is the bug we're closing).\n\t\t\t\t\t// Live supervisor implies the current stack's group is live and\n\t\t\t\t\t// therefore excluded from the default selection — exactly the\n\t\t\t\t\t// invariant the live-attached `prune` needs (operator can prune\n\t\t\t\t\t// sibling stacks under the same Docker daemon without touching\n\t\t\t\t\t// the running one).\n\t\t\t\t\treturn collectLifecyclePruneInventory({ runtimeRoot: params.runtimeRoot }).pipe(\n\t\t\t\t\t\tEffect.flatMap((inventory) =>\n\t\t\t\t\t\t\trunLifecyclePrune(\n\t\t\t\t\t\t\t\t{ runtimeRoot: params.runtimeRoot },\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tgroupKeys: defaultLifecyclePruneSelection(\n\t\t\t\t\t\t\t\t\t\tinventory,\n\t\t\t\t\t\t\t\t\t\tDEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\tresources: DEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\t\t\t\t\t\t\t\t\tdryRun: false,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tEffect.as([]),\n\t\t\t\t\t);\n\t\t\t\tdefault:\n\t\t\t\t\treturn Effect.succeed([]);\n\t\t\t}\n\t\t};\n\t\treturn handler;\n\t});\n\nconst installLiveSupervisorRoster = (params: {\n\treadonly stackRoot: string;\n\treadonly app: string;\n\treadonly stack: string;\n}): Effect.Effect<void, unknown, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst paths = rosterPathsFor(params.stackRoot);\n\t\tconst claimed = yield* claim(paths);\n\t\tif (!claimed.soleHolder) {\n\t\t\tyield* release(paths).pipe(Effect.catch(() => Effect.void));\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliSupervisorLiveError({\n\t\t\t\t\tapp: params.app,\n\t\t\t\t\tstack: params.stack,\n\t\t\t\t\thint: 'use `devstack apply` from another shell, or choose a different --stack name',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tyield* Effect.addFinalizer(() => release(paths).pipe(Effect.catch(() => Effect.void)));\n\t\tyield* Effect.forkScoped(heartbeatFiber(paths));\n\t});\n\n// -----------------------------------------------------------------------------\n// CLI boot bundle — the `commandHandler` + `boot` hooks the seam consumes\n// -----------------------------------------------------------------------------\n\nexport interface UpBootBundleInput {\n\treadonly stack: SupervisedStack;\n\treadonly identityValue: Identity;\n\treadonly runtimeRoot: string;\n\treadonly devstackVersion: string;\n\treadonly rendererMode: import('../../surfaces/tui/mode-detect.ts').RendererMode;\n\treadonly startFromSnapshot?: {\n\t\treadonly id: string;\n\t\treadonly stalePolicy: 'warn' | 'block' | 'clean-start';\n\t};\n\treadonly snapshotCache?: {\n\t\treadonly name: string;\n\t\treadonly existingSnapshotId?: string;\n\t};\n}\n\nexport interface UpBootBundle {\n\treadonly commandHandler: CommandHandlerFactory;\n\treadonly boot: RunStackBootBag;\n}\n\ntype StartFromSnapshot = NonNullable<UpBootBundleInput['startFromSnapshot']>;\ntype SnapshotCache = NonNullable<UpBootBundleInput['snapshotCache']>;\n\n/**\n * Build the CLI's boot concerns as a VALUE bundle the seam consumes — the\n * snapshot/wipe/prune `commandHandler` plus the `beforeInitialAcquire`\n * (recover → IPC bridge → roster claim → TUI mount + event tee). Pure\n * value construction: no substrate is touched here; the hooks pull the\n * SEAM's substrate services at boot (they run inside the supervised scope).\n *\n * Extracted from `runUpLive` so the Docker-free CLI-boot smoke test drives\n * the EXACT bundle the production path feeds `runStackWithBoot` — the only\n * non-e2e gate on the cutover (`main.test.ts` only runs `up --help`).\n */\nexport const buildUpBootBundle = (input: UpBootBundleInput): UpBootBundle => {\n\tconst {\n\t\tstack,\n\t\tidentityValue,\n\t\truntimeRoot,\n\t\tdevstackVersion,\n\t\trendererMode,\n\t\tstartFromSnapshot,\n\t\tsnapshotCache,\n\t} = input;\n\n\tconst computeGraphInput = computeSnapshotGraphInputFromStack({ stack, devstackVersion });\n\tlet refreshSnapshotCache =\n\t\tsnapshotCache !== undefined && snapshotCache.existingSnapshotId === undefined;\n\n\t// The snapshot/wipe/prune `commandHandler` — a FACTORY the seam resolves\n\t// against its LIVE substrate (the seam runs it before `startSupervisor`\n\t// consumes the handler). The factory closes over the seam's ONE\n\t// `SnapshotOrchestratorService` instance — the same one the supervisor's\n\t// contribution dispatcher registers live participants on — so an operator\n\t// `snapshot save` captures the LIVE chain/blob/db state, not an empty set\n\t// off a sibling orchestrator. No Deferred hand-off.\n\tconst commandHandler = makeSnapshotCommandHandler({ runtimeRoot, stack, devstackVersion });\n\n\tconst boot: RunStackBootBag = {\n\t\tdevstackVersion,\n\t\tbeforeInitialAcquire: (h) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// The seam's substrate services — the SAME instances the\n\t\t\t\t// `commandHandler` factory closed over and the supervisor uses,\n\t\t\t\t// driven here for the recover/IPC/roster/TUI work.\n\t\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t\t\tconst stackPaths = yield* StackPathsService;\n\t\t\t\t// Resume any restore interrupted by a hard kill / power-loss\n\t\t\t\t// between the atomic swap and the end of the image-promotion\n\t\t\t\t// handoff (the interrupted-restore sentinel rode the swap into\n\t\t\t\t// the live root and was never cleared). Runs BEFORE any plugin\n\t\t\t\t// acquire so a half-promoted image set is reconciled before any\n\t\t\t\t// L2 lookup observes the runtime root. No-op when the sentinel\n\t\t\t\t// is absent (the clean-boot case); idempotent re-run of restore\n\t\t\t\t// when present.\n\t\t\t\t//\n\t\t\t\t// `restore({ id })` passes NO participants on purpose: this\n\t\t\t\t// hook fires BEFORE the initial acquire registers any snapshot\n\t\t\t\t// participant, so there is no live stack to contribute identity.\n\t\t\t\t// `runRestore` reads the empty participant set as \"no live\n\t\t\t\t// stack\" and skips ONLY the cross-plugin contribution guard\n\t\t\t\t// (the runtime + snapshot-emptiness guards still fire). With a\n\t\t\t\t// participants-required guard this recovery could never clear\n\t\t\t\t// the sentinel — it failed `IdentityMissingLive` every boot.\n\t\t\t\tyield* recoverInterruptedRestore({\n\t\t\t\t\tliveRoot: stackPaths.stackRoot,\n\t\t\t\t\trestoreSnapshot: (id) =>\n\t\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\t\tEffect.flatMap((currentGraphInput) =>\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t\tif (snapshotCache !== undefined) {\n\t\t\t\t\tif (snapshotCache.existingSnapshotId === undefined) {\n\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t`snapshot cache ${snapshotCache.name} was not found; boot will refresh it`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst graphInput = yield* computeGraphInput;\n\t\t\t\t\t\tconst catalog = yield* provideFileSystem(fs, snapshot.list);\n\t\t\t\t\t\tconst entry = catalog.find(\n\t\t\t\t\t\t\t(snapshotEntry) => snapshotEntry.id === snapshotCache.existingSnapshotId,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst snapshotGraphInputId = entry?.metadata?.graphInput.graphInputId ?? null;\n\t\t\t\t\t\tif (snapshotGraphInputId === graphInput.graphInputId) {\n\t\t\t\t\t\t\trefreshSnapshotCache = false;\n\t\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: snapshotCache.existingSnapshotId,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'block',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trefreshSnapshotCache = true;\n\t\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t\t`snapshot cache ${snapshotCache.name} graph input is stale or unreadable; boot will refresh it`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (startFromSnapshot !== undefined) {\n\t\t\t\t\tconst graphInput = yield* computeGraphInput;\n\t\t\t\t\tif (startFromSnapshot.stalePolicy === 'clean-start') {\n\t\t\t\t\t\tconst catalog = yield* provideFileSystem(fs, snapshot.list);\n\t\t\t\t\t\tconst entry = catalog.find(\n\t\t\t\t\t\t\t(snapshotEntry) => snapshotEntry.id === startFromSnapshot.id,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst snapshotGraphInputId = entry?.metadata?.graphInput.graphInputId ?? null;\n\t\t\t\t\t\tif (snapshotGraphInputId !== graphInput.graphInputId) {\n\t\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t\t`snapshot ${startFromSnapshot.id} graph input is stale; clean-start policy skipped restore`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: startFromSnapshot.id,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'block',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\tid: startFromSnapshot.id,\n\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: startFromSnapshot.stalePolicy,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst commandChannel = yield* installCommandChannelBridge({\n\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\thandle: h.supervisor,\n\t\t\t\t});\n\t\t\t\tyield* installLiveSupervisorRoster({\n\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\tapp: String(identityValue.app),\n\t\t\t\t\tstack: String(identityValue.stack),\n\t\t\t\t});\n\t\t\t\tconst rendererEvents = yield* Queue.unbounded<EngineEvent>();\n\t\t\t\tconst renderer = makeTuiSurface({\n\t\t\t\t\tmode: rendererMode,\n\t\t\t\t\tpublishCommand: makeQueueCommandPublisher(h.commands),\n\t\t\t\t\t// Opt-in quiet plain stream (readiness/endpoint/codegen\n\t\t\t\t\t// milestones + warns/errors only). Set by embedded consumers\n\t\t\t\t\t// that pipe `devstack up` output through another tool — the\n\t\t\t\t\t// Playwright `webServer` (see `devstackPlaywrightWebServer`)\n\t\t\t\t\t// — where the full per-event firehose reads as `[WebServer]`\n\t\t\t\t\t// noise. No effect on a TTY (`ink`) or normal CI `plain` run.\n\t\t\t\t\tquiet: process.env.DEVSTACK_PLAIN_QUIET === '1',\n\t\t\t\t});\n\t\t\t\tyield* Effect.addFinalizer(() => renderer.flush.pipe(Effect.catch(() => Effect.void)));\n\t\t\t\tyield* Effect.forkScoped(\n\t\t\t\t\trenderer.mount(h.state, Stream.fromQueue(rendererEvents)).pipe(\n\t\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\tprocess.stderr.write(`renderer failed: ${cause.detail ?? String(cause)}\\n`);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tyield* Effect.forkScoped(\n\t\t\t\t\th.events.pipe(\n\t\t\t\t\t\tStream.runForEach((event) =>\n\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\tyield* Queue.offer(rendererEvents, event);\n\t\t\t\t\t\t\t\tyield* commandChannel.publishEvent(event);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\t// L0 file watcher — dev-only. Drives the supervisor's\n\t\t\t\t// `notifyWatchFire` on edits to any plugin's declared `watch`\n\t\t\t\t// paths (today: the package plugin's Move sources), which\n\t\t\t\t// issues a selective restart → re-produce. Forked into the\n\t\t\t\t// supervised scope so it tears down on shutdown.\n\t\t\t\tyield* startFileWatcher({\n\t\t\t\t\twatchIndex: h.supervisor.watchIndex,\n\t\t\t\t\tnotifyWatchFire: h.supervisor.notifyWatchFire,\n\t\t\t\t});\n\t\t\t}),\n\t\twithinScope: (h) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (snapshotCache === undefined || !refreshSnapshotCache) return;\n\t\t\t\tyield* h.supervisor.runCommand({\n\t\t\t\t\ttag: 'snapshot.capture',\n\t\t\t\t\tname: snapshotCache.name,\n\t\t\t\t\treplaceExisting: true,\n\t\t\t\t});\n\t\t\t}),\n\t};\n\n\treturn { commandHandler, boot };\n};\n\n/**\n * Run `devstack up`. Wires the substrate Layer stack, supervisor,\n * attached renderer, and in-process TUI command queue. The Effect runs\n * as the outer Node fiber so SIGINT reaches scope finalizers.\n *\n * Returns a typed `CommandResult` / fails with a typed `CliError` so\n * the dispatcher's `emitFailure` renders the envelope (JSON mode) or\n * human-mode stderr line. Wirings MUST NOT write raw bytes to\n * `process.stderr` or mutate `process.exitCode` for terminal failures —\n * the dispatcher owns the projection.\n */\nexport const runUpLive = (\n\tconfigPath: string | undefined,\n\tidentity: ResolvedIdentity,\n\toptions: {\n\t\treadonly renderer: GlobalFlags['renderer'];\n\t\treadonly stdoutIsTty: boolean;\n\t\treadonly fromSnapshot?: string;\n\t\treadonly snapshotCache?: string;\n\t\treadonly snapshotStalePolicy?: GlobalFlags['snapshotStalePolicy'];\n\t},\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\tif (options.fromSnapshot !== undefined && options.snapshotCache !== undefined) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliUsageError({\n\t\t\t\t\tmessage: '--snapshot-cache cannot be combined with --from-snapshot',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (options.snapshotCache !== undefined && options.snapshotStalePolicy !== undefined) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliUsageError({\n\t\t\t\t\tmessage: '--snapshot-stale is only valid with --from-snapshot',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst loadExit = yield* Effect.exit(loader.load(configPath));\n\t\tif (Exit.isFailure(loadExit)) {\n\t\t\treturn yield* Effect.fail(cliErrorFromConfigExit(loadExit));\n\t\t}\n\t\tconst loaded = loadExit.value;\n\t\tconst publicStack = (\n\t\t\tloaded as LoadedConfig & {\n\t\t\t\treadonly stack: Stack<SupervisedStack['members']>;\n\t\t\t}\n\t\t).stack;\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\n\t\t// Re-derive the identity against the EFFECTIVE stack (explicit\n\t\t// `--stack`/`$DEVSTACK_STACK` > `config.stackName` > inferred) so\n\t\t// the roster lock, command channel, and container/router naming all\n\t\t// target the same stack the operator selected — matching what\n\t\t// `snapshot.ts` already does. Without this, an explicit `--stack`\n\t\t// would boot the supervisor under the config's `stackName` and a\n\t\t// concurrent default-stack `up` would falsely collide on the live\n\t\t// supervisor (`error: supervisor live for <app>/<stack>`, exit 40).\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\n\t\tconst devstackVersion = readDevstackVersion({ fallback: '0.0.0' });\n\t\tconst rendererMode = resolveUpRendererMode({\n\t\t\tcliRenderer: options.renderer,\n\t\t\tstackRenderer: stack.options.renderer,\n\t\t\tstdoutIsTty: options.stdoutIsTty,\n\t\t});\n\t\tlet startFromSnapshot: StartFromSnapshot | undefined;\n\t\tlet snapshotCache: SnapshotCache | undefined;\n\t\tif (options.fromSnapshot !== undefined) {\n\t\t\tconst resolved = yield* makeSnapshotReader({ stackRoot: effectiveIdentity.stackRoot })\n\t\t\t\t.resolve(options.fromSnapshot)\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\tmessage: 'snapshot lookup failed',\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\tswitch (resolved.tag) {\n\t\t\t\tcase 'found':\n\t\t\t\t\tstartFromSnapshot = {\n\t\t\t\t\t\tid: resolved.entry.snapshotId,\n\t\t\t\t\t\tstalePolicy: options.snapshotStalePolicy ?? 'warn',\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'not-found':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliSnapshotNotFoundError({ snapshotRef: options.fromSnapshot }),\n\t\t\t\t\t);\n\t\t\t\tcase 'ambiguous':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUsageError({\n\t\t\t\t\t\t\tmessage: `snapshot reference is ambiguous: ${resolved.snapshotRef}`,\n\t\t\t\t\t\t\thint: `matches: ${resolved.matches.map((entry) => entry.snapshotId).join(', ')}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (options.snapshotCache !== undefined) {\n\t\t\tconst resolved = yield* makeSnapshotReader({ stackRoot: effectiveIdentity.stackRoot })\n\t\t\t\t.resolve(options.snapshotCache)\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\tmessage: 'snapshot lookup failed',\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\tswitch (resolved.tag) {\n\t\t\t\tcase 'found':\n\t\t\t\t\tsnapshotCache = {\n\t\t\t\t\t\tname: options.snapshotCache,\n\t\t\t\t\t\texistingSnapshotId: resolved.entry.snapshotId,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'not-found':\n\t\t\t\t\tsnapshotCache = { name: options.snapshotCache };\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'ambiguous':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUsageError({\n\t\t\t\t\t\t\tmessage: `snapshot reference is ambiguous: ${resolved.snapshotRef}`,\n\t\t\t\t\t\t\thint: `matches: ${resolved.matches.map((entry) => entry.snapshotId).join(', ')}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// The CLI concerns (snapshot/wipe/prune `commandHandler` + the\n\t\t// recover/roster/IPC/TUI boot hooks) as a value bundle — the\n\t\t// EXACT thing the seam consumes, extracted so the Docker-free CLI-boot\n\t\t// smoke test can drive the real bundle (see `test/cli/up-boot-smoke`).\n\t\tconst { commandHandler, boot } = buildUpBootBundle({\n\t\t\tstack,\n\t\t\tidentityValue,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t\tdevstackVersion,\n\t\t\trendererMode,\n\t\t\t...(startFromSnapshot === undefined ? {} : { startFromSnapshot }),\n\t\t\t...(snapshotCache === undefined ? {} : { snapshotCache }),\n\t\t});\n\n\t\t// The ONE boot seam. `runStackWithBoot` owns the substrate Layer\n\t\t// composition, the contribution dispatcher + post-acquire hook +\n\t\t// `extendContext` assembly, the projection ref, and the\n\t\t// forkDetach/Deferred boot lifecycle — `up` no longer forks its own\n\t\t// parallel orchestration. The CLI concerns (recover/roster/IPC/TUI\n\t\t// + snapshot handler) are passed as injected hooks + `commandHandler`.\n\t\tconst handle = runStackWithBoot(publicStack, {\n\t\t\tidentity: {\n\t\t\t\tapp: String(identityValue.app),\n\t\t\t\tstack: String(identityValue.stack),\n\t\t\t\tnetwork: String(identityValue.network),\n\t\t\t},\n\t\t\tappRoot,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t\tcodegen: stack.options.codegen,\n\t\t\t// Dev loop: a Move-source edit fires the watcher → reacquires the\n\t\t\t// package (new id) → this regenerates the committed bindings so the\n\t\t\t// app sees the new shapes. Pass the STATIC (id-free) contributions —\n\t\t\t// the same the stack-free `codegen` verb uses — so the regen NEVER\n\t\t\t// bakes the live on-chain id into the committed `config.ts`. `apply` /\n\t\t\t// `runStack` leave it off.\n\t\t\temitBindings: deriveContributions(stack.members),\n\t\t\tcommandHandler,\n\t\t\tboot,\n\t\t});\n\n\t\t// The outer fiber BLOCKS on `awaitShutdown` so SIGINT reaches the\n\t\t// supervisor's in-scope signal handler (forked inside `startSupervisor`)\n\t\t// and drives teardown through the command-channel/latch — the same path\n\t\t// the CLI used as the outer fiber before the cutover. `Effect.scoped`\n\t\t// owns the supervised scope so the TUI flush finalizer + roster release\n\t\t// run on scope close.\n\t\t//\n\t\t// Errors project through the seam's DISCRIMINATED channels — no\n\t\t// `matchCauseEffect` re-discrimination:\n\t\t// - `handle.start` fails ⇒ BOOT-time `BootError`. Extract a\n\t\t// `CliSupervisorLiveError` (roster-claim loss) for exit 40; else\n\t\t// wrap as `CliInternalError`.\n\t\t// - `handle.awaitShutdown` fails ⇒ MID-RUN cause. Wrap as\n\t\t// `CliInternalError` (boot already succeeded by then).\n\t\treturn yield* Effect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tyield* handle.start.pipe(\n\t\t\t\t\tEffect.catch((bootError: BootError) => {\n\t\t\t\t\t\tconst live = findCliSupervisorLiveError(bootError.cause);\n\t\t\t\t\t\treturn Effect.fail(\n\t\t\t\t\t\t\tlive ??\n\t\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\t\tmessage: 'stack failed',\n\t\t\t\t\t\t\t\t\tcause: Cause.pretty(bootError.cause),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tyield* handle.awaitShutdown.pipe(\n\t\t\t\t\tEffect.catchCause(\n\t\t\t\t\t\t(cause): Effect.Effect<never, CliError> =>\n\t\t\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\t\tmessage: 'stack failed',\n\t\t\t\t\t\t\t\t\tcause: Cause.pretty(cause as Cause.Cause<unknown>),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn { exitCode: ExitCode.OK } satisfies CommandResult;\n\t\t\t}),\n\t\t);\n\t});\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"up.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/up.ts"],"sourcesContent":["// `devstack up` verb wiring.\n//\n// Wires the substrate Layer stack, supervisor, attached renderer, and\n// in-process TUI command queue. The Effect runs as the outer Node fiber\n// so SIGINT reaches scope finalizers (see surfaces/cli/index.ts\n// architecture invariant).\n//\n// Logger layer: `Logger.layer([])` (silent). `up` owns the TUI, which is\n// the operator-facing output channel; routing structured `Effect.log*`\n// records to stderr would scribble on top of the live ink renderer in\n// human mode and would duplicate the structured event stream the plain\n// renderer already emits in non-TTY mode. The sibling `apply` verb (no\n// TUI) uses `Logger.consolePretty()` because its consumer is CI.\n\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, FileSystem, Queue, Scope, Stream } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport type { EngineEvent } from '../../substrate/events.ts';\nimport { StackPathsService } from '../../substrate/runtime/paths.ts';\nimport {\n\tclaim,\n\theartbeatFiber,\n\trelease,\n\tstartFileWatcher,\n\ttype SupervisedStack,\n\ttype SupervisorCommandHandler,\n} from '../../substrate/runtime/index.ts';\nimport {\n\tcollectLifecyclePruneInventory,\n\tdefaultLifecyclePruneSelection,\n\tDEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\trunLifecyclePrune,\n} from '../../orchestrators/lifecycle-prune/index.ts';\nimport {\n\tcomputeSnapshotGraphInputFromStack,\n\trecoverInterruptedRestore,\n\tSnapshotOrchestratorService,\n} from '../../orchestrators/snapshot/index.ts';\nimport type { Stack } from '../../api/define-devstack.ts';\nimport {\n\trunStackWithBoot,\n\ttype CommandHandlerFactory,\n\ttype RunStackBootBag,\n} from '../../api/run-stack-internal.ts';\nimport type { BootError } from '../../api/run-stack.ts';\nimport {\n\ttype CliError,\n\tCliInternalError,\n\tCliSnapshotNotFoundError,\n\tCliSupervisorLiveError,\n\tCliUsageError,\n\ttype GlobalFlags,\n} from '../../surfaces/cli/index.ts';\nimport type { CommandResult } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport { makeTuiSurface } from '../../surfaces/tui/index.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeQueueCommandPublisher, resolveUpRendererMode } from '../up-lifecycle.ts';\nimport { makeConfigLoader } from './config-loader.ts';\nimport { makeSnapshotReader } from '../snapshot-reader.ts';\nimport {\n\tfindCliSupervisorLiveError,\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { installCommandChannelBridge } from './up-ipc.ts';\nimport { provideFileSystem } from './provide-file-system.ts';\nimport { deriveContributions } from './codegen.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst rosterPathsFor = (stackRoot: string) => ({\n\tstackLockFile: resolvePath(stackRoot, 'stack.lock'),\n\trosterFile: resolvePath(stackRoot, 'roster.json'),\n});\n\n// -----------------------------------------------------------------------------\n// Supervisor command handler — snapshot/wipe/prune injected dispatch\n// -----------------------------------------------------------------------------\n\n/** Build the snapshot/wipe/prune `commandHandler` as a FACTORY the seam\n * resolves against its LIVE substrate (see `CommandHandlerFactory`). The\n * factory yields the seam's `SnapshotOrchestratorService` + `FileSystem` —\n * the SAME orchestrator instance the supervisor's contribution dispatcher\n * registers live participants on, so an operator `snapshot save` captures\n * real chain/blob/db state, not an empty set — and returns a handler\n * (R = `never`, as the command loop requires) that closes over them\n * directly. No Deferred hand-off: the seam runs this factory before\n * `startSupervisor` consumes the handler. */\nconst makeSnapshotCommandHandler = (params: {\n\treadonly runtimeRoot: string;\n\treadonly stack: SupervisedStack;\n\treadonly devstackVersion: string;\n}): CommandHandlerFactory =>\n\tEffect.gen(function* () {\n\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tconst computeGraphInput = computeSnapshotGraphInputFromStack({\n\t\t\tstack: params.stack,\n\t\t\tdevstackVersion: params.devstackVersion,\n\t\t});\n\t\tconst handler: SupervisorCommandHandler = (cmd) => {\n\t\t\tswitch (cmd.tag) {\n\t\t\t\tcase 'snapshot.capture':\n\t\t\t\t\t// The L3 capture is the bounce's gather → stop → commit → retag\n\t\t\t\t\t// → hard-rm half; the RESUME (recreate + wait-write-ready) is the\n\t\t\t\t\t// command-loop's converge after this handler succeeds (it owns the\n\t\t\t\t\t// graph), mirroring restore. So no `resume` is injected here.\n\t\t\t\t\treturn provideFileSystem(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\t\tEffect.flatMap((graphInput) =>\n\t\t\t\t\t\t\t\tsnapshot.capture({\n\t\t\t\t\t\t\t\t\tid: cmd.snapshotId,\n\t\t\t\t\t\t\t\t\t...(cmd.name === undefined ? {} : { label: cmd.name }),\n\t\t\t\t\t\t\t\t\tgraphInput,\n\t\t\t\t\t\t\t\t\t...(cmd.replaceExisting === true ? { replaceExistingLabel: true } : {}),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((meta) => [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttag: 'snapshot.captured',\n\t\t\t\t\t\t\t\tsnapshotId: meta.id,\n\t\t\t\t\t\t\t\t...(meta.label === null ? {} : { name: meta.label }),\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\tcase 'snapshot.restore':\n\t\t\t\t\treturn provideFileSystem(\n\t\t\t\t\t\tfs,\n\t\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\t\tEffect.flatMap((currentGraphInput) =>\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: cmd.snapshotId,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t).pipe(\n\t\t\t\t\t\tEffect.map((meta) => [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttag: 'snapshot.restored',\n\t\t\t\t\t\t\t\tsnapshotId: meta.id,\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\tcase 'snapshot.list':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.list).pipe(Effect.as([]));\n\t\t\t\tcase 'snapshot.delete':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.delete(cmd.snapshotId)).pipe(Effect.as([]));\n\t\t\t\tcase 'wipe.requested':\n\t\t\t\t\treturn provideFileSystem(fs, snapshot.wipe({})).pipe(Effect.as([]));\n\t\t\t\tcase 'prune.requested':\n\t\t\t\t\t// Route to the same orchestrator the offline `devstack prune`\n\t\t\t\t\t// verb uses (`runLifecyclePrune`), NOT the snapshot-orchestrator\n\t\t\t\t\t// prune (which only cleans the snapshot catalog and would leave\n\t\t\t\t\t// stale containers/networks/volumes/images behind for an attached\n\t\t\t\t\t// supervisor — silent under-prune is the bug we're closing).\n\t\t\t\t\t// Live supervisor implies the current stack's group is live and\n\t\t\t\t\t// therefore excluded from the default selection — exactly the\n\t\t\t\t\t// invariant the live-attached `prune` needs (operator can prune\n\t\t\t\t\t// sibling stacks under the same Docker daemon without touching\n\t\t\t\t\t// the running one).\n\t\t\t\t\treturn collectLifecyclePruneInventory({ runtimeRoot: params.runtimeRoot }).pipe(\n\t\t\t\t\t\tEffect.flatMap((inventory) =>\n\t\t\t\t\t\t\trunLifecyclePrune(\n\t\t\t\t\t\t\t\t{ runtimeRoot: params.runtimeRoot },\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tgroupKeys: defaultLifecyclePruneSelection(\n\t\t\t\t\t\t\t\t\t\tinventory,\n\t\t\t\t\t\t\t\t\t\tDEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\tresources: DEFAULT_LIFECYCLE_PRUNE_RESOURCES,\n\t\t\t\t\t\t\t\t\tdryRun: false,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tEffect.as([]),\n\t\t\t\t\t);\n\t\t\t\tdefault:\n\t\t\t\t\treturn Effect.succeed([]);\n\t\t\t}\n\t\t};\n\t\treturn handler;\n\t});\n\nconst installLiveSupervisorRoster = (params: {\n\treadonly stackRoot: string;\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly graphInputId: string;\n}): Effect.Effect<void, unknown, Scope.Scope> =>\n\tEffect.gen(function* () {\n\t\tconst paths = rosterPathsFor(params.stackRoot);\n\t\tconst claimed = yield* claim(paths, 'normal', undefined, {\n\t\t\tgraphInputId: params.graphInputId,\n\t\t});\n\t\tif (!claimed.soleHolder) {\n\t\t\tyield* release(paths).pipe(Effect.catch(() => Effect.void));\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliSupervisorLiveError({\n\t\t\t\t\tapp: params.app,\n\t\t\t\t\tstack: params.stack,\n\t\t\t\t\thint: 'use `devstack apply` from another shell, or choose a different --stack name',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tyield* Effect.addFinalizer(() => release(paths).pipe(Effect.catch(() => Effect.void)));\n\t\tyield* Effect.forkScoped(heartbeatFiber(paths));\n\t});\n\n// -----------------------------------------------------------------------------\n// CLI boot bundle — the `commandHandler` + `boot` hooks the seam consumes\n// -----------------------------------------------------------------------------\n\nexport interface UpBootBundleInput {\n\treadonly stack: SupervisedStack;\n\treadonly identityValue: Identity;\n\treadonly runtimeRoot: string;\n\treadonly devstackVersion: string;\n\treadonly rendererMode: import('../../surfaces/tui/mode-detect.ts').RendererMode;\n\treadonly startFromSnapshot?: {\n\t\treadonly id: string;\n\t\treadonly stalePolicy: 'warn' | 'block' | 'clean-start';\n\t};\n\treadonly snapshotCache?: {\n\t\treadonly name: string;\n\t\treadonly existingSnapshotId?: string;\n\t};\n}\n\nexport interface UpBootBundle {\n\treadonly commandHandler: CommandHandlerFactory;\n\treadonly boot: RunStackBootBag;\n}\n\ntype StartFromSnapshot = NonNullable<UpBootBundleInput['startFromSnapshot']>;\ntype SnapshotCache = NonNullable<UpBootBundleInput['snapshotCache']>;\n\n/**\n * Build the CLI's boot concerns as a VALUE bundle the seam consumes — the\n * snapshot/wipe/prune `commandHandler` plus the `beforeInitialAcquire`\n * (recover → IPC bridge → roster claim → TUI mount + event tee). Pure\n * value construction: no substrate is touched here; the hooks pull the\n * SEAM's substrate services at boot (they run inside the supervised scope).\n *\n * Extracted from `runUpLive` so the Docker-free CLI-boot smoke test drives\n * the EXACT bundle the production path feeds `runStackWithBoot` — the only\n * non-e2e gate on the cutover (`main.test.ts` only runs `up --help`).\n */\nexport const buildUpBootBundle = (input: UpBootBundleInput): UpBootBundle => {\n\tconst {\n\t\tstack,\n\t\tidentityValue,\n\t\truntimeRoot,\n\t\tdevstackVersion,\n\t\trendererMode,\n\t\tstartFromSnapshot,\n\t\tsnapshotCache,\n\t} = input;\n\n\tconst computeGraphInput = computeSnapshotGraphInputFromStack({ stack, devstackVersion });\n\tlet refreshSnapshotCache =\n\t\tsnapshotCache !== undefined && snapshotCache.existingSnapshotId === undefined;\n\n\t// The snapshot/wipe/prune `commandHandler` — a FACTORY the seam resolves\n\t// against its LIVE substrate (the seam runs it before `startSupervisor`\n\t// consumes the handler). The factory closes over the seam's ONE\n\t// `SnapshotOrchestratorService` instance — the same one the supervisor's\n\t// contribution dispatcher registers live participants on — so an operator\n\t// `snapshot save` captures the LIVE chain/blob/db state, not an empty set\n\t// off a sibling orchestrator. No Deferred hand-off.\n\tconst commandHandler = makeSnapshotCommandHandler({ runtimeRoot, stack, devstackVersion });\n\n\tconst boot: RunStackBootBag = {\n\t\tdevstackVersion,\n\t\tbeforeInitialAcquire: (h) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// The seam's substrate services — the SAME instances the\n\t\t\t\t// `commandHandler` factory closed over and the supervisor uses,\n\t\t\t\t// driven here for the recover/IPC/roster/TUI work.\n\t\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\t\tconst fs = yield* FileSystem.FileSystem;\n\t\t\t\tconst stackPaths = yield* StackPathsService;\n\t\t\t\tconst graphInput = yield* computeGraphInput;\n\t\t\t\t// Resume any restore interrupted by a hard kill / power-loss\n\t\t\t\t// between the atomic swap and the end of the image-promotion\n\t\t\t\t// handoff (the interrupted-restore sentinel rode the swap into\n\t\t\t\t// the live root and was never cleared). Runs BEFORE any plugin\n\t\t\t\t// acquire so a half-promoted image set is reconciled before any\n\t\t\t\t// L2 lookup observes the runtime root. No-op when the sentinel\n\t\t\t\t// is absent (the clean-boot case); idempotent re-run of restore\n\t\t\t\t// when present.\n\t\t\t\t//\n\t\t\t\t// `restore({ id })` passes NO participants on purpose: this\n\t\t\t\t// hook fires BEFORE the initial acquire registers any snapshot\n\t\t\t\t// participant, so there is no live stack to contribute identity.\n\t\t\t\t// `runRestore` reads the empty participant set as \"no live\n\t\t\t\t// stack\" and skips ONLY the cross-plugin contribution guard\n\t\t\t\t// (the runtime + snapshot-emptiness guards still fire). With a\n\t\t\t\t// participants-required guard this recovery could never clear\n\t\t\t\t// the sentinel — it failed `IdentityMissingLive` every boot.\n\t\t\t\tyield* recoverInterruptedRestore({\n\t\t\t\t\tliveRoot: stackPaths.stackRoot,\n\t\t\t\t\trestoreSnapshot: (id) =>\n\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tif (snapshotCache !== undefined) {\n\t\t\t\t\tif (snapshotCache.existingSnapshotId === undefined) {\n\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t`snapshot cache ${snapshotCache.name} was not found; boot will refresh it`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst catalog = yield* provideFileSystem(fs, snapshot.list);\n\t\t\t\t\t\tconst entry = catalog.find(\n\t\t\t\t\t\t\t(snapshotEntry) => snapshotEntry.id === snapshotCache.existingSnapshotId,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst snapshotGraphInputId = entry?.metadata?.graphInput.graphInputId ?? null;\n\t\t\t\t\t\tif (snapshotGraphInputId === graphInput.graphInputId) {\n\t\t\t\t\t\t\trefreshSnapshotCache = false;\n\t\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: snapshotCache.existingSnapshotId,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'block',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trefreshSnapshotCache = true;\n\t\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t\t`snapshot cache ${snapshotCache.name} graph input is stale or unreadable; boot will refresh it`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (startFromSnapshot !== undefined) {\n\t\t\t\t\tif (startFromSnapshot.stalePolicy === 'clean-start') {\n\t\t\t\t\t\tconst catalog = yield* provideFileSystem(fs, snapshot.list);\n\t\t\t\t\t\tconst entry = catalog.find(\n\t\t\t\t\t\t\t(snapshotEntry) => snapshotEntry.id === startFromSnapshot.id,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst snapshotGraphInputId = entry?.metadata?.graphInput.graphInputId ?? null;\n\t\t\t\t\t\tif (snapshotGraphInputId !== graphInput.graphInputId) {\n\t\t\t\t\t\t\tyield* Effect.logWarning(\n\t\t\t\t\t\t\t\t`snapshot ${startFromSnapshot.id} graph input is stale; clean-start policy skipped restore`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\t\tid: startFromSnapshot.id,\n\t\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'block',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tyield* provideFileSystem(\n\t\t\t\t\t\t\tfs,\n\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\tid: startFromSnapshot.id,\n\t\t\t\t\t\t\t\tcurrentGraphInput: graphInput,\n\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: startFromSnapshot.stalePolicy,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst commandChannel = yield* installCommandChannelBridge({\n\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\thandle: h.supervisor,\n\t\t\t\t});\n\t\t\t\tyield* installLiveSupervisorRoster({\n\t\t\t\t\tstackRoot: stackPaths.stackRoot,\n\t\t\t\t\tapp: String(identityValue.app),\n\t\t\t\t\tstack: String(identityValue.stack),\n\t\t\t\t\tgraphInputId: graphInput.graphInputId,\n\t\t\t\t});\n\t\t\t\tconst rendererEvents = yield* Queue.unbounded<EngineEvent>();\n\t\t\t\tconst renderer = makeTuiSurface({\n\t\t\t\t\tmode: rendererMode,\n\t\t\t\t\tpublishCommand: makeQueueCommandPublisher(h.commands),\n\t\t\t\t\t// Opt-in quiet plain stream (readiness/endpoint/codegen\n\t\t\t\t\t// milestones + warns/errors only). Set by embedded consumers\n\t\t\t\t\t// that pipe `devstack up` output through another tool — the\n\t\t\t\t\t// Playwright `webServer` (see `devstackPlaywrightWebServer`)\n\t\t\t\t\t// — where the full per-event firehose reads as `[WebServer]`\n\t\t\t\t\t// noise. No effect on a TTY (`ink`) or normal CI `plain` run.\n\t\t\t\t\tquiet: process.env.DEVSTACK_PLAIN_QUIET === '1',\n\t\t\t\t});\n\t\t\t\tyield* Effect.addFinalizer(() => renderer.flush.pipe(Effect.catch(() => Effect.void)));\n\t\t\t\tyield* Effect.forkScoped(\n\t\t\t\t\trenderer.mount(h.state, Stream.fromQueue(rendererEvents)).pipe(\n\t\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\tprocess.stderr.write(`renderer failed: ${cause.detail ?? String(cause)}\\n`);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tyield* Effect.forkScoped(\n\t\t\t\t\th.events.pipe(\n\t\t\t\t\t\tStream.runForEach((event) =>\n\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\tyield* Queue.offer(rendererEvents, event);\n\t\t\t\t\t\t\t\tyield* commandChannel.publishEvent(event);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\t// L0 file watcher — dev-only. Drives the supervisor's\n\t\t\t\t// `notifyWatchFire` on edits to any plugin's declared `watch`\n\t\t\t\t// paths (today: the package plugin's Move sources), which\n\t\t\t\t// issues a selective restart → re-produce. Forked into the\n\t\t\t\t// supervised scope so it tears down on shutdown.\n\t\t\t\tyield* startFileWatcher({\n\t\t\t\t\twatchIndex: h.supervisor.watchIndex,\n\t\t\t\t\tnotifyWatchFire: h.supervisor.notifyWatchFire,\n\t\t\t\t});\n\t\t\t}),\n\t\twithinScope: (h) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tif (snapshotCache === undefined || !refreshSnapshotCache) return;\n\t\t\t\tyield* h.supervisor.runCommand({\n\t\t\t\t\ttag: 'snapshot.capture',\n\t\t\t\t\tname: snapshotCache.name,\n\t\t\t\t\treplaceExisting: true,\n\t\t\t\t});\n\t\t\t}),\n\t};\n\n\treturn { commandHandler, boot };\n};\n\n/**\n * Run `devstack up`. Wires the substrate Layer stack, supervisor,\n * attached renderer, and in-process TUI command queue. The Effect runs\n * as the outer Node fiber so SIGINT reaches scope finalizers.\n *\n * Returns a typed `CommandResult` / fails with a typed `CliError` so\n * the dispatcher's `emitFailure` renders the envelope (JSON mode) or\n * human-mode stderr line. Wirings MUST NOT write raw bytes to\n * `process.stderr` or mutate `process.exitCode` for terminal failures —\n * the dispatcher owns the projection.\n */\nexport const runUpLive = (\n\tconfigPath: string | undefined,\n\tidentity: ResolvedIdentity,\n\toptions: {\n\t\treadonly renderer: GlobalFlags['renderer'];\n\t\treadonly stdoutIsTty: boolean;\n\t\treadonly fromSnapshot?: string;\n\t\treadonly snapshotCache?: string;\n\t\treadonly snapshotStalePolicy?: GlobalFlags['snapshotStalePolicy'];\n\t},\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\tif (options.fromSnapshot !== undefined && options.snapshotCache !== undefined) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliUsageError({\n\t\t\t\t\tmessage: '--snapshot-cache cannot be combined with --from-snapshot',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (options.snapshotCache !== undefined && options.snapshotStalePolicy !== undefined) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliUsageError({\n\t\t\t\t\tmessage: '--snapshot-stale is only valid with --from-snapshot',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst loadExit = yield* Effect.exit(loader.load(configPath));\n\t\tif (Exit.isFailure(loadExit)) {\n\t\t\treturn yield* Effect.fail(cliErrorFromConfigExit(loadExit));\n\t\t}\n\t\tconst loaded = loadExit.value;\n\t\tconst publicStack = (\n\t\t\tloaded as LoadedConfig & {\n\t\t\t\treadonly stack: Stack<SupervisedStack['members']>;\n\t\t\t}\n\t\t).stack;\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\n\t\t// Re-derive the identity against the EFFECTIVE stack (explicit\n\t\t// `--stack`/`$DEVSTACK_STACK` > `config.stackName` > inferred) so\n\t\t// the roster lock, command channel, and container/router naming all\n\t\t// target the same stack the operator selected — matching what\n\t\t// `snapshot.ts` already does. Without this, an explicit `--stack`\n\t\t// would boot the supervisor under the config's `stackName` and a\n\t\t// concurrent default-stack `up` would falsely collide on the live\n\t\t// supervisor (`error: supervisor live for <app>/<stack>`, exit 40).\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\n\t\tconst devstackVersion = readDevstackVersion({ fallback: '0.0.0' });\n\t\tconst rendererMode = resolveUpRendererMode({\n\t\t\tcliRenderer: options.renderer,\n\t\t\tstackRenderer: stack.options.renderer,\n\t\t\tstdoutIsTty: options.stdoutIsTty,\n\t\t});\n\t\tlet startFromSnapshot: StartFromSnapshot | undefined;\n\t\tlet snapshotCache: SnapshotCache | undefined;\n\t\tif (options.fromSnapshot !== undefined) {\n\t\t\tconst resolved = yield* makeSnapshotReader({ stackRoot: effectiveIdentity.stackRoot })\n\t\t\t\t.resolve(options.fromSnapshot)\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\tmessage: 'snapshot lookup failed',\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\tswitch (resolved.tag) {\n\t\t\t\tcase 'found':\n\t\t\t\t\tstartFromSnapshot = {\n\t\t\t\t\t\tid: resolved.entry.snapshotId,\n\t\t\t\t\t\tstalePolicy: options.snapshotStalePolicy ?? 'warn',\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'not-found':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliSnapshotNotFoundError({ snapshotRef: options.fromSnapshot }),\n\t\t\t\t\t);\n\t\t\t\tcase 'ambiguous':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUsageError({\n\t\t\t\t\t\t\tmessage: `snapshot reference is ambiguous: ${resolved.snapshotRef}`,\n\t\t\t\t\t\t\thint: `matches: ${resolved.matches.map((entry) => entry.snapshotId).join(', ')}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (options.snapshotCache !== undefined) {\n\t\t\tconst resolved = yield* makeSnapshotReader({ stackRoot: effectiveIdentity.stackRoot })\n\t\t\t\t.resolve(options.snapshotCache)\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\tmessage: 'snapshot lookup failed',\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\tswitch (resolved.tag) {\n\t\t\t\tcase 'found':\n\t\t\t\t\tsnapshotCache = {\n\t\t\t\t\t\tname: options.snapshotCache,\n\t\t\t\t\t\texistingSnapshotId: resolved.entry.snapshotId,\n\t\t\t\t\t};\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'not-found':\n\t\t\t\t\tsnapshotCache = { name: options.snapshotCache };\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'ambiguous':\n\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\tnew CliUsageError({\n\t\t\t\t\t\t\tmessage: `snapshot reference is ambiguous: ${resolved.snapshotRef}`,\n\t\t\t\t\t\t\thint: `matches: ${resolved.matches.map((entry) => entry.snapshotId).join(', ')}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// The CLI concerns (snapshot/wipe/prune `commandHandler` + the\n\t\t// recover/roster/IPC/TUI boot hooks) as a value bundle — the\n\t\t// EXACT thing the seam consumes, extracted so the Docker-free CLI-boot\n\t\t// smoke test can drive the real bundle (see `test/cli/up-boot-smoke`).\n\t\tconst { commandHandler, boot } = buildUpBootBundle({\n\t\t\tstack,\n\t\t\tidentityValue,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t\tdevstackVersion,\n\t\t\trendererMode,\n\t\t\t...(startFromSnapshot === undefined ? {} : { startFromSnapshot }),\n\t\t\t...(snapshotCache === undefined ? {} : { snapshotCache }),\n\t\t});\n\n\t\t// The ONE boot seam. `runStackWithBoot` owns the substrate Layer\n\t\t// composition, the contribution dispatcher + post-acquire hook +\n\t\t// `extendContext` assembly, the projection ref, and the\n\t\t// forkDetach/Deferred boot lifecycle — `up` no longer forks its own\n\t\t// parallel orchestration. The CLI concerns (recover/roster/IPC/TUI\n\t\t// + snapshot handler) are passed as injected hooks + `commandHandler`.\n\t\tconst handle = runStackWithBoot(publicStack, {\n\t\t\tidentity: {\n\t\t\t\tapp: String(identityValue.app),\n\t\t\t\tstack: String(identityValue.stack),\n\t\t\t\tnetwork: String(identityValue.network),\n\t\t\t},\n\t\t\tappRoot,\n\t\t\truntimeRoot: effectiveIdentity.runtimeRoot,\n\t\t\tcodegen: stack.options.codegen,\n\t\t\t// Dev loop: a Move-source edit fires the watcher → reacquires the\n\t\t\t// package (new id) → this regenerates the committed bindings so the\n\t\t\t// app sees the new shapes. Pass the STATIC (id-free) contributions —\n\t\t\t// the same the stack-free `codegen` verb uses — so the regen NEVER\n\t\t\t// bakes the live on-chain id into the committed `config.ts`. `apply` /\n\t\t\t// `runStack` leave it off.\n\t\t\temitBindings: deriveContributions(stack.members),\n\t\t\tcommandHandler,\n\t\t\tboot,\n\t\t});\n\n\t\t// The outer fiber BLOCKS on `awaitShutdown` so SIGINT reaches the\n\t\t// supervisor's in-scope signal handler (forked inside `startSupervisor`)\n\t\t// and drives teardown through the command-channel/latch — the same path\n\t\t// the CLI used as the outer fiber before the cutover. `Effect.scoped`\n\t\t// owns the supervised scope so the TUI flush finalizer + roster release\n\t\t// run on scope close.\n\t\t//\n\t\t// Errors project through the seam's DISCRIMINATED channels — no\n\t\t// `matchCauseEffect` re-discrimination:\n\t\t// - `handle.start` fails ⇒ BOOT-time `BootError`. Extract a\n\t\t// `CliSupervisorLiveError` (roster-claim loss) for exit 40; else\n\t\t// wrap as `CliInternalError`.\n\t\t// - `handle.awaitShutdown` fails ⇒ MID-RUN cause. Wrap as\n\t\t// `CliInternalError` (boot already succeeded by then).\n\t\treturn yield* Effect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tyield* handle.start.pipe(\n\t\t\t\t\tEffect.catch((bootError: BootError) => {\n\t\t\t\t\t\tconst live = findCliSupervisorLiveError(bootError.cause);\n\t\t\t\t\t\treturn Effect.fail(\n\t\t\t\t\t\t\tlive ??\n\t\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\t\tmessage: 'stack failed',\n\t\t\t\t\t\t\t\t\tcause: Cause.pretty(bootError.cause),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tyield* handle.awaitShutdown.pipe(\n\t\t\t\t\tEffect.catchCause(\n\t\t\t\t\t\t(cause): Effect.Effect<never, CliError> =>\n\t\t\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\t\t\tmessage: 'stack failed',\n\t\t\t\t\t\t\t\t\tcause: Cause.pretty(cause as Cause.Cause<unknown>),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn { exitCode: ExitCode.OK } satisfies CommandResult;\n\t\t\t}),\n\t\t);\n\t});\n};\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RouteCollision, RouterValidationError } from "./errors.mjs";
|
|
2
2
|
import { CORS_MIDDLEWARE_NAME } from "./cors.mjs";
|
|
3
|
-
import { renderUrl } from "../../substrate/runtime/routed-url.mjs";
|
|
4
3
|
import { dispatchFileId, routerHostname } from "./hostname.mjs";
|
|
5
4
|
import { Effect } from "effect";
|
|
6
5
|
//#region src/orchestrators/router/file-provider.ts
|
|
@@ -41,15 +40,11 @@ const resolveRoute = (identity, decl, registry, upstreams) => Effect.gen(functio
|
|
|
41
40
|
value: entrypoint.name,
|
|
42
41
|
detail: `wireProtocol family mismatch: decl is '${wireProtocol}' but entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`
|
|
43
42
|
}));
|
|
44
|
-
const upstreamUrl =
|
|
45
|
-
|
|
46
|
-
hostname: upstream.host,
|
|
47
|
-
port: upstream.port
|
|
48
|
-
});
|
|
49
|
-
if (!/^(?:http|https|tcp):\/\/[A-Za-z0-9_.:-]+:\d+$/.test(upstreamUrl)) return yield* Effect.fail(new RouterValidationError({
|
|
43
|
+
const upstreamUrl = `${wireProtocol === "tcp" ? "tcp" : wireProtocol === "https" ? "https" : wireProtocol === "h2c" ? "h2c" : "http"}://${upstream.host}:${upstream.port}`;
|
|
44
|
+
if (!/^(?:http|h2c|https|tcp):\/\/[A-Za-z0-9_.:-]+:\d+$/.test(upstreamUrl)) return yield* Effect.fail(new RouterValidationError({
|
|
50
45
|
field: "upstreamUrl",
|
|
51
46
|
value: upstreamUrl,
|
|
52
|
-
detail: "expected http(s)://<host>:<port
|
|
47
|
+
detail: "expected http(s)://<host>:<port>, h2c://<host>:<port>, or tcp://<host>:<port>"
|
|
53
48
|
}));
|
|
54
49
|
const cors = decl.wireProtocol === "tcp" ? false : decl.cors;
|
|
55
50
|
return {
|
|
@@ -66,7 +61,7 @@ const resolveRoute = (identity, decl, registry, upstreams) => Effect.gen(functio
|
|
|
66
61
|
* TCP renderer to write the `address:` field. Safe because
|
|
67
62
|
* `resolveRoute` validates the URL shape before we get here. */
|
|
68
63
|
const splitUpstream = (url) => {
|
|
69
|
-
const stripped = url.replace(/^(?:http|https|tcp):\/\//, "");
|
|
64
|
+
const stripped = url.replace(/^(?:http|h2c|https|tcp):\/\//, "");
|
|
70
65
|
const lastColon = stripped.lastIndexOf(":");
|
|
71
66
|
return {
|
|
72
67
|
host: stripped.slice(0, lastColon),
|
|
@@ -90,6 +85,7 @@ const renderLeaseHeader = (lease) => [
|
|
|
90
85
|
* no-op rewrites don't wake the watcher. */
|
|
91
86
|
const renderRouteYaml = (route, lease) => {
|
|
92
87
|
if (route.wireProtocol === "tcp") return renderTcpRouteYaml(route, lease);
|
|
88
|
+
if (route.wireProtocol === "h2c") return renderH2cRouteYaml(route, lease);
|
|
93
89
|
return renderHttpRouteYaml(route, lease);
|
|
94
90
|
};
|
|
95
91
|
const routeReadinessMiddlewareName = (route) => `${route.dispatchFileId}-route-ready`;
|
|
@@ -128,6 +124,52 @@ const renderHttpRouteYaml = (route, lease) => {
|
|
|
128
124
|
``
|
|
129
125
|
].filter((line) => line !== null).join("\n");
|
|
130
126
|
};
|
|
127
|
+
const renderH2cRouteYaml = (route, lease) => {
|
|
128
|
+
const middlewares = [routeReadinessMiddlewareName(route), ...route.cors ? [CORS_MIDDLEWARE_NAME] : []];
|
|
129
|
+
const httpFallbackUrl = route.upstreamUrl.replace(/^h2c:\/\//, "http://");
|
|
130
|
+
return [
|
|
131
|
+
`# Auto-generated by devstack router orchestrator. Do not edit by hand.`,
|
|
132
|
+
`# dispatchFileId: ${route.dispatchFileId}`,
|
|
133
|
+
`# wireProtocol: h2c`,
|
|
134
|
+
`# entrypointName: ${route.entrypointName}`,
|
|
135
|
+
`# entrypointPort: ${route.entrypointPort}`,
|
|
136
|
+
`# hostname: ${route.hostname}`,
|
|
137
|
+
...renderLeaseHeader(lease),
|
|
138
|
+
`http:`,
|
|
139
|
+
` routers:`,
|
|
140
|
+
` ${route.dispatchFileId}-grpc-router:`,
|
|
141
|
+
` rule: "Host(\`${route.hostname}\`) && Header(\`Content-Type\`, \`application/grpc\`)"`,
|
|
142
|
+
` entryPoints: ["${route.entrypointName}"]`,
|
|
143
|
+
` service: "${route.dispatchFileId}-grpc-svc"`,
|
|
144
|
+
` middlewares: [${middlewares.map((name) => `"${name}"`).join(", ")}]`,
|
|
145
|
+
` priority: 20`,
|
|
146
|
+
` ${route.dispatchFileId}-router:`,
|
|
147
|
+
` rule: "Host(\`${route.hostname}\`)"`,
|
|
148
|
+
` entryPoints: ["${route.entrypointName}"]`,
|
|
149
|
+
` service: "${route.dispatchFileId}-svc"`,
|
|
150
|
+
` middlewares: [${middlewares.map((name) => `"${name}"`).join(", ")}]`,
|
|
151
|
+
` priority: 10`,
|
|
152
|
+
` middlewares:`,
|
|
153
|
+
` ${routeReadinessMiddlewareName(route)}:`,
|
|
154
|
+
` headers:`,
|
|
155
|
+
` customResponseHeaders:`,
|
|
156
|
+
` ${ROUTE_READINESS_HEADER}: "${route.dispatchFileId}"`,
|
|
157
|
+
` services:`,
|
|
158
|
+
` ${route.dispatchFileId}-grpc-svc:`,
|
|
159
|
+
` loadBalancer:`,
|
|
160
|
+
` passHostHeader: false`,
|
|
161
|
+
` # h2c upstream — native gRPC cleartext HTTP/2.`,
|
|
162
|
+
` servers:`,
|
|
163
|
+
` - url: "${route.upstreamUrl}"`,
|
|
164
|
+
` ${route.dispatchFileId}-svc:`,
|
|
165
|
+
` loadBalancer:`,
|
|
166
|
+
` passHostHeader: false`,
|
|
167
|
+
` # HTTP fallback — JSON-RPC and grpc-web.`,
|
|
168
|
+
` servers:`,
|
|
169
|
+
` - url: "${httpFallbackUrl}"`,
|
|
170
|
+
``
|
|
171
|
+
].join("\n");
|
|
172
|
+
};
|
|
131
173
|
const renderTcpRouteYaml = (route, lease) => {
|
|
132
174
|
const { host, port } = splitUpstream(route.upstreamUrl);
|
|
133
175
|
return [
|
|
@@ -254,7 +296,7 @@ const parseDispatchRouteFile = (body, fallbackDispatchFileId = null) => {
|
|
|
254
296
|
_tag: "DispatchRouteDecodeDiagnostic",
|
|
255
297
|
dispatchFileId: protectedDispatchFileId,
|
|
256
298
|
reason: "missing-required-route-metadata",
|
|
257
|
-
detail: "route file is missing dispatchFileId, wireProtocol/http|https|tcp block, entrypointName, or hostname metadata"
|
|
299
|
+
detail: "route file is missing dispatchFileId, wireProtocol/http|h2c|https|tcp block, entrypointName, or hostname metadata"
|
|
258
300
|
}]
|
|
259
301
|
};
|
|
260
302
|
}
|