@mysten-incubation/devstack 0.2.0 → 0.4.0
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/README.md +1 -1
- package/dashboard-ui/assets/{grpc-a4usE3Nk.js → grpc-CpkDu4SA.js} +1 -1
- package/dashboard-ui/assets/{index-CRYJ4pru.js → index-jLPRmjst.js} +39 -33
- package/dashboard-ui/index.html +1 -1
- package/dist/api/inference-network.d.mts +5 -5
- package/dist/api/inference-network.mjs +5 -5
- package/dist/api/inference-network.mjs.map +1 -1
- package/dist/api/run-stack-internal.mjs +2 -3
- package/dist/api/run-stack-internal.mjs.map +1 -1
- package/dist/build-integrations/dapp-kit/index.d.mts +12 -0
- package/dist/build-integrations/dapp-kit/index.mjs +14 -1
- package/dist/build-integrations/dapp-kit/index.mjs.map +1 -1
- package/dist/build-integrations/playwright/config.d.mts +2 -1
- package/dist/build-integrations/playwright/config.mjs +1 -1
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/playwright/errors.d.mts +1 -1
- package/dist/build-integrations/playwright/errors.mjs.map +1 -1
- package/dist/build-integrations/playwright/index.d.mts +2 -2
- package/dist/build-integrations/playwright/index.mjs +2 -2
- package/dist/build-integrations/playwright/wallet-context.d.mts +2 -1
- package/dist/build-integrations/playwright/wallet-context.mjs +141 -41
- package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
- package/dist/build-integrations/vite/index.d.mts +82 -40
- package/dist/build-integrations/vite/index.mjs +281 -112
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/build-integrations/vitest/config.mjs +3 -4
- package/dist/build-integrations/vitest/config.mjs.map +1 -1
- package/dist/build-integrations/vitest/global-setup.mjs +8 -8
- package/dist/build-integrations/vitest/global-setup.mjs.map +1 -1
- package/dist/cli/doctor-probes.mjs +7 -2
- package/dist/cli/doctor-probes.mjs.map +1 -1
- package/dist/cli/main.mjs +5 -4
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +0 -1
- package/dist/cli/wirings/apply.mjs.map +1 -1
- package/dist/cli/wirings/codegen.mjs +37 -38
- package/dist/cli/wirings/codegen.mjs.map +1 -1
- package/dist/cli/wirings/dump-deployment.mjs +141 -0
- package/dist/cli/wirings/dump-deployment.mjs.map +1 -0
- package/dist/cli/wirings/snapshot.mjs +0 -1
- package/dist/cli/wirings/snapshot.mjs.map +1 -1
- package/dist/cli/wirings/up.mjs +7 -0
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/contracts/codegenable.d.mts +22 -25
- package/dist/contracts/codegenable.mjs +35 -4
- package/dist/contracts/codegenable.mjs.map +1 -1
- package/dist/contracts/config-bindings.mjs +43 -18
- package/dist/contracts/config-bindings.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/orchestrators/boot.d.mts +0 -10
- package/dist/orchestrators/boot.mjs +24 -54
- package/dist/orchestrators/boot.mjs.map +1 -1
- package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
- package/dist/orchestrators/codegen/config-runtime.mjs +137 -112
- package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -1
- package/dist/orchestrators/codegen/deployment-network-file.mjs +130 -0
- package/dist/orchestrators/codegen/deployment-network-file.mjs.map +1 -0
- package/dist/orchestrators/codegen/deployment-strict.mjs +77 -0
- package/dist/orchestrators/codegen/deployment-strict.mjs.map +1 -0
- package/dist/orchestrators/codegen/deployment.d.mts +63 -0
- package/dist/orchestrators/codegen/deployment.mjs +153 -0
- package/dist/orchestrators/codegen/deployment.mjs.map +1 -0
- package/dist/orchestrators/codegen/errors.mjs +7 -9
- package/dist/orchestrators/codegen/errors.mjs.map +1 -1
- package/dist/orchestrators/codegen/format.mjs +20 -1
- package/dist/orchestrators/codegen/format.mjs.map +1 -1
- package/dist/orchestrators/codegen/paths.mjs +1 -8
- package/dist/orchestrators/codegen/paths.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +369 -141
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/orchestrators/layers.mjs +0 -1
- package/dist/orchestrators/layers.mjs.map +1 -1
- package/dist/orchestrators/network-options.d.mts +37 -35
- package/dist/orchestrators/network-options.mjs +13 -10
- package/dist/orchestrators/network-options.mjs.map +1 -1
- package/dist/plugins/account/codegen.d.mts +3 -4
- package/dist/plugins/account/codegen.mjs +5 -7
- package/dist/plugins/account/codegen.mjs.map +1 -1
- package/dist/plugins/account/index.mjs +1 -3
- package/dist/plugins/account/index.mjs.map +1 -1
- package/dist/plugins/coin/codegen.mjs +5 -5
- package/dist/plugins/coin/codegen.mjs.map +1 -1
- package/dist/plugins/coin/index.mjs.map +1 -1
- package/dist/plugins/dashboard/domain.mjs +18 -1
- package/dist/plugins/dashboard/domain.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/root.mjs +5 -3
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/types.mjs +15 -2
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
- package/dist/plugins/deepbook/codegen.mjs +4 -4
- package/dist/plugins/deepbook/codegen.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +7 -3
- package/dist/plugins/deepbook/index.mjs +3 -3
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/internal/codegen-helpers.mjs +0 -1
- package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
- package/dist/plugins/package/codegen.d.mts +1 -12
- package/dist/plugins/package/codegen.mjs +43 -25
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/dep-resolution.mjs +1 -1
- package/dist/plugins/package/dep-resolution.mjs.map +1 -1
- package/dist/plugins/package/index.d.mts +14 -11
- package/dist/plugins/package/index.mjs +6 -7
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/seal/codegen.mjs +3 -3
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/sui/codegen.mjs +5 -4
- package/dist/plugins/sui/codegen.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +10 -10
- package/dist/plugins/sui/index.mjs +1 -2
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/plugins/sui/move-summary-runner.mjs +29 -2
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
- package/dist/plugins/wallet/codegen.d.mts +8 -12
- package/dist/plugins/wallet/codegen.mjs +22 -15
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/index.d.mts +4 -3
- package/dist/plugins/wallet/index.mjs +1 -1
- package/dist/plugins/wallet/index.mjs.map +1 -1
- package/dist/plugins/wallet/service.d.mts +2 -2
- package/dist/plugins/wallet/service.mjs +2 -3
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/plugins/walrus/codegen.mjs +4 -4
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/index.mjs +1 -1
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/substrate/manifest.d.mts +17 -22
- package/dist/substrate/manifest.mjs +4 -6
- package/dist/substrate/manifest.mjs.map +1 -1
- package/dist/substrate/plugin-ctx.d.mts +6 -6
- package/dist/substrate/plugin-ctx.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
- package/dist/substrate/runtime/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.d.mts +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs +54 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs.map +1 -0
- package/dist/substrate/runtime/lifecycle/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +76 -6
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
- package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/paths.mjs +1 -1
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +2 -6
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
- package/dist/surfaces/cli/command-tree.mjs +6 -6
- package/dist/surfaces/cli/command-tree.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs +6 -4
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/cli/wirings/dump-ids.mjs +0 -86
- package/dist/cli/wirings/dump-ids.mjs.map +0 -1
- package/dist/orchestrators/codegen/id-config.d.mts +0 -1
- package/dist/orchestrators/codegen/id-config.mjs +0 -107
- package/dist/orchestrators/codegen/id-config.mjs.map +0 -1
- package/dist/orchestrators/codegen/output-location.mjs +0 -32
- package/dist/orchestrators/codegen/output-location.mjs.map +0 -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\tnetworkOptions: stack.options.networkOptions,\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(() => 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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"}
|
package/dist/cli/wirings/up.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { claim, heartbeatFiber, release } from "../../substrate/runtime/cross-process/roster.mjs";
|
|
2
|
+
import { startFileWatcher } from "../../substrate/runtime/lifecycle/file-watcher.mjs";
|
|
2
3
|
import { StackPathsService } from "../../substrate/runtime/paths.mjs";
|
|
3
4
|
import { computeSnapshotGraphInputFromStack } from "../../orchestrators/snapshot/descriptor.mjs";
|
|
4
5
|
import { recoverInterruptedRestore } from "../../orchestrators/snapshot/interrupted-restore.mjs";
|
|
@@ -19,6 +20,7 @@ import { makeConfigLoader } from "./config-loader.mjs";
|
|
|
19
20
|
import { findCliSupervisorLiveError, identityValueFor, resolvedIdentityForStack } from "./identity.mjs";
|
|
20
21
|
import { installCommandChannelBridge } from "./up-ipc.mjs";
|
|
21
22
|
import { provideFileSystem } from "./provide-file-system.mjs";
|
|
23
|
+
import { deriveContributions } from "./codegen.mjs";
|
|
22
24
|
import { Cause, Effect, Exit, FileSystem, Queue, Stream } from "effect";
|
|
23
25
|
import { dirname, resolve } from "node:path";
|
|
24
26
|
//#region src/cli/wirings/up.ts
|
|
@@ -180,6 +182,10 @@ const buildUpBootBundle = (input) => {
|
|
|
180
182
|
yield* Queue.offer(rendererEvents, event);
|
|
181
183
|
yield* commandChannel.publishEvent(event);
|
|
182
184
|
}))));
|
|
185
|
+
yield* startFileWatcher({
|
|
186
|
+
watchIndex: h.supervisor.watchIndex,
|
|
187
|
+
notifyWatchFire: h.supervisor.notifyWatchFire
|
|
188
|
+
});
|
|
183
189
|
}),
|
|
184
190
|
withinScope: (h) => Effect.gen(function* () {
|
|
185
191
|
if (snapshotCache === void 0 || !refreshSnapshotCache) return;
|
|
@@ -282,6 +288,7 @@ const runUpLive = (configPath, identity, options) => {
|
|
|
282
288
|
appRoot,
|
|
283
289
|
runtimeRoot: effectiveIdentity.runtimeRoot,
|
|
284
290
|
codegen: stack.options.codegen,
|
|
291
|
+
emitBindings: deriveContributions(stack.members),
|
|
285
292
|
commandHandler,
|
|
286
293
|
boot
|
|
287
294
|
});
|
|
@@ -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\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 { 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}),\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\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}): 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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"}
|
|
@@ -54,23 +54,15 @@ interface AggregateContribution {
|
|
|
54
54
|
* orchestrator MUST NOT branch on this value — it is annotation-
|
|
55
55
|
* only. */
|
|
56
56
|
readonly kind?: string;
|
|
57
|
-
/** Where the synthesized aggregate file lands. `'generated'` is
|
|
58
|
-
* the canonical runtime tree (`src/generated/`); `'generated-extras'`
|
|
59
|
-
* routes the aggregate to the gitignored
|
|
60
|
-
* `.devstack/stacks/<stack>/generated-extras/` tree (dev-only +
|
|
61
|
-
* secret artifacts). All decls contributing to one bucket MUST
|
|
62
|
-
* agree on this; the orchestrator reads it from the first
|
|
63
|
-
* contributor it sees for the bucket. Defaults to `'generated'`. */
|
|
64
|
-
readonly outputLocation?: OutputLocation;
|
|
65
57
|
/** When `true`, the synthesized aggregate file is written with
|
|
66
58
|
* tightened `0o600` perms and injected into `.gitignore`. Mirrors
|
|
67
59
|
* `CodegenableDecl.sensitive` for aggregate files. Defaults to
|
|
68
60
|
* `false`. */
|
|
69
61
|
readonly sensitive?: boolean;
|
|
70
|
-
/** Generic-channel contributions for the loadable
|
|
62
|
+
/** Generic-channel contributions for the loadable deployment. Only the
|
|
71
63
|
* LIVE (boot) aggregate of a unified config-binding set sets this;
|
|
72
|
-
* boot's `
|
|
73
|
-
* Carries the live plugin JSON the typed
|
|
64
|
+
* boot's `assembleDeployment` folds it into `deployment.values[ns][key]`.
|
|
65
|
+
* Carries the live plugin JSON the typed deployment fields can't (pool
|
|
74
66
|
* ids, coin types, walrus/seal endpoints). The committed-tree (static)
|
|
75
67
|
* aggregate omits it — those values resolve at app build time. */
|
|
76
68
|
readonly idConfigValues?: {
|
|
@@ -78,13 +70,22 @@ interface AggregateContribution {
|
|
|
78
70
|
readonly [key: string]: unknown;
|
|
79
71
|
};
|
|
80
72
|
};
|
|
73
|
+
/** The STATIC TS type of each generic-channel (`values[ns][key]`)
|
|
74
|
+
* contribution, as a source-string map `valueTypes[namespace][key] =
|
|
75
|
+
* '<tsType>'`. Derived from the SAME config-binding set as `idConfigValues`
|
|
76
|
+
* (every `resolved` binding WITHOUT a `sugar` resolver), but carries the
|
|
77
|
+
* field's declared TS type rather than its live value — so it is present on
|
|
78
|
+
* BOTH the static AND live aggregates (the type is config-known, not
|
|
79
|
+
* resolution-dependent). The codegen orchestrator folds it into the strict
|
|
80
|
+
* `deployment.ts`'s required `values` shape, so a hand-written
|
|
81
|
+
* `deployments/<net>.ts` is compile-checked for every service value. A
|
|
82
|
+
* binding with no declared tsType contributes `'unknown'`. */
|
|
83
|
+
readonly valueTypes?: {
|
|
84
|
+
readonly [namespace: string]: {
|
|
85
|
+
readonly [key: string]: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
81
88
|
}
|
|
82
|
-
/** Which codegen output tree a decl (or aggregate) emits into.
|
|
83
|
-
* `'generated'` is the runtime-imported `src/generated/` tree;
|
|
84
|
-
* `'generated-extras'` is the gitignored
|
|
85
|
-
* `.devstack/stacks/<stack>/generated-extras/` dev-only tree reached
|
|
86
|
-
* via the `@devstack-dev` alias. */
|
|
87
|
-
type OutputLocation = 'generated' | 'generated-extras';
|
|
88
89
|
/**
|
|
89
90
|
* Static, stack-free codegen-decl source. A plugin that can describe its
|
|
90
91
|
* codegen contributions from CONFIG ALONE (no live acquire) exposes this
|
|
@@ -93,9 +94,10 @@ type OutputLocation = 'generated' | 'generated-extras';
|
|
|
93
94
|
* the live `acquire` would emit, but stack-free — KNOWN package ids come
|
|
94
95
|
* from the declared `networks` literals, LOCAL ids stay the all-zero
|
|
95
96
|
* sentinel (`UNRESOLVED_ID`) resolved at app build/dev time through the
|
|
96
|
-
* injected `
|
|
97
|
-
* require live resolution (
|
|
98
|
-
*
|
|
97
|
+
* injected `__DEVSTACK_DEPLOYMENT__` global. Plugins whose contributions genuinely
|
|
98
|
+
* require live resolution (e.g. the values-only account / dev-wallet decls,
|
|
99
|
+
* which feed the deployment envelope rather than a committed file) omit this
|
|
100
|
+
* — the verb simply skips them.
|
|
99
101
|
*/
|
|
100
102
|
type StaticCodegenSource = () => ReadonlyArray<CodegenableDecl>;
|
|
101
103
|
/**
|
|
@@ -107,11 +109,6 @@ interface CodegenableDecl<Emitter extends string = string> {
|
|
|
107
109
|
readonly emitterName: Emitter;
|
|
108
110
|
/** Relative path under the codegen staging dir. */
|
|
109
111
|
readonly outputPath: string;
|
|
110
|
-
/** Which codegen tree this decl's standalone file emits into.
|
|
111
|
-
* `'generated'` (default) → `src/generated/`; `'generated-extras'`
|
|
112
|
-
* → the gitignored `.devstack/stacks/<stack>/generated-extras/`
|
|
113
|
-
* dev-only tree (reached via the `@devstack-dev` alias). */
|
|
114
|
-
readonly outputLocation?: OutputLocation;
|
|
115
112
|
/** When `true`, this decl contributes ONLY to its `aggregate`
|
|
116
113
|
* bucket — the orchestrator skips emitting the standalone
|
|
117
114
|
* per-decl file. Use when the per-decl singleton has no consumer
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* A value the codegen renderer emits VERBATIM (not as a quoted literal).
|
|
4
4
|
*
|
|
5
5
|
* The one authorized use is id resolution in the emitted `config.ts`:
|
|
6
|
-
* `
|
|
7
|
-
* `RawExpr('
|
|
6
|
+
* `mvrOverrides` / `packageId` entries hold a
|
|
7
|
+
* `RawExpr('requireId(dep, "@local/foo")')` so the COMMITTED config carries NO
|
|
8
8
|
* on-chain id. The id resolves at app build/dev time through the injected
|
|
9
|
-
* `
|
|
9
|
+
* `__DEVSTACK_DEPLOYMENT__` global (see the emitted `config-runtime.ts`), which
|
|
10
10
|
* throws loudly when an id is unresolved. Lives at the L0 contract layer
|
|
11
11
|
* so both the package plugin (producer) and the renderer (consumer) can
|
|
12
12
|
* reach it without a layering cycle.
|
|
@@ -22,7 +22,38 @@ var RawExpr = class {
|
|
|
22
22
|
const rawExpr = (expr) => new RawExpr(expr);
|
|
23
23
|
/** Type guard for `RawExpr` — the renderer's verbatim-emit branch. */
|
|
24
24
|
const isRawExpr = (v) => typeof v === "object" && v !== null && v.__rawExpr === true;
|
|
25
|
+
/**
|
|
26
|
+
* A PER-NETWORK service bucket value. The renderer wraps `inner` in a
|
|
27
|
+
* `{ forNetwork(network) { const dep = loadDeployment().forNetwork(network);
|
|
28
|
+
* return <inner> as const; } }` accessor so a service bucket (coins / deepbook
|
|
29
|
+
* / seal / walrus) resolves its ids for whichever network the app's dapp-kit
|
|
30
|
+
* has selected — a runtime `switchNetwork` flips service ids in lockstep with
|
|
31
|
+
* rpc / packages, instead of baking the default network at module load.
|
|
32
|
+
*
|
|
33
|
+
* `inner` is the SAME object the bucket projected before (literals + the
|
|
34
|
+
* resolved `requireValue(dep, …)` raw expressions); the only change is WHERE
|
|
35
|
+
* `dep` comes from — a per-call `loadDeployment().forNetwork(network)` local
|
|
36
|
+
* inside the accessor, not the module-level default-network `dep`. The
|
|
37
|
+
* orchestrator therefore suppresses the module-level `dep` preamble for these
|
|
38
|
+
* buckets (the accessor declares its own) while still importing
|
|
39
|
+
* `loadDeployment` / `requireValue`.
|
|
40
|
+
*
|
|
41
|
+
* `needsDep` records whether `inner` actually references `dep` (a pure-literal
|
|
42
|
+
* bucket — e.g. a known/live seal or a builtin-only coin — does not, so the
|
|
43
|
+
* accessor omits the `const dep = …` line and ignores its `network` param).
|
|
44
|
+
*/
|
|
45
|
+
var ForNetworkBucket = class {
|
|
46
|
+
inner;
|
|
47
|
+
needsDep;
|
|
48
|
+
__forNetworkBucket = true;
|
|
49
|
+
constructor(inner, needsDep) {
|
|
50
|
+
this.inner = inner;
|
|
51
|
+
this.needsDep = needsDep;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** Type guard for `ForNetworkBucket` — the renderer's per-network-accessor branch. */
|
|
55
|
+
const isForNetworkBucket = (v) => typeof v === "object" && v !== null && v.__forNetworkBucket === true;
|
|
25
56
|
//#endregion
|
|
26
|
-
export { isRawExpr, rawExpr };
|
|
57
|
+
export { ForNetworkBucket, isForNetworkBucket, isRawExpr, rawExpr };
|
|
27
58
|
|
|
28
59
|
//# sourceMappingURL=codegenable.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegenable.mjs","names":[],"sources":["../../src/contracts/codegenable.ts"],"sourcesContent":["// Codegenable capability contract (architecture §6).\n//\n// Lets a plugin contribute files to the user's source tree without\n// the codegen surface (L4) knowing the plugin exists. The runtime\n// contract only needs the literal emitter name, output path,\n// sensitivity, and an emit operation that writes through an opaque\n// context. Generated files own their app-facing export types directly.\n\nimport type { Effect } from 'effect';\n\n// -----------------------------------------------------------------------------\n// Raw-expression escape hatch\n// -----------------------------------------------------------------------------\n\n/**\n * A value the codegen renderer emits VERBATIM (not as a quoted literal).\n *\n * The one authorized use is id resolution in the emitted `config.ts`:\n * `byNetwork` / `mvrOverrides` / `packageId` entries hold a\n * `RawExpr('resolveId(\"@local/foo\")')` so the COMMITTED config carries NO\n * on-chain id. The id resolves at app build/dev time through the injected\n * `__DEVSTACK_IDS__` global (see the emitted `config-runtime.ts`), which\n * throws loudly when an id is unresolved. Lives at the L0 contract layer\n * so both the package plugin (producer) and the renderer (consumer) can\n * reach it without a layering cycle.\n */\nexport class RawExpr {\n\treadonly __rawExpr = true as const;\n\tconstructor(readonly expr: string) {}\n}\n\n/** Build a `RawExpr`. */\nexport const rawExpr = (expr: string): RawExpr => new RawExpr(expr);\n\n/** Type guard for `RawExpr` — the renderer's verbatim-emit branch. */\nexport const isRawExpr = (v: unknown): v is RawExpr =>\n\ttypeof v === 'object' && v !== null && (v as { __rawExpr?: unknown }).__rawExpr === true;\n\n/**\n * Opaque per-file emission context.\n *\n * Plugin authors declare named generated exports by calling\n * `exportConst(...)`. The orchestrator owns how those declarations are\n * collected, rendered, grouped, permissioned, and written. Emitters do\n * not return a raw `{ [exportName]: value }` record, which keeps the\n * codegen contract focused on the public generated file shape instead\n * of an internal renderer payload.\n */\nexport interface CodegenEmitContext {\n\t/** Add one `export const <name> = <value> as const;` to the file. */\n\treadonly exportConst: (name: string, value: unknown) => void;\n\t/** Add a raw import statement before the generated exports. */\n\treadonly importStatement: (statement: string) => void;\n\t/** Finish emission after all exports/imports have been written. */\n\treadonly done: () => CodegenEmitDone;\n}\n\nexport interface CodegenEmitDone {\n\treadonly _tag: 'CodegenEmitDone';\n}\n\n/**\n * Optional cross-decl aggregation. When present, the codegen\n * orchestrator additionally folds this decl's exported map into a\n * shared aggregate file alongside its own per-decl output.\n *\n * Architectural note: the orchestrator treats `bucket` as opaque and\n * delegates the projection to the plugin via `project`. Aggregate\n * special-casing (e.g. \"the Sui `services.ts` row holds `{rpc,\n * faucet, graphql}`\") belongs in the plugin's contributor, NOT in\n * the codegen orchestrator (architecture: \"Orchestrator boundaries\n * — never names a service\").\n */\nexport interface AggregateContribution {\n\t/** Which aggregate file this decl contributes to (e.g.\n\t * `'accounts.ts'`, `'coins.ts'`, `'config.ts'`). The plugin\n\t * chooses; the orchestrator treats it as opaque. Distinct decls\n\t * that target the same `bucket` deep-merge into one aggregate\n\t * file (so e.g. sui's `networks.localnet` and every package's\n\t * `packages.<name>` coexist in one `config.ts`). */\n\treadonly bucket: string;\n\t/** Project this decl's `exported` map into the value to merge\n\t * into the aggregate bucket. Returning `null` opts out of\n\t * contributing for this cycle (e.g. when the emitter produced\n\t * no usable shape). The returned record is deep-merged onto\n\t * the bucket; for typed shapes the plugin owns the merge\n\t * semantics via the returned object's key set. */\n\treadonly project: (\n\t\texported: Readonly<Record<string, unknown>>,\n\t) => Readonly<Record<string, unknown>> | null;\n\t/** Optional plugin-supplied kind tag for diagnostics / span\n\t * attributes (e.g. `'sui-network'`, `'account'`). The\n\t * orchestrator MUST NOT branch on this value — it is annotation-\n\t * only. */\n\treadonly kind?: string;\n\t/** Where the synthesized aggregate file lands. `'generated'` is\n\t * the canonical runtime tree (`src/generated/`); `'generated-extras'`\n\t * routes the aggregate to the gitignored\n\t * `.devstack/stacks/<stack>/generated-extras/` tree (dev-only +\n\t * secret artifacts). All decls contributing to one bucket MUST\n\t * agree on this; the orchestrator reads it from the first\n\t * contributor it sees for the bucket. Defaults to `'generated'`. */\n\treadonly outputLocation?: OutputLocation;\n\t/** When `true`, the synthesized aggregate file is written with\n\t * tightened `0o600` perms and injected into `.gitignore`. Mirrors\n\t * `CodegenableDecl.sensitive` for aggregate files. Defaults to\n\t * `false`. */\n\treadonly sensitive?: boolean;\n\t/** Generic-channel contributions for the loadable id-config. Only the\n\t * LIVE (boot) aggregate of a unified config-binding set sets this;\n\t * boot's `assembleIdConfig` folds it into `idConfig.values[ns][key]`.\n\t * Carries the live plugin JSON the typed id-config fields can't (pool\n\t * ids, coin types, walrus/seal endpoints). The committed-tree (static)\n\t * aggregate omits it — those values resolve at app build time. */\n\treadonly idConfigValues?: {\n\t\treadonly [namespace: string]: { readonly [key: string]: unknown };\n\t};\n}\n\n/** Which codegen output tree a decl (or aggregate) emits into.\n * `'generated'` is the runtime-imported `src/generated/` tree;\n * `'generated-extras'` is the gitignored\n * `.devstack/stacks/<stack>/generated-extras/` dev-only tree reached\n * via the `@devstack-dev` alias. */\nexport type OutputLocation = 'generated' | 'generated-extras';\n\n/**\n * Static, stack-free codegen-decl source. A plugin that can describe its\n * codegen contributions from CONFIG ALONE (no live acquire) exposes this\n * on its spec so the `codegen` verb can emit a committed projection without\n * booting a stack. The hook is pure: it returns the same `CodegenableDecl`s\n * the live `acquire` would emit, but stack-free — KNOWN package ids come\n * from the declared `networks` literals, LOCAL ids stay the all-zero\n * sentinel (`UNRESOLVED_ID`) resolved at app build/dev time through the\n * injected `__DEVSTACK_IDS__` global. Plugins whose contributions genuinely\n * require live resolution (or which only land in the gitignored\n * `generated-extras` dev tree) omit this — the verb simply skips them.\n */\nexport type StaticCodegenSource = () => ReadonlyArray<CodegenableDecl>;\n\n/**\n * Codegen contribution. `Emitter` is a literal emitter name used\n * by the codegen orchestrator for attribution and grouping.\n */\nexport interface CodegenableDecl<Emitter extends string = string> {\n\treadonly kind: 'codegenable';\n\treadonly emitterName: Emitter;\n\t/** Relative path under the codegen staging dir. */\n\treadonly outputPath: string;\n\t/** Which codegen tree this decl's standalone file emits into.\n\t * `'generated'` (default) → `src/generated/`; `'generated-extras'`\n\t * → the gitignored `.devstack/stacks/<stack>/generated-extras/`\n\t * dev-only tree (reached via the `@devstack-dev` alias). */\n\treadonly outputLocation?: OutputLocation;\n\t/** When `true`, this decl contributes ONLY to its `aggregate`\n\t * bucket — the orchestrator skips emitting the standalone\n\t * per-decl file. Use when the per-decl singleton has no consumer\n\t * (the combined aggregate is the only app-facing surface). */\n\treadonly aggregateOnly?: boolean;\n\t/** Optional sensitivity flag — drives file permissions and\n\t * `.gitignore` inclusion. */\n\treadonly sensitive?: boolean;\n\t/** When `true`, multiple decls may share this `emitterName`\n\t * (the orchestrator skips the emitter-name uniqueness check for\n\t * this decl). Use sparingly — only when the plugin legitimately\n\t * emits one decl per item (e.g. one `Package` per published\n\t * package). Output-path uniqueness is still enforced. */\n\treadonly allowEmitterNameRepetition?: boolean;\n\t/** Optional aggregation seam — when present, the orchestrator\n\t * folds the per-decl exported map into a shared aggregate file\n\t * via `aggregate.project(exported)`. Plugins own the projection\n\t * shape; the orchestrator is name-blind. */\n\treadonly aggregate?: AggregateContribution;\n\t/** Emit operation; writes generated file declarations through the\n\t * supplied context. */\n\treadonly emit: (ctx: CodegenEmitContext) => Effect.Effect<CodegenEmitDone>;\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAa,UAAb,MAAqB;CAEC;CADrB,YAAqB;CACrB,YAAY,MAAuB;EAAd,KAAA,OAAA;CAAe;AACrC;;AAGA,MAAa,WAAW,SAA0B,IAAI,QAAQ,IAAI;;AAGlE,MAAa,aAAa,MACzB,OAAO,MAAM,YAAY,MAAM,QAAS,EAA8B,cAAc"}
|
|
1
|
+
{"version":3,"file":"codegenable.mjs","names":[],"sources":["../../src/contracts/codegenable.ts"],"sourcesContent":["// Codegenable capability contract (architecture §6).\n//\n// Lets a plugin contribute files to the user's source tree without\n// the codegen surface (L4) knowing the plugin exists. The runtime\n// contract only needs the literal emitter name, output path,\n// sensitivity, and an emit operation that writes through an opaque\n// context. Generated files own their app-facing export types directly.\n\nimport type { Effect } from 'effect';\n\n// -----------------------------------------------------------------------------\n// Raw-expression escape hatch\n// -----------------------------------------------------------------------------\n\n/**\n * A value the codegen renderer emits VERBATIM (not as a quoted literal).\n *\n * The one authorized use is id resolution in the emitted `config.ts`:\n * `mvrOverrides` / `packageId` entries hold a\n * `RawExpr('requireId(dep, \"@local/foo\")')` so the COMMITTED config carries NO\n * on-chain id. The id resolves at app build/dev time through the injected\n * `__DEVSTACK_DEPLOYMENT__` global (see the emitted `config-runtime.ts`), which\n * throws loudly when an id is unresolved. Lives at the L0 contract layer\n * so both the package plugin (producer) and the renderer (consumer) can\n * reach it without a layering cycle.\n */\nexport class RawExpr {\n\treadonly __rawExpr = true as const;\n\tconstructor(readonly expr: string) {}\n}\n\n/** Build a `RawExpr`. */\nexport const rawExpr = (expr: string): RawExpr => new RawExpr(expr);\n\n/** Type guard for `RawExpr` — the renderer's verbatim-emit branch. */\nexport const isRawExpr = (v: unknown): v is RawExpr =>\n\ttypeof v === 'object' && v !== null && (v as { __rawExpr?: unknown }).__rawExpr === true;\n\n/**\n * A PER-NETWORK service bucket value. The renderer wraps `inner` in a\n * `{ forNetwork(network) { const dep = loadDeployment().forNetwork(network);\n * return <inner> as const; } }` accessor so a service bucket (coins / deepbook\n * / seal / walrus) resolves its ids for whichever network the app's dapp-kit\n * has selected — a runtime `switchNetwork` flips service ids in lockstep with\n * rpc / packages, instead of baking the default network at module load.\n *\n * `inner` is the SAME object the bucket projected before (literals + the\n * resolved `requireValue(dep, …)` raw expressions); the only change is WHERE\n * `dep` comes from — a per-call `loadDeployment().forNetwork(network)` local\n * inside the accessor, not the module-level default-network `dep`. The\n * orchestrator therefore suppresses the module-level `dep` preamble for these\n * buckets (the accessor declares its own) while still importing\n * `loadDeployment` / `requireValue`.\n *\n * `needsDep` records whether `inner` actually references `dep` (a pure-literal\n * bucket — e.g. a known/live seal or a builtin-only coin — does not, so the\n * accessor omits the `const dep = …` line and ignores its `network` param).\n */\nexport class ForNetworkBucket {\n\treadonly __forNetworkBucket = true as const;\n\tconstructor(\n\t\treadonly inner: Record<string, unknown>,\n\t\treadonly needsDep: boolean,\n\t) {}\n}\n\n/** Type guard for `ForNetworkBucket` — the renderer's per-network-accessor branch. */\nexport const isForNetworkBucket = (v: unknown): v is ForNetworkBucket =>\n\ttypeof v === 'object' &&\n\tv !== null &&\n\t(v as { __forNetworkBucket?: unknown }).__forNetworkBucket === true;\n\n/**\n * Opaque per-file emission context.\n *\n * Plugin authors declare named generated exports by calling\n * `exportConst(...)`. The orchestrator owns how those declarations are\n * collected, rendered, grouped, permissioned, and written. Emitters do\n * not return a raw `{ [exportName]: value }` record, which keeps the\n * codegen contract focused on the public generated file shape instead\n * of an internal renderer payload.\n */\nexport interface CodegenEmitContext {\n\t/** Add one `export const <name> = <value> as const;` to the file. */\n\treadonly exportConst: (name: string, value: unknown) => void;\n\t/** Add a raw import statement before the generated exports. */\n\treadonly importStatement: (statement: string) => void;\n\t/** Finish emission after all exports/imports have been written. */\n\treadonly done: () => CodegenEmitDone;\n}\n\nexport interface CodegenEmitDone {\n\treadonly _tag: 'CodegenEmitDone';\n}\n\n/**\n * Optional cross-decl aggregation. When present, the codegen\n * orchestrator additionally folds this decl's exported map into a\n * shared aggregate file alongside its own per-decl output.\n *\n * Architectural note: the orchestrator treats `bucket` as opaque and\n * delegates the projection to the plugin via `project`. Aggregate\n * special-casing (e.g. \"the Sui `services.ts` row holds `{rpc,\n * faucet, graphql}`\") belongs in the plugin's contributor, NOT in\n * the codegen orchestrator (architecture: \"Orchestrator boundaries\n * — never names a service\").\n */\nexport interface AggregateContribution {\n\t/** Which aggregate file this decl contributes to (e.g.\n\t * `'accounts.ts'`, `'coins.ts'`, `'config.ts'`). The plugin\n\t * chooses; the orchestrator treats it as opaque. Distinct decls\n\t * that target the same `bucket` deep-merge into one aggregate\n\t * file (so e.g. sui's `networks.localnet` and every package's\n\t * `packages.<name>` coexist in one `config.ts`). */\n\treadonly bucket: string;\n\t/** Project this decl's `exported` map into the value to merge\n\t * into the aggregate bucket. Returning `null` opts out of\n\t * contributing for this cycle (e.g. when the emitter produced\n\t * no usable shape). The returned record is deep-merged onto\n\t * the bucket; for typed shapes the plugin owns the merge\n\t * semantics via the returned object's key set. */\n\treadonly project: (\n\t\texported: Readonly<Record<string, unknown>>,\n\t) => Readonly<Record<string, unknown>> | null;\n\t/** Optional plugin-supplied kind tag for diagnostics / span\n\t * attributes (e.g. `'sui-network'`, `'account'`). The\n\t * orchestrator MUST NOT branch on this value — it is annotation-\n\t * only. */\n\treadonly kind?: string;\n\t/** When `true`, the synthesized aggregate file is written with\n\t * tightened `0o600` perms and injected into `.gitignore`. Mirrors\n\t * `CodegenableDecl.sensitive` for aggregate files. Defaults to\n\t * `false`. */\n\treadonly sensitive?: boolean;\n\t/** Generic-channel contributions for the loadable deployment. Only the\n\t * LIVE (boot) aggregate of a unified config-binding set sets this;\n\t * boot's `assembleDeployment` folds it into `deployment.values[ns][key]`.\n\t * Carries the live plugin JSON the typed deployment fields can't (pool\n\t * ids, coin types, walrus/seal endpoints). The committed-tree (static)\n\t * aggregate omits it — those values resolve at app build time. */\n\treadonly idConfigValues?: {\n\t\treadonly [namespace: string]: { readonly [key: string]: unknown };\n\t};\n\t/** The STATIC TS type of each generic-channel (`values[ns][key]`)\n\t * contribution, as a source-string map `valueTypes[namespace][key] =\n\t * '<tsType>'`. Derived from the SAME config-binding set as `idConfigValues`\n\t * (every `resolved` binding WITHOUT a `sugar` resolver), but carries the\n\t * field's declared TS type rather than its live value — so it is present on\n\t * BOTH the static AND live aggregates (the type is config-known, not\n\t * resolution-dependent). The codegen orchestrator folds it into the strict\n\t * `deployment.ts`'s required `values` shape, so a hand-written\n\t * `deployments/<net>.ts` is compile-checked for every service value. A\n\t * binding with no declared tsType contributes `'unknown'`. */\n\treadonly valueTypes?: {\n\t\treadonly [namespace: string]: { readonly [key: string]: string };\n\t};\n}\n\n/**\n * Static, stack-free codegen-decl source. A plugin that can describe its\n * codegen contributions from CONFIG ALONE (no live acquire) exposes this\n * on its spec so the `codegen` verb can emit a committed projection without\n * booting a stack. The hook is pure: it returns the same `CodegenableDecl`s\n * the live `acquire` would emit, but stack-free — KNOWN package ids come\n * from the declared `networks` literals, LOCAL ids stay the all-zero\n * sentinel (`UNRESOLVED_ID`) resolved at app build/dev time through the\n * injected `__DEVSTACK_DEPLOYMENT__` global. Plugins whose contributions genuinely\n * require live resolution (e.g. the values-only account / dev-wallet decls,\n * which feed the deployment envelope rather than a committed file) omit this\n * — the verb simply skips them.\n */\nexport type StaticCodegenSource = () => ReadonlyArray<CodegenableDecl>;\n\n/**\n * Codegen contribution. `Emitter` is a literal emitter name used\n * by the codegen orchestrator for attribution and grouping.\n */\nexport interface CodegenableDecl<Emitter extends string = string> {\n\treadonly kind: 'codegenable';\n\treadonly emitterName: Emitter;\n\t/** Relative path under the codegen staging dir. */\n\treadonly outputPath: string;\n\t/** When `true`, this decl contributes ONLY to its `aggregate`\n\t * bucket — the orchestrator skips emitting the standalone\n\t * per-decl file. Use when the per-decl singleton has no consumer\n\t * (the combined aggregate is the only app-facing surface). */\n\treadonly aggregateOnly?: boolean;\n\t/** Optional sensitivity flag — drives file permissions and\n\t * `.gitignore` inclusion. */\n\treadonly sensitive?: boolean;\n\t/** When `true`, multiple decls may share this `emitterName`\n\t * (the orchestrator skips the emitter-name uniqueness check for\n\t * this decl). Use sparingly — only when the plugin legitimately\n\t * emits one decl per item (e.g. one `Package` per published\n\t * package). Output-path uniqueness is still enforced. */\n\treadonly allowEmitterNameRepetition?: boolean;\n\t/** Optional aggregation seam — when present, the orchestrator\n\t * folds the per-decl exported map into a shared aggregate file\n\t * via `aggregate.project(exported)`. Plugins own the projection\n\t * shape; the orchestrator is name-blind. */\n\treadonly aggregate?: AggregateContribution;\n\t/** Emit operation; writes generated file declarations through the\n\t * supplied context. */\n\treadonly emit: (ctx: CodegenEmitContext) => Effect.Effect<CodegenEmitDone>;\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,IAAa,UAAb,MAAqB;CAEC;CADrB,YAAqB;CACrB,YAAY,MAAuB;EAAd,KAAA,OAAA;CAAe;AACrC;;AAGA,MAAa,WAAW,SAA0B,IAAI,QAAQ,IAAI;;AAGlE,MAAa,aAAa,MACzB,OAAO,MAAM,YAAY,MAAM,QAAS,EAA8B,cAAc;;;;;;;;;;;;;;;;;;;;;AAsBrF,IAAa,mBAAb,MAA8B;CAGnB;CACA;CAHV,qBAA8B;CAC9B,YACC,OACA,UACC;EAFQ,KAAA,QAAA;EACA,KAAA,WAAA;CACP;AACJ;;AAGA,MAAa,sBAAsB,MAClC,OAAO,MAAM,YACb,MAAM,QACL,EAAuC,uBAAuB"}
|
|
@@ -16,22 +16,24 @@ const setAtPath = (root, path, value) => {
|
|
|
16
16
|
}
|
|
17
17
|
cursor[path[path.length - 1]] = value;
|
|
18
18
|
};
|
|
19
|
-
/** The `rawExpr` the STATIC path emits for a resolved binding.
|
|
19
|
+
/** The `rawExpr` the STATIC path emits for a resolved binding. Reads off the
|
|
20
|
+
* loaded deployment's active network — the emitted file carries a
|
|
21
|
+
* `const dep = __deployment.forNetwork(__deployment.defaultNetwork);`
|
|
22
|
+
* preamble (wired by the codegen orchestrator), so these expressions
|
|
23
|
+
* reference `dep` / `__deployment` directly. */
|
|
20
24
|
const staticExprFor = (binding) => {
|
|
21
25
|
const sugar = binding.sugar;
|
|
22
|
-
if (sugar === void 0) {
|
|
23
|
-
const call = `resolveValue(${JSON.stringify(binding.namespace)}, ${JSON.stringify(binding.key)})`;
|
|
24
|
-
return rawExpr(binding.tsType === void 0 ? call : `${call} as ${binding.tsType}`);
|
|
25
|
-
}
|
|
26
|
+
if (sugar === void 0) return rawExpr(`requireValue${binding.tsType === void 0 ? "" : `<${binding.tsType}>`}(dep, ${JSON.stringify(binding.namespace)}, ${JSON.stringify(binding.key)})`);
|
|
26
27
|
switch (sugar.kind) {
|
|
27
|
-
case "id": return rawExpr(`
|
|
28
|
-
case "network": return rawExpr("
|
|
29
|
-
case "networks": return rawExpr("
|
|
28
|
+
case "id": return rawExpr(`requireId(dep, ${JSON.stringify(sugar.mvrPlaceholder)})`);
|
|
29
|
+
case "network": return rawExpr("dep.network");
|
|
30
|
+
case "networks": return rawExpr("Object.fromEntries(__deployment.networkNames.map((n) => [n, __deployment.forNetwork(n)]))");
|
|
31
|
+
case "mvrType": return rawExpr(`\`\${requireId(dep, ${JSON.stringify(sugar.mvrPlaceholder)})}::${sugar.typeSuffix}\``);
|
|
30
32
|
}
|
|
31
33
|
};
|
|
32
34
|
/** Build the STATIC (committed-tree) aggregate projection from a binding
|
|
33
|
-
* set — `rawExpr(
|
|
34
|
-
* rest. Pure: needs no acquired state. */
|
|
35
|
+
* set — `rawExpr(requireValue(dep, …)/requireId(dep, …)/dep.network/…)` for
|
|
36
|
+
* resolved bindings, literals for the rest. Pure: needs no acquired state. */
|
|
35
37
|
const projectStaticConfig = (set) => {
|
|
36
38
|
const root = {};
|
|
37
39
|
for (const binding of set.bindings) {
|
|
@@ -51,10 +53,10 @@ const projectLiveConfig = (set, state) => {
|
|
|
51
53
|
return root;
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
|
-
* Derive the live `
|
|
56
|
+
* Derive the live `deployment.values[namespace][key]` contributions of a
|
|
55
57
|
* binding set. Only `resolved` bindings WITHOUT a `sugar` resolver land in
|
|
56
|
-
* the generic channel (sugar bindings feed the typed
|
|
57
|
-
* sliced from the live `config.ts` bucket by `
|
|
58
|
+
* the generic channel (sugar bindings feed the typed deployment fields,
|
|
59
|
+
* sliced from the live `config.ts` bucket by `deploymentFromBucket`). The
|
|
58
60
|
* generic channel is the part the fixed typed schema can't carry.
|
|
59
61
|
*/
|
|
60
62
|
const liveValuesOf = (set, state) => {
|
|
@@ -67,19 +69,41 @@ const liveValuesOf = (set, state) => {
|
|
|
67
69
|
return out;
|
|
68
70
|
};
|
|
69
71
|
/**
|
|
72
|
+
* Derive the STATIC TS type of each generic-channel (`values[ns][key]`)
|
|
73
|
+
* contribution — `valueTypes[namespace][key] = '<tsType>'`. The SAME binding
|
|
74
|
+
* subset `liveValuesOf` covers (every `resolved` binding WITHOUT a `sugar`
|
|
75
|
+
* resolver — sugar bindings feed the typed deployment fields, not `values`),
|
|
76
|
+
* but carries each field's declared `tsType` source string (defaulting to
|
|
77
|
+
* `'unknown'`) instead of its live value. Pure over the binding set — the type
|
|
78
|
+
* is config-known, so this is identical on the live and static paths. The
|
|
79
|
+
* codegen orchestrator folds it into the strict `deployment.ts`'s required
|
|
80
|
+
* `values` shape so a hand-written deployment is compile-checked for every
|
|
81
|
+
* service value.
|
|
82
|
+
*/
|
|
83
|
+
const valueTypesOf = (set) => {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const binding of set.bindings) {
|
|
86
|
+
if (binding.variant !== "resolved" || binding.sugar !== void 0) continue;
|
|
87
|
+
const ns = out[binding.namespace] ??= {};
|
|
88
|
+
ns[binding.key] = binding.tsType ?? "unknown";
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
70
93
|
* Derive a `CodegenableDecl` from a binding set.
|
|
71
94
|
*
|
|
72
95
|
* `'static'` → the committed-tree decl (stack-free `codegen` verb). Its
|
|
73
96
|
* aggregate emits `rawExpr(resolve…())` for resolved bindings.
|
|
74
97
|
*
|
|
75
98
|
* `{ mode: 'live', state }` → the boot decl. Its aggregate bakes concrete
|
|
76
|
-
* values from `state`; boot's `
|
|
77
|
-
* loadable
|
|
99
|
+
* values from `state`; boot's `assembleDeployment` reads those back into the
|
|
100
|
+
* loadable deployment (typed channel + the generic `values` channel).
|
|
78
101
|
*/
|
|
79
102
|
const configCodegenable = (set, how, options = {}) => {
|
|
80
103
|
const live = how !== "static";
|
|
81
104
|
const projected = live ? projectLiveConfig(set, how.state) : projectStaticConfig(set);
|
|
82
105
|
const idConfigValues = live ? liveValuesOf(set, how.state) : {};
|
|
106
|
+
const valueTypes = valueTypesOf(set);
|
|
83
107
|
const extraExports = options.extraExports;
|
|
84
108
|
return {
|
|
85
109
|
kind: "codegenable",
|
|
@@ -91,7 +115,8 @@ const configCodegenable = (set, how, options = {}) => {
|
|
|
91
115
|
kind: set.kind,
|
|
92
116
|
bucket: set.bucket,
|
|
93
117
|
project: () => projected,
|
|
94
|
-
...live && Object.keys(idConfigValues).length > 0 ? { idConfigValues } : {}
|
|
118
|
+
...live && Object.keys(idConfigValues).length > 0 ? { idConfigValues } : {},
|
|
119
|
+
...Object.keys(valueTypes).length > 0 ? { valueTypes } : {}
|
|
95
120
|
},
|
|
96
121
|
emit: (ctx) => Effect.sync(() => {
|
|
97
122
|
if (extraExports !== void 0) for (const [name, value] of Object.entries(extraExports)) ctx.exportConst(name, value);
|
|
@@ -147,13 +172,13 @@ const siblingBucketBindings = (spec) => {
|
|
|
147
172
|
};
|
|
148
173
|
};
|
|
149
174
|
/** Derive the LIVE bucket decl — feeds concrete values into the generic
|
|
150
|
-
*
|
|
175
|
+
* deployment `values` channel (no per-stack generated file is written). */
|
|
151
176
|
const liveBucketCodegen = (spec, state) => configCodegenable(siblingBucketBindings(spec), {
|
|
152
177
|
mode: "live",
|
|
153
178
|
state
|
|
154
179
|
});
|
|
155
180
|
/** Derive the STATIC (stack-free) bucket decl — emits
|
|
156
|
-
* `
|
|
181
|
+
* `requireValue(dep, namespace, key)` for resolved fields, literals for the
|
|
157
182
|
* rest. The committed `<bucket>.ts` carries no baked runtime value. */
|
|
158
183
|
const staticBucketCodegen = (spec) => configCodegenable(siblingBucketBindings(spec), "static");
|
|
159
184
|
//#endregion
|