@mysten-incubation/devstack 0.3.0 → 0.5.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 +10 -11
- package/dashboard-ui/assets/{grpc-Clz0oOtc.js → grpc-CpkDu4SA.js} +1 -1
- package/dashboard-ui/assets/{index-Cmqv9kiU.js → index-jLPRmjst.js} +2 -2
- 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 +1 -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/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 +54 -42
- package/dist/build-integrations/vite/index.mjs +239 -106
- package/dist/build-integrations/vite/index.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/main.mjs +3 -2
- 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 +2 -3
- 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/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.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/orchestrators/boot.d.mts +0 -10
- package/dist/orchestrators/boot.mjs +19 -59
- 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 +354 -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/schema/root.mjs +3 -1
- package/dist/plugins/dashboard/schema/root.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 +36 -6
- package/dist/plugins/deepbook/index.mjs +52 -16
- 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.d.mts +5 -0
- package/dist/plugins/seal/codegen.mjs +55 -12
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +64 -19
- package/dist/plugins/seal/index.mjs +26 -36
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.d.mts +0 -2
- package/dist/plugins/seal/mode/fork-known.mjs +11 -22
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.d.mts +36 -13
- package/dist/plugins/seal/mode/live.mjs +104 -43
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +2 -1
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/registry-publish.d.mts +15 -3
- package/dist/plugins/seal/registry-publish.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.mjs +1 -2
- package/dist/plugins/sui/index.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.d.mts +137 -8
- package/dist/plugins/walrus/index.mjs +46 -9
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.d.mts +5 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +13 -10
- package/dist/plugins/walrus/mode/known-deploy.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/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/paths.mjs +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 +2 -2
- 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
- package/dist/plugins/seal/service.mjs +0 -17
- package/dist/plugins/seal/service.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/build-integrations/vite/index.ts"],"sourcesContent":["// Vite build-integration — `@generated` alias plugin.\n//\n// App source imports Move codegen through a configurable alias prefix\n// (default `@generated`) instead of `./generated`. This plugin points\n// that alias at the committed `<root>/src/generated` tree — the ONE\n// source of bindings, written only by the stack-free `devstack codegen`\n// verb. On-chain ids are NOT baked into that tree; they resolve at\n// runtime via the `__DEVSTACK_IDS__` global (see `resolveInjectedIds`),\n// so the same generated source serves every stack. Resolution:\n// 1. `options.generatedDir` — explicit escape hatch (relative → root).\n// 2. `<root>/src/generated` — the committed tree, always.\n//\n// Because Playwright's `webServer` runs the app's OWN Vite as a child,\n// the same plugin serves both `pnpm dev` and the e2e dev server\n// automatically. Vitest has its own Vite pipeline, so apps add this\n// plugin to `vitest.config.ts` too — and resolve `@generated` to the\n// same committed tree (the per-stack live tree no longer exists).\n//\n// SYNC + dependency-light, mirroring the playwright/vitest helpers: NO\n// heavy imports at module top-level, and `vite` is NOT imported (it is\n// an app-side dev dependency, not a devstack runtime dep). The return\n// value is a STRUCTURAL Vite `Plugin` — a `{ name, config }` object —\n// typed loosely so this module loads without `vite` installed. The\n// manifest read goes through the same `build-integrations/runtime`\n// machinery the playwright/vitest integrations use.\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\n\nimport { discoverManifestPath, resolveDiscoveryEnv } from '../runtime/index.ts';\nimport { resolveNetworkOptions } from '../../orchestrators/network-options.ts';\nimport { decodeIdConfig } from '../../orchestrators/codegen/id-config.ts';\n\n/** Default import-alias prefix. Customizable via `options.alias` (some\n * apps prefer `@gen`, `~generated`, …). The app MUST use the SAME\n * prefix in three derivable places: this plugin option, the\n * `tsconfig` `paths` entry, and its import specifiers. */\nexport const DEFAULT_GENERATED_ALIAS = '@generated';\n\n/** Default dev-extras import-alias prefix. Resolves the PRIMARY stack's\n * `.devstack/stacks/<stack>/generated-extras` tree (dev-only / secret\n * artifacts — `accounts.ts`, `dev-wallet.ts`). Mirror the `@generated`\n * three-place discipline: this option, the `tsconfig` `paths` entry,\n * and the import specifiers. */\nexport const DEFAULT_DEV_EXTRAS_ALIAS = '@devstack-dev';\n\n/** The committed generated-bindings subpath, relative to the Vite root.\n * The single source of bindings — written by `devstack codegen` — that\n * `@generated` always resolves to (absent the `options.generatedDir`\n * escape hatch). */\nconst GENERATED_SUBPATH = 'src/generated';\n\nexport interface DevstackVitePluginOptions {\n\t/** Import-alias prefix. Default `'@generated'`. */\n\treadonly alias?: string;\n\t/** Dev-extras import-alias prefix. Default `'@devstack-dev'`. */\n\treadonly devExtrasAlias?: string;\n\t/** Explicit generated dir — bypasses manifest discovery entirely\n\t * (escape hatch for unusual layouts / tests). Relative paths\n\t * resolve against the Vite root. */\n\treadonly generatedDir?: string;\n\t/** Explicit dev-extras dir — bypasses manifest discovery. Relative\n\t * paths resolve against the Vite root. */\n\treadonly extrasDir?: string;\n\t/** Inject + register the devstack dev wallet on the page in DEV\n\t * (wallet-standard, so dApp Kit auto-discovers it). Defaults to\n\t * `true`. Production builds (`command === 'build'`) inject nothing\n\t * regardless. */\n\treadonly injectDevWallet?: boolean;\n\t/** Auto-approve all dev-wallet signing requests (headless Playwright /\n\t * in-app \"Open/Join as\" buttons). Defaults to the `DEVSTACK_AUTO_APPROVE`\n\t * env (`'1'`/`'true'`), then to the active network's `autoApproveSigning`\n\t * per-network policy (ON for every network except live `mainnet`). A\n\t * single switch, replacing per-app `VITE_*_AUTO_APPROVE`.\n\t *\n\t * HARD-CLAMP: on live `mainnet` the per-network policy forces\n\t * auto-approve OFF — a real-funds signature is NEVER granted without a\n\t * human in the loop. An explicit `autoApprove: true` here, or\n\t * `DEVSTACK_AUTO_APPROVE`, still take precedence (the author opted in\n\t * deliberately), but the policy default never silently auto-approves on\n\t * mainnet. */\n\treadonly autoApprove?: boolean;\n\t/** Per-network overrides for the dev-convenience policy (the same\n\t * `networkOptions` shape `defineDevstack` takes, forwarded verbatim).\n\t * Only the `autoApproveSigning` field is read here, to resolve the\n\t * auto-approve default for the active network. Omitted ⇒ the built-in\n\t * policy (ON except live `mainnet`) applies. The override RECORD is not\n\t * otherwise on disk, so an app that customizes per-network signing must\n\t * thread it here explicitly; the mainnet hard-clamp holds regardless. */\n\treadonly networkOptions?: Readonly<Record<string, unknown>>;\n\t/** Production id-config FILE — the known deployment's `devstack-ids.json`\n\t * (same schema the dev stack writes), committed at e.g.\n\t * `config/<network>.ids.json`. Used only for `command === 'build'` to\n\t * inject `__DEVSTACK_IDS__`. Relative paths resolve against the Vite\n\t * root. If omitted, the `DEVSTACK_IDS_FILE` env (a path pointer) is used.\n\t * Neither ⇒ no ids baked, and the generated resolver throws loudly at\n\t * id-access time. We deliberately take a FILE, not a JSON env blob: a\n\t * real deployment's ids are many + nested. */\n\treadonly ids?: string;\n}\n\n/** A `vite` `Plugin`'s `config` hook receives the partial user config.\n * We only ever read `config.root` and return a `resolve.alias` patch,\n * so a one-field structural subset is enough — this keeps the module\n * loadable without `vite` types (mirroring how the playwright/vitest\n * presets avoid importing their optional peer at module init). */\ninterface ViteUserConfigLike {\n\treadonly root?: string;\n\treadonly command?: 'build' | 'serve';\n}\n\n/** Virtual module id the dev-only HTML injection imports. Vite convention:\n * a `virtual:` specifier resolved to a `\\0`-prefixed id in `resolveId`. */\nconst VIRTUAL_DEV_WALLET_ID = 'virtual:devstack-dev-wallet';\nconst RESOLVED_VIRTUAL_DEV_WALLET_ID = '\\0virtual:devstack-dev-wallet';\n/** The `/@id/` URL form Vite's middleware understands for a `\\0`-prefixed\n * virtual id: the NUL byte is encoded as the literal `__x00__` token (a\n * raw `\\0` in an HTML attribute is mangled to a space, which Vite then\n * misroutes to the SPA fallback). Used for the injected `<script src>`. */\nconst VIRTUAL_DEV_WALLET_SCRIPT_SRC = `/@id/__x00__${VIRTUAL_DEV_WALLET_ID}`;\n\n/** The structural `Plugin` shape we return — `name`, a `config` hook, and\n * the DEV-only dev-wallet-injection hooks. Typed as the loose object Vite\n * accepts (Vite's `Plugin` is structurally compatible) so callers spread\n * it into `plugins: []` without devstack depending on `vite`. */\n/** Vite's `config` hook second arg — `{ command, mode }`. We read only\n * `command` to pick the dev vs prod id-injection source. */\ninterface ViteConfigEnvLike {\n\treadonly command?: 'build' | 'serve';\n}\n\n/** The structural subset of Vite's `ViteDevServer` the `configureServer`\n * hook uses: the chokidar `watcher` (to track the ids file) and the HMR\n * channel (to push a full reload). `ws` is Vite's long-stable channel;\n * `hot` is the newer alias — we use whichever the running Vite exposes. */\ninterface ViteDevServerLike {\n\treadonly watcher: {\n\t\tadd: (paths: string | ReadonlyArray<string>) => void;\n\t\ton: (event: 'change' | 'add', listener: (path: string) => void) => void;\n\t};\n\treadonly ws?: { send: (payload: { type: 'full-reload' }) => void };\n\treadonly hot?: { send: (payload: { type: 'full-reload' }) => void };\n}\n\nexport interface DevstackVitePlugin {\n\treadonly name: string;\n\treadonly config: (\n\t\tconfig: ViteUserConfigLike,\n\t\tenv?: ViteConfigEnvLike,\n\t) => {\n\t\treadonly resolve: {\n\t\t\treadonly alias: Record<string, string>;\n\t\t\t// Mutable `string[]` (NOT `readonly`): Vite's `ResolveOptions.dedupe`\n\t\t\t// and `DepOptimizationConfig.include` are both `string[] | undefined`,\n\t\t\t// and a `readonly string[]` return makes the whole `config` hook\n\t\t\t// unassignable to Vite's `Plugin` type in a consuming app's\n\t\t\t// `vite.config.ts`/`vitest.config.ts`. The value we return\n\t\t\t// (`resolvableLitDedupe(root)`, a freshly built array) is already\n\t\t\t// mutable.\n\t\t\treadonly dedupe?: string[];\n\t\t};\n\t\treadonly optimizeDeps?: { readonly include: string[] };\n\t\t/** `define` injecting the on-chain ids as `__DEVSTACK_IDS__`. In a\n\t\t * prod `build` this is the static id literal; in dev `serve` it is a\n\t\t * reference to the `globalThis.__DEVSTACK_IDS_LIVE__` runtime global\n\t\t * the `transformIndexHtml` hook sets fresh per page load, so a\n\t\t * republished id reaches the app on reload (`define` is fixed for the\n\t\t * dev server's lifetime and could never hot-update). */\n\t\treadonly define: Record<string, string>;\n\t};\n\t/** Capture the resolved command so injection is DEV-only. */\n\treadonly configResolved: (config: ViteUserConfigLike) => void;\n\t/** Dev-only: watch the live `devstack-ids.json` and full-reload the page\n\t * when it changes, so a republished package id (rewritten by the\n\t * supervisor's post-acquire hook) reaches the running app. */\n\treadonly configureServer: (server: ViteDevServerLike) => void;\n\t/** Resolve the dev-wallet virtual module. */\n\treadonly resolveId: (id: string) => string | undefined;\n\t/** Emit the dev-wallet register module (or a no-op when not applicable). */\n\treadonly load: (id: string) => string | undefined;\n\t/** Inject the dev-only HTML tags: the `__DEVSTACK_IDS_LIVE__` global\n\t * (read fresh from the ids file per request) and, when the app carries\n\t * the dev wallet, the `<script type=\"module\">` importing the virtual\n\t * module. The return shape mirrors a structural subset of Vite's\n\t * `IndexHtmlTransformResult` so the plugin stays assignable to Vite's\n\t * `Plugin` without devstack importing `vite`. */\n\treadonly transformIndexHtml: (html: string) =>\n\t\t| {\n\t\t\t\thtml: string;\n\t\t\t\ttags: Array<{\n\t\t\t\t\ttag: string;\n\t\t\t\t\tattrs?: Record<string, string | boolean>;\n\t\t\t\t\tchildren?: string;\n\t\t\t\t\tinjectTo: 'head' | 'head-prepend';\n\t\t\t\t}>;\n\t\t }\n\t\t| undefined;\n}\n\n/** Best-effort, SYNC read of a string-valued field at `dottedPath` (e.g.\n * `'codegen.idsFile'`, `'identity.network'`) from the active stack's\n * manifest. Walks each path segment as a nested object, returning the leaf\n * value, or `null` on ANY miss (absent / partially-written / version-\n * mismatched manifest, a non-object hop, a missing or non-string/empty\n * leaf). We read + `JSON.parse` directly (rather than the schema-decoding\n * `readStackContext`, which drops the `codegen` field in its projection\n * and throws on a version mismatch) so an out-of-date or partially-written\n * manifest degrades gracefully instead of crashing the dev server. Never\n * throws — the single discover→parse→guard→degrade-to-null reader every\n * manifest field above (`codegen.idsFile`, `codegen.extrasDir`,\n * `identity.network`) routes through. */\nconst readManifestField = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n\tdottedPath: string,\n): string | null => {\n\ttry {\n\t\tconst { stack, stateDir } = resolveDiscoveryEnv(env, { cwd });\n\t\tconst manifestPath = discoverManifestPath({ env, stack, stateDir, cwd });\n\t\tif (manifestPath === undefined || !existsSync(manifestPath)) return null;\n\t\tlet node: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'));\n\t\tfor (const segment of dottedPath.split('.')) {\n\t\t\tif (typeof node !== 'object' || node === null) return null;\n\t\t\tnode = (node as Record<string, unknown>)[segment];\n\t\t}\n\t\treturn typeof node === 'string' && node.length > 0 ? node : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** The gitignored `devstack-ids.json` path the boot wrote for the active\n * stack (`codegen.idsFile`), or `null` on any miss. */\nconst readIdsFileFromManifest = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n): string | null => readManifestField(env, cwd, 'codegen.idsFile');\n\n/** SYNC read + schema-decode of an id-config FILE. The MISSING-file case\n * (absent path, no file on disk) collapses to `null` so the Vite config\n * load degrades gracefully when no stack has booted / no committed file is\n * wired. A PRESENT-but-malformed file is NOT swallowed: it flows through\n * the shared {@link decodeIdConfig}, which THROWS on bad JSON or a shape\n * that violates `IdConfigSchema` — surfacing a genuinely broken committed\n * id-config at config-load instead of silently injecting `null`. */\nconst readIdConfigFile = (idsFile: string | null): unknown => {\n\tif (idsFile === null || !existsSync(idsFile)) return null;\n\treturn decodeIdConfig(readFileSync(idsFile, 'utf8'));\n};\n\n/** Resolve the on-chain ids to inject as the `__DEVSTACK_IDS__` global —\n * ALWAYS from an id-config FILE (same schema in dev and prod), never a\n * JSON env blob. Dev (`serve`): the live `devstack-ids.json` (via the\n * manifest `codegen.idsFile`). Prod (`build`): the committed known-\n * deployment file — the plugin `ids` option, else the `DEVSTACK_IDS_FILE`\n * env (a PATH pointer, not data). Neither ⇒ `null`, so the generated\n * resolver throws loudly at id-access time. */\nconst resolveInjectedIds = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\troot: string,\n\tcommand: 'build' | 'serve' | undefined,\n\tidsOption: string | undefined,\n): unknown => {\n\t// Prod build: the known deployment's committed id-config file. Option\n\t// wins; else a `DEVSTACK_IDS_FILE` path pointer. Relative → Vite root.\n\tif (command === 'build') {\n\t\tconst pointer = idsOption ?? env['DEVSTACK_IDS_FILE'];\n\t\tif (pointer === undefined || pointer.length === 0) return null;\n\t\treturn readIdConfigFile(isAbsolute(pointer) ? pointer : resolve(root, pointer));\n\t}\n\t// Dev serve (and config-load default): the live id-config file.\n\treturn readIdConfigFile(readIdsFileFromManifest(env, root));\n};\n\n/** The manifest-recorded dev-extras tree (`codegen.extrasDir`) the\n * `@devstack-dev` alias points at for the active stack, or `null` on any\n * miss. */\nconst readExtrasDirFromManifest = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n): string | null => readManifestField(env, cwd, 'codegen.extrasDir');\n\n/**\n * Build the devstack Vite plugin that aliases `options.alias`\n * (default `@generated`) at the committed `<root>/src/generated` tree.\n *\n * // vite.config.ts\n * import { devstackVitePlugin } from '@mysten-incubation/devstack/vite';\n * export default defineConfig({ plugins: [devstackVitePlugin()] });\n * // or devstackVitePlugin({ alias: '@gen' })\n *\n * The plugin's `config` hook merges `resolve.alias[<prefix>] = <dir>`\n * into the user config (Vite deep-merges the returned partial). Sync;\n * reads `process.env` + the manifest (for `__DEVSTACK_IDS__` / the\n * `@devstack-dev` extras dir) once at config-load.\n */\n/** Derive the cold-start fallback dev-extras dir\n * (`<root>/.devstack/stacks/<stack>/generated-extras`) for the active\n * stack — used when no manifest / `codegen.extrasDir` is on disk yet.\n * Mirrors `output-location.ts`'s `extrasDirFor`. Best-effort: a\n * discovery failure collapses to the `default` stack name. */\nconst fallbackExtrasDir = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\troot: string,\n): string => {\n\tlet stack = 'default';\n\ttry {\n\t\tstack = resolveDiscoveryEnv(env, { cwd: root }).stack;\n\t} catch {\n\t\t// keep the `default` fallback.\n\t}\n\treturn resolve(root, '.devstack', 'stacks', stack, 'generated-extras');\n};\n\n/** Resolve the dev-extras dir for the active stack (shared by the alias\n * config and the dev-wallet virtual module). Explicit option wins, else\n * manifest `codegen.extrasDir`, else the derived cold-start path. */\nconst resolveExtrasDir = (\n\toptions: DevstackVitePluginOptions,\n\troot: string,\n\tenv: Readonly<Record<string, string | undefined>>,\n): string => {\n\tconst explicitExtras = options.extrasDir;\n\treturn explicitExtras !== undefined\n\t\t? resolve(root, explicitExtras)\n\t\t: (readExtrasDirFromManifest(env, root) ?? fallbackExtrasDir(env, root));\n};\n\n/** Read the `DEVSTACK_AUTO_APPROVE` env (`'1'`/`'true'`, case-insensitive). */\nconst autoApproveFromEnv = (env: Readonly<Record<string, string | undefined>>): boolean => {\n\tconst raw = env['DEVSTACK_AUTO_APPROVE'];\n\treturn raw === '1' || raw?.toLowerCase() === 'true';\n};\n\n/** Lit packages deduped to a single instance. The injected dev-wallet UI and\n * the app's dapp-kit UI are both Lit-based; if Vite loads two Lit copies they\n * register custom elements in separate realms, and the second realm's element\n * classes are unknown to the global `customElements` registry — so re-rendering\n * the dev-wallet UI (e.g. on disconnect/reconnect) throws `Illegal constructor`\n * and leaves the app in an unusable connection state. */\nconst LIT_DEDUPE = ['lit', 'lit-html', 'lit-element', '@lit/reactive-element'] as const;\n\n/** Filter {@link LIT_DEDUPE} to the packages actually installed at the app\n * root (`<root>/node_modules/<pkg>`). `resolve.dedupe` forces Vite to\n * resolve each listed package from the ROOT copy — but under pnpm's strict\n * layout a package is only surfaced at the root when the app declares it as\n * a direct dependency. Listing a package that is merely a phantom\n * (transitive-only, reachable solely under `@mysten/dapp-kit-core`'s nested\n * store dir) makes Vite's resolver look for a root copy that does not exist\n * and FAIL the production build with `Rollup failed to resolve import\n * \"lit\"`. So we dedupe only what the app truly hoists: the `app` template\n * declares `lit` (all dapp-kit-core Lit usage routes through the `lit`\n * meta-package, so one `lit` ⇒ one nested `@lit/reactive-element`); the\n * sub-packages stay phantom and are correctly dropped here. */\nconst resolvableLitDedupe = (root: string): string[] =>\n\tLIT_DEDUPE.filter((pkg) => existsSync(resolve(root, 'node_modules', ...pkg.split('/'))));\n\n/** The dev-wallet entry points the injected virtual module imports. They are\n * reached only through the `<script>` this plugin adds in `transformIndexHtml`,\n * so Vite's initial dep scan never sees them and re-optimizes mid-session the\n * first time the page loads them — and that late, separate optimize pass pulls\n * a SECOND Lit instance (see {@link LIT_DEDUPE}). Pre-bundling them up front via\n * `optimizeDeps.include` keeps the whole dev-wallet UI graph in the initial pass,\n * sharing one Lit. */\nconst DEV_WALLET_OPTIMIZE_ENTRIES = [\n\t'@mysten-incubation/dev-wallet/inject',\n\t'@mysten-incubation/dev-wallet/adapters',\n] as const;\n\n/** True when `@mysten-incubation/dev-wallet` is installed at the app root —\n * i.e. the app actually depends on the dev wallet (the `app` template does;\n * the headless `ts` template does not). Checked by package presence rather\n * than `require.resolve('.../inject')`, whose `exports` entry declares only an\n * `import` condition (the CJS resolver throws `ERR_PACKAGE_PATH_NOT_EXPORTED`).\n * Best-effort: if it's absent we never hand Vite an `optimizeDeps.include` it\n * can't resolve (which would fail the dep scan). */\nconst devWalletInstalled = (root: string): boolean =>\n\texistsSync(resolve(root, 'node_modules', '@mysten-incubation', 'dev-wallet', 'package.json'));\n\nexport const devstackVitePlugin = (options: DevstackVitePluginOptions = {}): DevstackVitePlugin => {\n\tconst alias = options.alias ?? DEFAULT_GENERATED_ALIAS;\n\tconst devExtrasAlias = options.devExtrasAlias ?? DEFAULT_DEV_EXTRAS_ALIAS;\n\tconst injectDevWallet = options.injectDevWallet ?? true;\n\n\t// Captured across hooks. `config` runs first (alias resolution),\n\t// `configResolved` records the command (DEV-gate), and `load` re-reads\n\t// the dev-wallet config off `extrasDir`.\n\tlet resolvedRoot = process.cwd();\n\tlet resolvedExtrasDir: string | null = null;\n\tlet isServe = false;\n\n\treturn {\n\t\tname: 'devstack:generated-alias',\n\t\tconfig: (config: ViteUserConfigLike, configEnv?: ViteConfigEnvLike) => {\n\t\t\tconst root = config.root ?? process.cwd();\n\t\t\tresolvedRoot = root;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// `command` comes from the second hook arg (`{ command, mode }`);\n\t\t\t// fall back to `config.command` (some callers pass it on the\n\t\t\t// config). Default the UNKNOWN case to `build` (build-safe): only an\n\t\t\t// EXPLICIT `serve` takes the live local-stack id-injection path, so a\n\t\t\t// programmatic `vite.build()` that omits the env arg never bakes\n\t\t\t// dev-stack ids into a production bundle.\n\t\t\tconst command = configEnv?.command ?? config.command ?? 'build';\n\t\t\t// Explicit `generatedDir` wins (relative → resolved against the\n\t\t\t// Vite root). Otherwise always the committed `<root>/src/generated`\n\t\t\t// tree — the single source of bindings written by `devstack\n\t\t\t// codegen`; ids resolve at runtime via `__DEVSTACK_IDS__`, so the\n\t\t\t// same tree serves every stack.\n\t\t\tconst explicit = options.generatedDir;\n\t\t\tconst generatedDir =\n\t\t\t\texplicit !== undefined ? resolve(root, explicit) : resolve(root, GENERATED_SUBPATH);\n\t\t\t// `@devstack-dev` mirrors `@generated` exactly.\n\t\t\tconst extrasDir = resolveExtrasDir(options, root, env);\n\t\t\tresolvedExtrasDir = extrasDir;\n\t\t\t// Return a partial config; Vite deep-merges it. `resolve.dedupe`\n\t\t\t// pins a single Lit copy, and — when this app carries the dev\n\t\t\t// wallet — we pre-bundle the injected dev-wallet entries so Vite\n\t\t\t// never re-optimizes them mid-session into a second Lit realm\n\t\t\t// (see the constants above).\n\t\t\tconst includeDevWallet = injectDevWallet && devWalletInstalled(root);\n\t\t\t// Inject the on-chain ids the generated `config-runtime.ts` resolver\n\t\t\t// reads as `__DEVSTACK_IDS__`.\n\t\t\t// - PROD build: bake the static literal.\n\t\t\t// - DEV server (`vite dev`): point the identifier at the\n\t\t\t// `__DEVSTACK_IDS_LIVE__` runtime global the `transformIndexHtml`\n\t\t\t// hook sets fresh per page load — `define` is fixed for the dev\n\t\t\t// server's lifetime, so baking the value would pin the FIRST id\n\t\t\t// forever and a republish would never reach the app. A distinct\n\t\t\t// global name avoids a single-pass `define` token collision with\n\t\t\t// `__DEVSTACK_IDS__`.\n\t\t\t// - VITEST: vitest ALSO reports `command === 'serve'`, but it runs\n\t\t\t// no `transformIndexHtml` (no HTML), so the live global is never\n\t\t\t// set — and esbuild's stricter `define` rejects an operator\n\t\t\t// expression anyway. Bake a literal instead: the Vite config loads\n\t\t\t// BEFORE the test stack boots, so `injectedIds` is null and the\n\t\t\t// resolver falls back to the `DEVSTACK_IDS_FILE` env the vitest\n\t\t\t// globalSetup points at the freshly-booted stack (see\n\t\t\t// vitest/global-setup.ts).\n\t\t\t// The define VALUE must be esbuild-valid (a JSON literal or a\n\t\t\t// member-access chain) — note the bare `globalThis.…`, NOT a\n\t\t\t// parenthesised `(… ?? null)`; `config-runtime.ts` already guards the\n\t\t\t// `typeof … === 'undefined'` case.\n\t\t\tconst injectedIds = resolveInjectedIds(env, root, command, options.ids);\n\t\t\tconst isVitest = env['VITEST'] !== undefined;\n\t\t\tconst idsDefine =\n\t\t\t\tcommand === 'serve' && !isVitest\n\t\t\t\t\t? 'globalThis.__DEVSTACK_IDS_LIVE__'\n\t\t\t\t\t: JSON.stringify(injectedIds ?? null);\n\t\t\treturn {\n\t\t\t\tresolve: {\n\t\t\t\t\t// A bare-prefix alias (no trailing `/`) matches both\n\t\t\t\t\t// `@generated` and `@generated/foo.js` under Vite's default\n\t\t\t\t\t// string-alias resolution.\n\t\t\t\t\talias: {\n\t\t\t\t\t\t[alias]: generatedDir,\n\t\t\t\t\t\t[devExtrasAlias]: extrasDir,\n\t\t\t\t\t},\n\t\t\t\t\tdedupe: resolvableLitDedupe(root),\n\t\t\t\t},\n\t\t\t\tdefine: {\n\t\t\t\t\t__DEVSTACK_IDS__: idsDefine,\n\t\t\t\t},\n\t\t\t\t...(includeDevWallet\n\t\t\t\t\t? { optimizeDeps: { include: [...DEV_WALLET_OPTIMIZE_ENTRIES] } }\n\t\t\t\t\t: {}),\n\t\t\t};\n\t\t},\n\n\t\tconfigResolved: (config: ViteUserConfigLike) => {\n\t\t\t// DEV-gate: only a `serve` command injects the dev wallet. A\n\t\t\t// production `vite build` injects nothing.\n\t\t\tisServe = config.command !== 'build';\n\t\t},\n\n\t\tresolveId: (id: string) => {\n\t\t\tif (id === VIRTUAL_DEV_WALLET_ID) return RESOLVED_VIRTUAL_DEV_WALLET_ID;\n\t\t\treturn undefined;\n\t\t},\n\n\t\tload: (id: string): string | undefined => {\n\t\t\tif (id !== RESOLVED_VIRTUAL_DEV_WALLET_ID) return undefined;\n\t\t\t// Graceful no-op when injection is off, not a dev serve, or the\n\t\t\t// dev-extras config is absent (no `devstack apply` yet).\n\t\t\tconst extrasDir = resolvedExtrasDir ?? resolveExtrasDir(options, resolvedRoot, process.env);\n\t\t\tconst devWalletFile = resolve(extrasDir, 'dev-wallet.ts');\n\t\t\tconst accountsFile = resolve(extrasDir, 'accounts.ts');\n\t\t\tif (!injectDevWallet || !isServe || !existsSync(devWalletFile) || !existsSync(accountsFile)) {\n\t\t\t\treturn 'export {};';\n\t\t\t}\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// Auto-approve resolution (highest precedence first): explicit\n\t\t\t// `options.autoApprove`, then the `DEVSTACK_AUTO_APPROVE` env, then\n\t\t\t// the active network's `autoApproveSigning` per-network policy. The\n\t\t\t// policy is ON for every network EXCEPT live `mainnet`, so a stack\n\t\t\t// booted against real mainnet never silently auto-approves a\n\t\t\t// real-funds signature. The active network comes from the manifest\n\t\t\t// boot wrote (`identity.network`); absent that we conservatively\n\t\t\t// resolve as `mainnet` (auto-approve OFF) rather than assuming a\n\t\t\t// dev network. The override RECORD isn't on disk — `networkOptions`\n\t\t\t// is read from the plugin option when the app threads it.\n\t\t\tconst network = readManifestField(env, resolvedRoot, 'identity.network') ?? 'mainnet';\n\t\t\tconst netOpts = resolveNetworkOptions(network, options.networkOptions);\n\t\t\tconst autoApprove =\n\t\t\t\toptions.autoApprove ?? (autoApproveFromEnv(env) || netOpts.autoApproveSigning);\n\t\t\t// Re-export the generated config through the `@devstack-dev`\n\t\t\t// alias (already wired in `config`), parse the token, and\n\t\t\t// register the page wallet on load. Kept as source the dev\n\t\t\t// server transpiles — it imports `@mysten-incubation/dev-wallet`\n\t\t\t// + the generated extras, both resolvable in the app graph.\n\t\t\treturn [\n\t\t\t\t`import { registerDevstackDevWallet } from '@mysten-incubation/dev-wallet/inject';`,\n\t\t\t\t`import { parseDevstackToken } from '@mysten-incubation/dev-wallet/adapters';`,\n\t\t\t\t`import { devWallet } from '${devExtrasAlias}/dev-wallet.js';`,\n\t\t\t\t`import { accounts } from '${devExtrasAlias}/accounts.js';`,\n\t\t\t\t// The wallet EXECUTES (and simulates) against the same routed\n\t\t\t\t// RPC the app's dApp Kit client uses — a raw 127.0.0.1 RPC is\n\t\t\t\t// CORS-blocked from the routed page origin. Sourced from the\n\t\t\t\t// generated runtime config (active network's `rpc`).\n\t\t\t\t`import { config as __devstackConfig } from '${alias}/config.js';`,\n\t\t\t\t`registerDevstackDevWallet({`,\n\t\t\t\t` serverOrigin: devWallet.walletUrl,`,\n\t\t\t\t` token: parseDevstackToken(devWallet.pairUrl),`,\n\t\t\t\t` accounts,`,\n\t\t\t\t` rpcUrl: __devstackConfig.networks[__devstackConfig.network].rpc,`,\n\t\t\t\t` network: devWallet.network,`,\n\t\t\t\t` autoApprove: ${autoApprove ? 'true' : 'false'},`,\n\t\t\t\t` mountUI: true,`,\n\t\t\t\t`}).catch((err) => console.error('[devstack] dev-wallet injection failed:', err));`,\n\t\t\t].join('\\n');\n\t\t},\n\n\t\tconfigureServer: (server: ViteDevServerLike) => {\n\t\t\tif (!isServe) return;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\tconst idsFile = readIdsFileFromManifest(env, resolvedRoot);\n\t\t\tif (idsFile === null) return;\n\t\t\t// Watch the live id-config and full-reload on change. The supervisor's\n\t\t\t// post-acquire hook rewrites this file whenever a package (re)publishes\n\t\t\t// (e.g. after a Move-source edit fires the file watcher → selective\n\t\t\t// restart); a reload re-runs `transformIndexHtml`, which re-reads the\n\t\t\t// file into `__DEVSTACK_IDS_LIVE__`, so the app picks up the new id.\n\t\t\tserver.watcher.add(idsFile);\n\t\t\tconst reloadOnIdsChange = (changed: string): void => {\n\t\t\t\t// Chokidar fires absolute paths for an absolute `watcher.add`, so\n\t\t\t\t// `resolve(changed)` is normally a no-op; resolve against the Vite\n\t\t\t\t// root anyway so a relative event path can't silently miss.\n\t\t\t\tif (resolve(resolvedRoot, changed) !== resolve(idsFile)) return;\n\t\t\t\tconst channel = server.hot ?? server.ws;\n\t\t\t\tchannel?.send({ type: 'full-reload' });\n\t\t\t};\n\t\t\tserver.watcher.on('change', reloadOnIdsChange);\n\t\t\tserver.watcher.on('add', reloadOnIdsChange);\n\t\t},\n\n\t\ttransformIndexHtml: (html: string) => {\n\t\t\tif (!isServe) return undefined;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// Read the live ids FRESH per request so a full-reload after a\n\t\t\t// republish injects the new id. Set on a distinct global\n\t\t\t// (`__DEVSTACK_IDS_LIVE__`) the `config` hook's `define` points\n\t\t\t// `__DEVSTACK_IDS__` at; `head-prepend` runs it before any app module.\n\t\t\tconst liveIds = resolveInjectedIds(env, resolvedRoot, 'serve', options.ids);\n\t\t\tconst tags: Array<{\n\t\t\t\ttag: string;\n\t\t\t\tattrs?: Record<string, string | boolean>;\n\t\t\t\tchildren?: string;\n\t\t\t\tinjectTo: 'head' | 'head-prepend';\n\t\t\t}> = [\n\t\t\t\t{\n\t\t\t\t\ttag: 'script',\n\t\t\t\t\tchildren: `globalThis.__DEVSTACK_IDS_LIVE__ = ${JSON.stringify(liveIds ?? null)};`,\n\t\t\t\t\tinjectTo: 'head-prepend',\n\t\t\t\t},\n\t\t\t];\n\t\t\t// Gate the script tag on the app actually carrying the wallet (same\n\t\t\t// condition as the `config` hook's `optimizeDeps` include), so a\n\t\t\t// headless app emits zero dev-wallet plumbing rather than a tag that\n\t\t\t// resolves to the no-op virtual module.\n\t\t\tif (injectDevWallet && devWalletInstalled(resolvedRoot)) {\n\t\t\t\ttags.push({\n\t\t\t\t\ttag: 'script',\n\t\t\t\t\tattrs: { type: 'module', src: VIRTUAL_DEV_WALLET_SCRIPT_SRC },\n\t\t\t\t\tinjectTo: 'head',\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { html, tags };\n\t\t},\n\t};\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/build-integrations/vite/index.ts"],"sourcesContent":["// Vite build-integration — `@generated` alias plugin.\n//\n// App source imports Move codegen through a configurable alias prefix\n// (default `@generated`) instead of `./generated`. This plugin points\n// that alias at the committed `<root>/src/generated` tree — the ONE\n// source of bindings, written only by the stack-free `devstack codegen`\n// verb. On-chain ids are NOT baked into that tree; they resolve at\n// runtime via the `__DEVSTACK_DEPLOYMENT__` global (see\n// `mergeDeployment`), so the same generated source serves every\n// stack. Resolution:\n// 1. `options.generatedDir` — explicit escape hatch (relative → root).\n// 2. `<root>/src/generated` — the committed tree, always.\n//\n// Because Playwright's `webServer` runs the app's OWN Vite as a child,\n// the same plugin serves both `pnpm dev` and the e2e dev server\n// automatically. Vitest has its own Vite pipeline, so apps add this\n// plugin to `vitest.config.ts` too — and resolve `@generated` to the\n// same committed tree (the per-stack live tree no longer exists).\n//\n// SYNC + dependency-light, mirroring the playwright/vitest helpers: NO\n// heavy imports at module top-level, and `vite` is NOT imported (it is\n// an app-side dev dependency, not a devstack runtime dep). The return\n// value is a STRUCTURAL Vite `Plugin` — a `{ name, config }` object —\n// typed loosely so this module loads without `vite` installed. The\n// manifest read goes through the same `build-integrations/runtime`\n// machinery the playwright/vitest integrations use.\n\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { discoverManifestPath, resolveDiscoveryEnv } from '../runtime/index.ts';\nimport { resolveNetworkOptions } from '../../orchestrators/network-options.ts';\nimport {\n\tdecodeDeployment,\n\tdecodeNetworkDeployment,\n\ttype DevstackDeployment,\n\ttype NetworkDeployment,\n} from '../../orchestrators/codegen/deployment.ts';\n\n/** Default import-alias prefix. Customizable via `options.alias` (some\n * apps prefer `@gen`, `~generated`, …). The app MUST use the SAME\n * prefix in three derivable places: this plugin option, the\n * `tsconfig` `paths` entry, and its import specifiers. */\nexport const DEFAULT_GENERATED_ALIAS = '@generated';\n\n/** The committed generated-bindings subpath, relative to the Vite root.\n * The single source of bindings — written by `devstack codegen` — that\n * `@generated` always resolves to (absent the `options.generatedDir`\n * escape hatch). */\nconst GENERATED_SUBPATH = 'src/generated';\n\nexport interface DevstackVitePluginOptions {\n\t/** Import-alias prefix. Default `'@generated'`. */\n\treadonly alias?: string;\n\t/** Explicit generated dir — bypasses manifest discovery entirely\n\t * (escape hatch for unusual layouts / tests). Relative paths\n\t * resolve against the Vite root. */\n\treadonly generatedDir?: string;\n\t/** Inject + register the devstack dev wallet on the page in DEV\n\t * (wallet-standard, so dApp Kit auto-discovers it). Defaults to\n\t * `true`. Production builds (`command === 'build'`) inject nothing\n\t * regardless. */\n\treadonly injectDevWallet?: boolean;\n\t/** Auto-approve all dev-wallet signing requests (headless Playwright /\n\t * in-app \"Open/Join as\" buttons). Defaults to the `DEVSTACK_AUTO_APPROVE`\n\t * env (`'1'`/`'true'`), then to the active network's `autoApproveSigning`\n\t * per-network policy (OFF by default on every network — a normal `pnpm dev`\n\t * shows the real connect + approve UX; opt in per-network). A single\n\t * switch, replacing per-app `VITE_*_AUTO_APPROVE`.\n\t *\n\t * PRECEDENCE: an explicit value here ALWAYS wins — passing `autoApprove:\n\t * false` forces it off even when `DEVSTACK_AUTO_APPROVE` is set, and\n\t * `autoApprove: true` forces it on. The `DEVSTACK_AUTO_APPROVE` env and the\n\t * per-network policy default apply ONLY when this option is left undefined.\n\t *\n\t * HARD-CLAMP: on live `mainnet` the per-network policy default forces\n\t * auto-approve OFF — a real-funds signature is NEVER granted without a\n\t * human in the loop. The policy default never silently auto-approves on\n\t * mainnet; an explicit `autoApprove: true` (or, when this option is\n\t * undefined, `DEVSTACK_AUTO_APPROVE`) is still the author opting in\n\t * deliberately. */\n\treadonly autoApprove?: boolean;\n\t/** Per-network overrides for the dev-convenience policy (the same\n\t * `networkOptions` shape `defineDevstack` takes, forwarded verbatim).\n\t * Only the `autoApproveSigning` field is read here, to resolve the\n\t * auto-approve default for the active network. Omitted ⇒ the built-in\n\t * policy (OFF by default; opt in per-network) applies. The override RECORD is not\n\t * otherwise on disk, so an app that customizes per-network signing must\n\t * thread it here explicitly; the mainnet hard-clamp holds regardless. */\n\treadonly networkOptions?: Readonly<Record<string, unknown>>;\n\t/** Committed, per-network PROD deployments — `{ <net>: () => import('./deployments/<net>.ts') }`,\n\t * each thunk resolving to `{ deployment: NetworkDeployment }` (the typed\n\t * hand-written / `dump-deployment`-emitted file). These are the\n\t * non-local networks a production `build` ships, and the additional\n\t * networks a dev `serve` makes selectable alongside the live local one.\n\t * Each thunk's `deployment` is validated against `NetworkDeploymentSchema`\n\t * at config-load — a malformed committed file fails LOUDLY rather than\n\t * silently injecting a broken network.\n\t *\n\t * DEFAULT (when omitted): AUTO-DISCOVERY of `<root>/deployments/*.ts` (D7\n\t * — \"just drop a file\"). Each filename (sans `.ts`) becomes a live network\n\t * name keyed to a dynamic-`import()` thunk reading the file's `deployment`\n\t * export. Supplying this option OVERRIDES auto-discovery entirely (a\n\t * custom dir / explicit paths). No `deployments/` dir + no option ⇒ no\n\t * committed networks. */\n\treadonly deployments?: Readonly<Record<string, () => Promise<{ deployment: NetworkDeployment }>>>;\n\t/** The network the app opens on when no live local network is present\n\t * (a pure production build, or a dev serve with only committed\n\t * networks). Defaults to the first committed network key. The live\n\t * local network always wins as the default in dev. */\n\treadonly defaultNetwork?: string;\n}\n\n/** A `vite` `Plugin`'s `config` hook receives the partial user config.\n * We only ever read `config.root` and return a `resolve.alias` patch,\n * so a one-field structural subset is enough — this keeps the module\n * loadable without `vite` types (mirroring how the playwright/vitest\n * presets avoid importing their optional peer at module init). */\ninterface ViteUserConfigLike {\n\treadonly root?: string;\n\treadonly command?: 'build' | 'serve';\n}\n\n/** Virtual module id the dev-only HTML injection imports. Vite convention:\n * a `virtual:` specifier resolved to a `\\0`-prefixed id in `resolveId`. */\nconst VIRTUAL_DEV_WALLET_ID = 'virtual:devstack-dev-wallet';\nconst RESOLVED_VIRTUAL_DEV_WALLET_ID = '\\0virtual:devstack-dev-wallet';\n/** The `/@id/` URL form Vite's middleware understands for a `\\0`-prefixed\n * virtual id: the NUL byte is encoded as the literal `__x00__` token (a\n * raw `\\0` in an HTML attribute is mangled to a space, which Vite then\n * misroutes to the SPA fallback). Used for the injected `<script src>`. */\nconst VIRTUAL_DEV_WALLET_SCRIPT_SRC = `/@id/__x00__${VIRTUAL_DEV_WALLET_ID}`;\n\n/** The structural `Plugin` shape we return — `name`, a `config` hook, and\n * the DEV-only dev-wallet-injection hooks. Typed as the loose object Vite\n * accepts (Vite's `Plugin` is structurally compatible) so callers spread\n * it into `plugins: []` without devstack depending on `vite`. */\n/** Vite's `config` hook second arg — `{ command, mode }`. We read only\n * `command` to pick the dev vs prod id-injection source. */\ninterface ViteConfigEnvLike {\n\treadonly command?: 'build' | 'serve';\n}\n\n/** The structural subset of Vite's `ViteDevServer` the `configureServer`\n * hook uses: the chokidar `watcher` (to track the ids file) and the HMR\n * channel (to push a full reload). `ws` is Vite's long-stable channel;\n * `hot` is the newer alias — we use whichever the running Vite exposes. */\ninterface ViteDevServerLike {\n\treadonly watcher: {\n\t\tadd: (paths: string | ReadonlyArray<string>) => void;\n\t\ton: (event: 'change' | 'add', listener: (path: string) => void) => void;\n\t};\n\treadonly ws?: { send: (payload: { type: 'full-reload' }) => void };\n\treadonly hot?: { send: (payload: { type: 'full-reload' }) => void };\n}\n\nexport interface DevstackVitePlugin {\n\treadonly name: string;\n\treadonly config: (\n\t\tconfig: ViteUserConfigLike,\n\t\tenv?: ViteConfigEnvLike,\n\t) => Promise<{\n\t\treadonly resolve: {\n\t\t\treadonly alias: Record<string, string>;\n\t\t\t// Mutable `string[]` (NOT `readonly`): Vite's `ResolveOptions.dedupe`\n\t\t\t// and `DepOptimizationConfig.include` are both `string[] | undefined`,\n\t\t\t// and a `readonly string[]` return makes the whole `config` hook\n\t\t\t// unassignable to Vite's `Plugin` type in a consuming app's\n\t\t\t// `vite.config.ts`/`vitest.config.ts`. The value we return\n\t\t\t// (`resolvableLitDedupe(root)`, a freshly built array) is already\n\t\t\t// mutable.\n\t\t\treadonly dedupe?: string[];\n\t\t};\n\t\treadonly optimizeDeps?: { readonly include: string[] };\n\t\t/** `define` injecting the deployment envelope as\n\t\t * `__DEVSTACK_DEPLOYMENT__`. In a prod `build` this is the static\n\t\t * envelope literal; in dev `serve` it is a reference to the\n\t\t * `globalThis.__DEVSTACK_DEPLOYMENT_LIVE__` runtime global the\n\t\t * `transformIndexHtml` hook sets fresh per page load, so a republished\n\t\t * id reaches the app on reload (`define` is fixed for the dev server's\n\t\t * lifetime and could never hot-update). */\n\t\treadonly define: Record<string, string>;\n\t}>;\n\t/** Capture the resolved command so injection is DEV-only. */\n\treadonly configResolved: (config: ViteUserConfigLike) => void;\n\t/** Dev-only: watch the live `deployment.json` and full-reload the page\n\t * when it changes, so a republished package id (rewritten by the\n\t * supervisor's post-acquire hook) reaches the running app. */\n\treadonly configureServer: (server: ViteDevServerLike) => void;\n\t/** Resolve the dev-wallet virtual module. */\n\treadonly resolveId: (id: string) => string | undefined;\n\t/** Emit the dev-wallet register module (or a no-op when not applicable). */\n\treadonly load: (id: string) => string | undefined;\n\t/** Inject the dev-only HTML tags: the `__DEVSTACK_DEPLOYMENT_LIVE__`\n\t * global (read fresh from the deployment file per request) and, when the\n\t * app carries the dev wallet, the `<script type=\"module\">` importing the\n\t * virtual module. The return shape mirrors a structural subset of Vite's\n\t * `IndexHtmlTransformResult` so the plugin stays assignable to Vite's\n\t * `Plugin` without devstack importing `vite`. */\n\treadonly transformIndexHtml: (html: string) =>\n\t\t| {\n\t\t\t\thtml: string;\n\t\t\t\ttags: Array<{\n\t\t\t\t\ttag: string;\n\t\t\t\t\tattrs?: Record<string, string | boolean>;\n\t\t\t\t\tchildren?: string;\n\t\t\t\t\tinjectTo: 'head' | 'head-prepend';\n\t\t\t\t}>;\n\t\t }\n\t\t| undefined;\n}\n\n/** Best-effort, SYNC read of a string-valued field at `dottedPath` (e.g.\n * `'codegen.deploymentFile'`, `'identity.network'`) from the active stack's\n * manifest. Walks each path segment as a nested object, returning the leaf\n * value, or `null` on ANY miss (absent / partially-written / version-\n * mismatched manifest, a non-object hop, a missing or non-string/empty\n * leaf). We read + `JSON.parse` directly (rather than the schema-decoding\n * `readStackContext`, which drops the `codegen` field in its projection\n * and throws on a version mismatch) so an out-of-date or partially-written\n * manifest degrades gracefully instead of crashing the dev server. Never\n * throws — the single discover→parse→guard→degrade-to-null reader every\n * manifest field above (`codegen.deploymentFile`, `identity.network`)\n * routes through. */\nconst readManifestObject = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n): unknown => {\n\ttry {\n\t\tconst { stack, stateDir } = resolveDiscoveryEnv(env, { cwd });\n\t\tconst manifestPath = discoverManifestPath({ env, stack, stateDir, cwd });\n\t\tif (manifestPath === undefined || !existsSync(manifestPath)) return null;\n\t\treturn JSON.parse(readFileSync(manifestPath, 'utf8'));\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Walk `dottedPath` over an already-parsed manifest object, returning the\n * non-empty string leaf or `null` on ANY miss (non-object hop, missing /\n * non-string / empty leaf). Lets one parsed manifest serve several field\n * reads without re-parsing. */\nconst pickManifestString = (manifest: unknown, dottedPath: string): string | null => {\n\tlet node = manifest;\n\tfor (const segment of dottedPath.split('.')) {\n\t\tif (typeof node !== 'object' || node === null) return null;\n\t\tnode = (node as Record<string, unknown>)[segment];\n\t}\n\treturn typeof node === 'string' && node.length > 0 ? node : null;\n};\n\nconst readManifestField = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n\tdottedPath: string,\n): string | null => pickManifestString(readManifestObject(env, cwd), dottedPath);\n\n/** The gitignored `deployment.json` path the boot wrote for the active\n * stack (`codegen.deploymentFile`), or `null` on any miss. */\nconst readIdsFileFromManifest = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\tcwd: string,\n): string | null => readManifestField(env, cwd, 'codegen.deploymentFile');\n\n/** SYNC read + schema-decode of a deployment ENVELOPE file. The MISSING-file\n * case (absent path, no file on disk) collapses to `null` so the Vite config\n * load degrades gracefully when no stack has booted / no committed file is\n * wired. A PRESENT-but-malformed file is NOT swallowed: it flows through the\n * shared {@link decodeDeployment}, which THROWS on bad JSON or a shape that\n * violates `DevstackDeploymentSchema` — surfacing a genuinely broken\n * deployment at config-load instead of silently injecting `null`. */\nconst readDeploymentFile = (file: string | null): DevstackDeployment | null => {\n\tif (file === null || !existsSync(file)) return null;\n\treturn decodeDeployment(readFileSync(file, 'utf8'));\n};\n\n/** The LIVE local-stack deployment envelope for the active stack — the\n * `deployment.json` boot wrote (via the manifest `codegen.deploymentFile`),\n * or `null` on any miss. Read fresh so a republish reaches the app. */\nconst readLiveEnvelope = (\n\tenv: Readonly<Record<string, string | undefined>>,\n\troot: string,\n): DevstackDeployment | null => readDeploymentFile(readIdsFileFromManifest(env, root));\n\n/** Resolve + validate the committed per-network deployments — the\n * `deployments` option's thunks, each `() => Promise<{ deployment }>`. Each\n * `deployment` is validated against `NetworkDeploymentSchema` (loud-fail on a\n * malformed committed file) and stamped `{ network: <key>, local: false }` so\n * the envelope key is authoritative and the deploy filter treats it as a real\n * (non-local) network. Empty when no `deployments` are supplied. */\nconst resolveCommittedNetworks = async (\n\tdeployments: Readonly<Record<string, () => Promise<{ deployment: NetworkDeployment }>>>,\n): Promise<Record<string, NetworkDeployment>> => {\n\tconst committed: Record<string, NetworkDeployment> = {};\n\tfor (const [net, thunk] of Object.entries(deployments)) {\n\t\tconst loaded = await thunk();\n\t\tconst validated = decodeNetworkDeployment(loaded.deployment, `deployments['${net}']`);\n\t\tcommitted[net] = { ...validated, network: net, local: false };\n\t}\n\treturn committed;\n};\n\n/** Auto-discover the committed per-network deployment thunks by globbing\n * `<root>/deployments/*.ts` (D7 — \"just drop a file\" UX). Each filename\n * (sans `.ts`) becomes a live network name keyed to a thunk that\n * dynamically `import()`s the absolute file path and reads its `deployment`\n * export (the `dump-deployment`-emitted / hand-written\n * `export const deployment = {…} satisfies AppNetworkDeployment`). Returns\n * `{}` when there is no `deployments/` dir (a clean clone / template — a\n * pure types-only `deployment.ts` with an empty `ProvidedDeployments`). The\n * explicit `deployments` option overrides this entirely (custom dir/paths).\n * File CONTENTS are not read here — only the import is wired; ids load when\n * Vite transforms the thunk at build/serve. */\nconst autoDiscoverDeployments = (\n\troot: string,\n): Record<string, () => Promise<{ deployment: NetworkDeployment }>> => {\n\tconst deploymentsDir = resolve(root, 'deployments');\n\tif (!existsSync(deploymentsDir)) return {};\n\tconst thunks: Record<string, () => Promise<{ deployment: NetworkDeployment }>> = {};\n\tlet files: ReadonlyArray<string>;\n\ttry {\n\t\tfiles = readdirSync(deploymentsDir);\n\t} catch {\n\t\treturn {};\n\t}\n\tfor (const file of files) {\n\t\tif (!file.endsWith('.ts') || file.endsWith('.d.ts')) continue;\n\t\tconst net = file.slice(0, -'.ts'.length);\n\t\tif (net.length === 0) continue;\n\t\tconst abs = resolve(deploymentsDir, file);\n\t\t// `/* @vite-ignore */` — the specifier is a runtime-computed absolute\n\t\t// path (not a static literal Vite/Rollup can pre-analyze). Both Vite's\n\t\t// dev server and `vite build` resolve it as a dynamic import.\n\t\tthunks[net] = () =>\n\t\t\timport(/* @vite-ignore */ pathToFileURL(abs).href) as Promise<{\n\t\t\t\tdeployment: NetworkDeployment;\n\t\t\t}>;\n\t}\n\treturn thunks;\n};\n\n/** The committed per-network deployments to merge — the explicit\n * `deployments` option when supplied, else auto-discovery of\n * `<root>/deployments/*.ts` (D7). */\nconst resolveDeploymentThunks = (\n\toptions: DevstackVitePluginOptions,\n\troot: string,\n): Record<string, () => Promise<{ deployment: NetworkDeployment }>> =>\n\toptions.deployments !== undefined ? { ...options.deployments } : autoDiscoverDeployments(root);\n\n/** The first key of a record, or `undefined` when empty. */\nconst firstKey = (record: Record<string, unknown>): string | undefined => Object.keys(record)[0];\n\n/** Merge the committed + live deployments into the envelope to inject as\n * `__DEVSTACK_DEPLOYMENT__`.\n * - `command === 'build'` (DEPLOY): the committed, NON-local networks only —\n * the live local network is dropped. Empty ⇒ `null` (the generated\n * resolver throws loudly at access time).\n * - dev `serve` (and config-load default): the committed networks OVERLAID\n * with the live local network(s), the live default winning. No live\n * envelope ⇒ committed only (default = `defaultNetwork` option / first\n * committed key).\n * The live envelope's entries are forced `local: true` (they ARE the running\n * local stack) so a later `build` would drop them. */\nconst mergeDeployment = (\n\tcommand: 'build' | 'serve' | undefined,\n\tcommitted: Record<string, NetworkDeployment>,\n\tlive: DevstackDeployment | null,\n\tdefaultNetworkOption: string | undefined,\n): DevstackDeployment | null => {\n\tif (command === 'build') {\n\t\tif (Object.keys(committed).length === 0) return null;\n\t\tconst fallback = firstKey(committed)!;\n\t\t// A pure prod build ships NO dev accounts (committed-only, network-\n\t\t// agnostic identities exist only when running through devstack).\n\t\treturn {\n\t\t\tdefaultNetwork:\n\t\t\t\tdefaultNetworkOption !== undefined && committed[defaultNetworkOption] !== undefined\n\t\t\t\t\t? defaultNetworkOption\n\t\t\t\t\t: fallback,\n\t\t\tnetworks: committed,\n\t\t\taccounts: {},\n\t\t};\n\t}\n\t// Dev serve / config-load default: overlay the live local network(s).\n\tconst networks: Record<string, NetworkDeployment> = { ...committed };\n\tif (live !== null) {\n\t\tfor (const [net, dep] of Object.entries(live.networks)) {\n\t\t\tnetworks[net] = { ...dep, network: net, local: true };\n\t\t}\n\t\t// Dev `accounts` ride the ENVELOPE (network-agnostic dev identities):\n\t\t// the live local stack supplies them; committed `deployments/*.ts`\n\t\t// networks carry none. Carry them through onto the merged envelope.\n\t\treturn {\n\t\t\tdefaultNetwork: live.defaultNetwork,\n\t\t\tnetworks,\n\t\t\taccounts: live.accounts ?? {},\n\t\t};\n\t}\n\tconst fallback = firstKey(networks);\n\tif (fallback === undefined) return null;\n\treturn {\n\t\tdefaultNetwork:\n\t\t\tdefaultNetworkOption !== undefined && networks[defaultNetworkOption] !== undefined\n\t\t\t\t? defaultNetworkOption\n\t\t\t\t: fallback,\n\t\tnetworks,\n\t\taccounts: {},\n\t};\n};\n\n/**\n * Build the devstack Vite plugin that aliases `options.alias`\n * (default `@generated`) at the committed `<root>/src/generated` tree.\n *\n * // vite.config.ts\n * import { devstackVitePlugin } from '@mysten-incubation/devstack/vite';\n * export default defineConfig({ plugins: [devstackVitePlugin()] });\n * // or devstackVitePlugin({ alias: '@gen' })\n *\n * The plugin's `config` hook merges `resolve.alias[<prefix>] = <dir>`\n * into the user config (Vite deep-merges the returned partial). Async\n * (it awaits the committed `deployments` thunks); reads `process.env` +\n * the manifest (for `__DEVSTACK_DEPLOYMENT__`) once at config-load.\n */\n\n/** The on-disk dev-wallet pairing-token for the active stack, given the\n * already-resolved `deploymentFile` path (the manifest's\n * `codegen.deploymentFile`). The token lives in a `0o600` side-channel file\n * (`<stackRoot>/wallet/token`, written by `pairing.ts`), NEVER in the\n * world-readable `deployment.json`. The boot writes `deployment.json` into the\n * SAME `<stackRoot>`, so the token sits at `<dirname(deploymentFile)>/wallet/\n * token`. Read in Node by the `load` hook (which runs server-side) so the\n * secret stays off the manifest/deployment surface. Returns `null` on any\n * miss (including a `null` deployment file). */\nconst readDevWalletToken = (deploymentFile: string | null): string | null => {\n\tif (deploymentFile === null) return null;\n\tconst tokenFile = resolve(dirname(deploymentFile), 'wallet', 'token');\n\ttry {\n\t\tif (!existsSync(tokenFile)) return null;\n\t\tconst raw = readFileSync(tokenFile, 'utf8').trim();\n\t\treturn /^[0-9a-f]{32}$/.test(raw) ? raw : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Escape a `JSON.stringify` result for safe embedding in an inline\n * `<script>` element's text. Unlike an external file, inline-script text is\n * parsed by the HTML tokenizer first, so a raw `</script>` (or `<!--`) inside\n * any string value would terminate the tag early — and a lone `<` is enough to\n * matter. We escape `<` / `>` / `&` to their `\\uXXXX` JS-string forms (valid\n * inside JSON-as-JS, and inert to the HTML tokenizer) and the U+2028/U+2029\n * line separators (legal in JSON strings but illegal as raw JS line\n * terminators in older engines). Used ONLY on the inline-script injection path\n * — the `define` build path embeds into a JS module, not HTML, so it needs no\n * such escaping. */\nconst escapeForInlineScript = (json: string): string =>\n\tjson\n\t\t.replace(/</g, '\\\\u003c')\n\t\t.replace(/>/g, '\\\\u003e')\n\t\t.replace(/&/g, '\\\\u0026')\n\t\t// U+2028 / U+2029: legal in JSON strings but illegal as raw JS line\n\t\t// terminators in older engines — matched by code point so no literal\n\t\t// separator lives in this source.\n\t\t.replace(/[\\u2028\\u2029]/g, (ch) => '\\\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0'));\n\n/** A boolean env flag is truthy when set to `'1'` or `'true'`\n * (case-insensitive). Shared by the auto-approve + e2e env readers. */\nconst isTruthyEnvFlag = (raw: string | undefined): boolean =>\n\traw === '1' || raw?.toLowerCase() === 'true';\n\n/** Read the `DEVSTACK_AUTO_APPROVE` env (`'1'`/`'true'`, case-insensitive). */\nconst autoApproveFromEnv = (env: Readonly<Record<string, string | undefined>>): boolean =>\n\tisTruthyEnvFlag(env['DEVSTACK_AUTO_APPROVE']);\n\n/** True under a Playwright e2e run (`DEVSTACK_E2E` set to `'1'`/`'true'`,\n * case-insensitive). DEDICATED signal — distinct from `DEVSTACK_AUTO_APPROVE`\n * — driving the injected `__DEVSTACK_E2E__` global. Gates dApp Kit's\n * `autoConnect` ON only under e2e, so a normal `pnpm dev` serve exercises the\n * real connect UX. A prod `vite build` never sets the env ⇒ injects `false`\n * (tree-shakeable). */\nconst e2eFromEnv = (env: Readonly<Record<string, string | undefined>>): boolean =>\n\tisTruthyEnvFlag(env['DEVSTACK_E2E']);\n\n/** Lit packages deduped to a single instance. The injected dev-wallet UI and\n * the app's dapp-kit UI are both Lit-based; if Vite loads two Lit copies they\n * register custom elements in separate realms, and the second realm's element\n * classes are unknown to the global `customElements` registry — so re-rendering\n * the dev-wallet UI (e.g. on disconnect/reconnect) throws `Illegal constructor`\n * and leaves the app in an unusable connection state. */\nconst LIT_DEDUPE = ['lit', 'lit-html', 'lit-element', '@lit/reactive-element'] as const;\n\n/** Filter {@link LIT_DEDUPE} to the packages actually installed at the app\n * root (`<root>/node_modules/<pkg>`). `resolve.dedupe` forces Vite to\n * resolve each listed package from the ROOT copy — but under pnpm's strict\n * layout a package is only surfaced at the root when the app declares it as\n * a direct dependency. Listing a package that is merely a phantom\n * (transitive-only, reachable solely under `@mysten/dapp-kit-core`'s nested\n * store dir) makes Vite's resolver look for a root copy that does not exist\n * and FAIL the production build with `Rollup failed to resolve import\n * \"lit\"`. So we dedupe only what the app truly hoists: the `app` template\n * declares `lit` (all dapp-kit-core Lit usage routes through the `lit`\n * meta-package, so one `lit` ⇒ one nested `@lit/reactive-element`); the\n * sub-packages stay phantom and are correctly dropped here. */\nconst resolvableLitDedupe = (root: string): string[] =>\n\tLIT_DEDUPE.filter((pkg) => existsSync(resolve(root, 'node_modules', ...pkg.split('/'))));\n\n/** The dev-wallet entry points the injected virtual module imports. They are\n * reached only through the `<script>` this plugin adds in `transformIndexHtml`,\n * so Vite's initial dep scan never sees them and re-optimizes mid-session the\n * first time the page loads them — and that late, separate optimize pass pulls\n * a SECOND Lit instance (see {@link LIT_DEDUPE}). Pre-bundling them up front via\n * `optimizeDeps.include` keeps the whole dev-wallet UI graph in the initial pass,\n * sharing one Lit. */\nconst DEV_WALLET_OPTIMIZE_ENTRIES = [\n\t'@mysten-incubation/dev-wallet/inject',\n\t'@mysten-incubation/dev-wallet/adapters',\n] as const;\n\n/** True when `@mysten-incubation/dev-wallet` is installed at the app root —\n * i.e. the app actually depends on the dev wallet (the `app` template does;\n * the headless `ts` template does not). Checked by package presence rather\n * than `require.resolve('.../inject')`, whose `exports` entry declares only an\n * `import` condition (the CJS resolver throws `ERR_PACKAGE_PATH_NOT_EXPORTED`).\n * Best-effort: if it's absent we never hand Vite an `optimizeDeps.include` it\n * can't resolve (which would fail the dep scan). */\nconst devWalletInstalled = (root: string): boolean =>\n\texistsSync(resolve(root, 'node_modules', '@mysten-incubation', 'dev-wallet', 'package.json'));\n\nexport const devstackVitePlugin = (options: DevstackVitePluginOptions = {}): DevstackVitePlugin => {\n\tconst alias = options.alias ?? DEFAULT_GENERATED_ALIAS;\n\tconst injectDevWallet = options.injectDevWallet ?? true;\n\n\t// Captured across hooks. `config` runs first (alias resolution + committed\n\t// deployment validation), `configResolved` records the command (DEV-gate),\n\t// and `load` reads the dev-wallet connection off the injected deployment +\n\t// the secret token off its side-channel file.\n\tlet resolvedRoot = process.cwd();\n\tlet isServe = false;\n\t// Whether the app carries the dev wallet (`devWalletInstalled(root)`),\n\t// computed ONCE in the `config` hook and reused by `transformIndexHtml` so\n\t// the per-request HTML injection doesn't re-`stat` the package on disk.\n\tlet devWalletPresent = false;\n\t// The committed per-network deployments, resolved + validated ONCE in the\n\t// async `config` hook (the thunks are async; the runtime-global path in\n\t// `transformIndexHtml` is sync). Stable for the dev server's lifetime, so\n\t// the per-request HTML injection overlays the fresh live network on top of\n\t// these without re-awaiting.\n\tlet resolvedCommittedNetworks: Record<string, NetworkDeployment> = {};\n\t// Per-republish cache of the live local envelope. `readLiveEnvelope` walks\n\t// the manifest + reads/decodes `deployment.json` on every call, but the file\n\t// only changes on a republish — which the `configureServer` watcher already\n\t// observes. So we read a PRESENT envelope once (lazily, on the first\n\t// `transformIndexHtml`) and reuse it until the watcher invalidates it, rather\n\t// than re-reading per page load. `liveEnvelopeFresh === false` is the\n\t// \"needs (re)read\" sentinel.\n\tlet liveEnvelopeFresh = false;\n\tlet liveEnvelopeCache: DevstackDeployment | null = null;\n\tconst readLiveEnvelopeCached = (\n\t\tenv: Readonly<Record<string, string | undefined>>,\n\t): DevstackDeployment | null => {\n\t\tif (liveEnvelopeFresh) return liveEnvelopeCache;\n\t\tconst live = readLiveEnvelope(env, resolvedRoot);\n\t\t// Only memoize a PRESENT envelope. A `null` result (no live stack yet)\n\t\t// must NOT stick: the `configureServer` watcher that invalidates this\n\t\t// cache only registers when a manifest already exists at server start\n\t\t// (`if (idsFile === null) return`), so a standalone `vite dev` launched\n\t\t// BEFORE `devstack up` has no invalidation hook — caching `null` here\n\t\t// would pin it forever, and the app would never pick up the stack even\n\t\t// after it boots and the page reloads. Re-reading while absent is cheap\n\t\t// (no `deployment.json` to decode) and lets the first post-boot reload\n\t\t// resolve the freshly-written envelope.\n\t\tif (live !== null) {\n\t\t\tliveEnvelopeCache = live;\n\t\t\tliveEnvelopeFresh = true;\n\t\t} else {\n\t\t\tliveEnvelopeCache = null;\n\t\t}\n\t\treturn live;\n\t};\n\n\treturn {\n\t\tname: 'devstack:generated-alias',\n\t\tconfig: async (config: ViteUserConfigLike, configEnv?: ViteConfigEnvLike) => {\n\t\t\tconst root = config.root ?? process.cwd();\n\t\t\tresolvedRoot = root;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// `command` comes from the second hook arg (`{ command, mode }`);\n\t\t\t// fall back to `config.command` (some callers pass it on the\n\t\t\t// config). Default the UNKNOWN case to `build` (build-safe): only an\n\t\t\t// EXPLICIT `serve` takes the live local-stack id-injection path, so a\n\t\t\t// programmatic `vite.build()` that omits the env arg never bakes\n\t\t\t// dev-stack ids into a production bundle.\n\t\t\tconst command = configEnv?.command ?? config.command ?? 'build';\n\t\t\t// Explicit `generatedDir` wins (relative → resolved against the\n\t\t\t// Vite root). Otherwise always the committed `<root>/src/generated`\n\t\t\t// tree — the single source of bindings written by `devstack\n\t\t\t// codegen`; ids resolve at runtime via `__DEVSTACK_DEPLOYMENT__`, so\n\t\t\t// the same tree serves every stack.\n\t\t\tconst explicit = options.generatedDir;\n\t\t\tconst generatedDir =\n\t\t\t\texplicit !== undefined ? resolve(root, explicit) : resolve(root, GENERATED_SUBPATH);\n\t\t\t// Return a partial config; Vite deep-merges it. `resolve.dedupe`\n\t\t\t// pins a single Lit copy, and — when this app carries the dev\n\t\t\t// wallet — we pre-bundle the injected dev-wallet entries so Vite\n\t\t\t// never re-optimizes them mid-session into a second Lit realm\n\t\t\t// (see the constants above).\n\t\t\t// `devWalletInstalled` stats the package on disk; do it once here and\n\t\t\t// reuse the result in `transformIndexHtml` (the root is stable for the\n\t\t\t// server's lifetime).\n\t\t\tdevWalletPresent = devWalletInstalled(root);\n\t\t\tconst includeDevWallet = injectDevWallet && devWalletPresent;\n\t\t\t// Resolve + validate the committed per-network deployments ONCE (the\n\t\t\t// thunks are async; the per-request HTML path is sync). Captured so\n\t\t\t// `transformIndexHtml` overlays the fresh live network on top of\n\t\t\t// these without re-awaiting. A malformed committed file throws here,\n\t\t\t// at config-load.\n\t\t\tresolvedCommittedNetworks = await resolveCommittedNetworks(\n\t\t\t\tresolveDeploymentThunks(options, root),\n\t\t\t);\n\t\t\t// Inject the deployment envelope the generated `config-runtime.ts`\n\t\t\t// resolver reads as `__DEVSTACK_DEPLOYMENT__`.\n\t\t\t// - PROD build: bake the static merged envelope literal (committed,\n\t\t\t// non-local networks only).\n\t\t\t// - DEV server (`vite dev`): point the identifier at the\n\t\t\t// `__DEVSTACK_DEPLOYMENT_LIVE__` runtime global the\n\t\t\t// `transformIndexHtml` hook sets fresh per page load — `define`\n\t\t\t// is fixed for the dev server's lifetime, so baking the value\n\t\t\t// would pin the FIRST id forever and a republish would never\n\t\t\t// reach the app. A distinct global name avoids a single-pass\n\t\t\t// `define` token collision with `__DEVSTACK_DEPLOYMENT__`.\n\t\t\t// - VITEST: vitest ALSO reports `command === 'serve'`, but it runs\n\t\t\t// no `transformIndexHtml` (no HTML), so the live global is never\n\t\t\t// set — and esbuild's stricter `define` rejects an operator\n\t\t\t// expression anyway. Bake a literal instead: the Vite config loads\n\t\t\t// BEFORE the test stack boots, so the merged envelope is null and\n\t\t\t// the resolver falls back to the `DEVSTACK_DEPLOYMENT_FILE` env the\n\t\t\t// vitest globalSetup points at the freshly-booted stack (see\n\t\t\t// vitest/global-setup.ts).\n\t\t\t// The define VALUE must be esbuild-valid (a JSON literal or a\n\t\t\t// member-access chain) — note the bare `globalThis.…`, NOT a\n\t\t\t// parenthesised `(… ?? null)`; `config-runtime.ts` already guards the\n\t\t\t// `typeof … === 'undefined'` case.\n\t\t\t// Merge the deployment envelope to bake/point at — reusing the\n\t\t\t// committed networks just resolved above rather than re-resolving the\n\t\t\t// thunks. `build` drops the live local stack; `serve`/config-load\n\t\t\t// overlays it.\n\t\t\tconst injectedLive = command === 'build' ? null : readLiveEnvelope(env, root);\n\t\t\tconst injectedDeployment = mergeDeployment(\n\t\t\t\tcommand,\n\t\t\t\tresolvedCommittedNetworks,\n\t\t\t\tinjectedLive,\n\t\t\t\toptions.defaultNetwork,\n\t\t\t);\n\t\t\tconst isVitest = env['VITEST'] !== undefined;\n\t\t\tconst deploymentDefine =\n\t\t\t\tcommand === 'serve' && !isVitest\n\t\t\t\t\t? 'globalThis.__DEVSTACK_DEPLOYMENT_LIVE__'\n\t\t\t\t\t: JSON.stringify(injectedDeployment ?? null);\n\t\t\treturn {\n\t\t\t\tresolve: {\n\t\t\t\t\t// A bare-prefix alias (no trailing `/`) matches both\n\t\t\t\t\t// `@generated` and `@generated/foo.js` under Vite's default\n\t\t\t\t\t// string-alias resolution.\n\t\t\t\t\talias: {\n\t\t\t\t\t\t[alias]: generatedDir,\n\t\t\t\t\t},\n\t\t\t\t\tdedupe: resolvableLitDedupe(root),\n\t\t\t\t},\n\t\t\t\tdefine: {\n\t\t\t\t\t__DEVSTACK_DEPLOYMENT__: deploymentDefine,\n\t\t\t\t\t// `true` ONLY under a Playwright e2e run (`DEVSTACK_E2E`), so the\n\t\t\t\t\t// app's dApp Kit `autoConnect` is on for headless e2e and OFF for a\n\t\t\t\t\t// normal `pnpm dev` serve. A prod `vite build` never sets the env ⇒\n\t\t\t\t\t// `false` (tree-shakeable). Distinct from `DEVSTACK_AUTO_APPROVE`.\n\t\t\t\t\t__DEVSTACK_E2E__: JSON.stringify(e2eFromEnv(env)),\n\t\t\t\t},\n\t\t\t\t...(includeDevWallet\n\t\t\t\t\t? { optimizeDeps: { include: [...DEV_WALLET_OPTIMIZE_ENTRIES] } }\n\t\t\t\t\t: {}),\n\t\t\t};\n\t\t},\n\n\t\tconfigResolved: (config: ViteUserConfigLike) => {\n\t\t\t// DEV-gate: only a `serve` command injects the dev wallet. A\n\t\t\t// production `vite build` injects nothing.\n\t\t\tisServe = config.command !== 'build';\n\t\t},\n\n\t\tresolveId: (id: string) => {\n\t\t\tif (id === VIRTUAL_DEV_WALLET_ID) return RESOLVED_VIRTUAL_DEV_WALLET_ID;\n\t\t\treturn undefined;\n\t\t},\n\n\t\tload: (id: string): string | undefined => {\n\t\t\tif (id !== RESOLVED_VIRTUAL_DEV_WALLET_ID) return undefined;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// The dev-wallet connection metadata rides the injected deployment's\n\t\t\t// `values['dev-wallet'].connection` channel (non-secret); the secret\n\t\t\t// pairing token stays in its `0o600` side-channel file, read HERE in\n\t\t\t// Node (the `load` hook runs server-side) so it never lands in the\n\t\t\t// world-readable `deployment.json`. Graceful no-op when injection is\n\t\t\t// off, not a dev serve, or no live stack has written the token yet\n\t\t\t// (no `devstack up`). The presence of BOTH the deployment file (for the\n\t\t\t// connection) and the token gate injection — the browser side resolves\n\t\t\t// the connection off the injected envelope.\n\t\t\t// Parse the manifest ONCE and read both fields it carries below — the\n\t\t\t// `codegen.deploymentFile` (locating the token side-channel) and the\n\t\t\t// `identity.network` (the active network for the auto-approve policy) —\n\t\t\t// off the same parsed object rather than discovering + parsing twice.\n\t\t\tconst manifest = readManifestObject(env, resolvedRoot);\n\t\t\tconst deploymentFile = pickManifestString(manifest, 'codegen.deploymentFile');\n\t\t\tconst token = readDevWalletToken(deploymentFile);\n\t\t\tif (!injectDevWallet || !isServe || token === null) {\n\t\t\t\treturn 'export {};';\n\t\t\t}\n\t\t\t// Auto-approve resolution (highest precedence first): explicit\n\t\t\t// `options.autoApprove`, then the `DEVSTACK_AUTO_APPROVE` env, then\n\t\t\t// the active network's `autoApproveSigning` per-network policy. The\n\t\t\t// policy defaults OFF on every network — a normal `pnpm dev` exercises\n\t\t\t// the real connect + approve UX, headless e2e sets the env, and an\n\t\t\t// author can opt a network back in via `networkOptions`. Live\n\t\t\t// `mainnet` is additionally hard-clamped OFF, so a stack booted\n\t\t\t// against real mainnet never silently auto-approves a real-funds\n\t\t\t// signature. The active network comes from the manifest\n\t\t\t// boot wrote (`identity.network`); absent that we conservatively\n\t\t\t// resolve as `mainnet` (auto-approve OFF) rather than assuming a\n\t\t\t// dev network. The override RECORD isn't on disk — `networkOptions`\n\t\t\t// is read from the plugin option when the app threads it.\n\t\t\tconst network = pickManifestString(manifest, 'identity.network') ?? 'mainnet';\n\t\t\tconst netOpts = resolveNetworkOptions(network, options.networkOptions);\n\t\t\tconst autoApprove =\n\t\t\t\toptions.autoApprove ?? (autoApproveFromEnv(env) || netOpts.autoApproveSigning);\n\t\t\t// Browser-side registration source the dev server transpiles. It reads\n\t\t\t// the dev-only account name→address map (`resolveAccounts()`) and the\n\t\t\t// non-secret dev-wallet connection (`optionalValue(dep, 'dev-wallet',\n\t\t\t// 'connection')`) off the injected deployment envelope (via the\n\t\t\t// generated `config-runtime.ts`), and the FULL network set the app\n\t\t\t// supports off the generated `config.networks` map (each with its\n\t\t\t// routed `rpc` + optional `faucet`). The wallet advertises each network\n\t\t\t// as a wallet-standard chain and operates on whichever dApp Kit has\n\t\t\t// selected, so it PERSISTS across a UI `switchNetwork` (registered once;\n\t\t\t// only the active chain changes). The token is baked in from the Node\n\t\t\t// read above — the only secret on this surface.\n\t\t\treturn [\n\t\t\t\t`import { registerDevstackDevWallet } from '@mysten-incubation/dev-wallet/inject';`,\n\t\t\t\t`import { config as __devstackConfig } from '${alias}/config.js';`,\n\t\t\t\t`import { resolveAccounts, optionalValue } from '${alias}/config-runtime.js';`,\n\t\t\t\t`const __devstackToken = ${JSON.stringify(token)};`,\n\t\t\t\t// name → address (from the injected envelope's dev `accounts`),\n\t\t\t\t// mapped to the inject API's `{ address }` shape.\n\t\t\t\t`const __devstackAccounts = Object.fromEntries(`,\n\t\t\t\t` Object.entries(resolveAccounts()).map(([__name, __address]) => [`,\n\t\t\t\t` __name,`,\n\t\t\t\t` { name: __name, address: __address },`,\n\t\t\t\t` ]),`,\n\t\t\t\t`);`,\n\t\t\t\t// The full routed network set (live local + any committed live).\n\t\t\t\t`const __devstackNetworks = Object.fromEntries(`,\n\t\t\t\t` Object.entries(__devstackConfig.networks).map(([__net, __dep]) => [`,\n\t\t\t\t` __net,`,\n\t\t\t\t` { rpc: __dep.rpc, faucet: __dep.faucet ?? null },`,\n\t\t\t\t` ]),`,\n\t\t\t\t`);`,\n\t\t\t\t// Non-secret connection (server URL + protocol paths), resolved off\n\t\t\t\t// the default network's `values['dev-wallet'].connection`. No\n\t\t\t\t// stack ⇒ undefined ⇒ no injection.\n\t\t\t\t`const __devstackDefaultDep = __devstackConfig.networks[__devstackConfig.defaultNetwork];`,\n\t\t\t\t`const __devstackConnection = __devstackDefaultDep`,\n\t\t\t\t` ? optionalValue(__devstackDefaultDep, 'dev-wallet', 'connection')`,\n\t\t\t\t` : undefined;`,\n\t\t\t\t`if (__devstackConnection) {`,\n\t\t\t\t` registerDevstackDevWallet({`,\n\t\t\t\t` serverOrigin: __devstackConnection.walletUrl,`,\n\t\t\t\t` token: __devstackToken,`,\n\t\t\t\t` accounts: __devstackAccounts,`,\n\t\t\t\t` networks: __devstackNetworks,`,\n\t\t\t\t` defaultNetwork: __devstackConfig.defaultNetwork,`,\n\t\t\t\t` autoApprove: ${autoApprove ? 'true' : 'false'},`,\n\t\t\t\t` mountUI: true,`,\n\t\t\t\t` }).catch((err) => console.error('[devstack] dev-wallet injection failed:', err));`,\n\t\t\t\t`}`,\n\t\t\t].join('\\n');\n\t\t},\n\n\t\tconfigureServer: (server: ViteDevServerLike) => {\n\t\t\tif (!isServe) return;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\tconst idsFile = readIdsFileFromManifest(env, resolvedRoot);\n\t\t\tif (idsFile === null) return;\n\t\t\t// Watch the live deployment and full-reload on change. The supervisor's\n\t\t\t// post-acquire hook rewrites this file whenever a package (re)publishes\n\t\t\t// (e.g. after a Move-source edit fires the file watcher → selective\n\t\t\t// restart); a reload re-runs `transformIndexHtml`, which re-reads the\n\t\t\t// file into `__DEVSTACK_DEPLOYMENT_LIVE__`, so the app picks up the new id.\n\t\t\tserver.watcher.add(idsFile);\n\t\t\tconst reloadOnIdsChange = (changed: string): void => {\n\t\t\t\t// Chokidar fires absolute paths for an absolute `watcher.add`, so\n\t\t\t\t// `resolve(changed)` is normally a no-op; resolve against the Vite\n\t\t\t\t// root anyway so a relative event path can't silently miss.\n\t\t\t\tif (resolve(resolvedRoot, changed) !== resolve(idsFile)) return;\n\t\t\t\t// Invalidate the per-republish live-envelope cache so the next\n\t\t\t\t// `transformIndexHtml` re-reads the rewritten `deployment.json`.\n\t\t\t\tliveEnvelopeFresh = false;\n\t\t\t\tconst channel = server.hot ?? server.ws;\n\t\t\t\tchannel?.send({ type: 'full-reload' });\n\t\t\t};\n\t\t\tserver.watcher.on('change', reloadOnIdsChange);\n\t\t\tserver.watcher.on('add', reloadOnIdsChange);\n\t\t},\n\n\t\ttransformIndexHtml: (html: string) => {\n\t\t\tif (!isServe) return undefined;\n\t\t\tconst env = process.env as Readonly<Record<string, string | undefined>>;\n\t\t\t// Read the live local envelope (cached per republish, invalidated by\n\t\t\t// the `configureServer` watcher on a `deployment.json` change), then\n\t\t\t// overlay it on the committed networks captured at config-load (a sync\n\t\t\t// merge — the committed thunks were already awaited in `config`). Set on\n\t\t\t// a distinct global (`__DEVSTACK_DEPLOYMENT_LIVE__`) the `config` hook's\n\t\t\t// `define` points `__DEVSTACK_DEPLOYMENT__` at; `head-prepend` runs it\n\t\t\t// before any app module.\n\t\t\tconst live = readLiveEnvelopeCached(env);\n\t\t\tconst liveDeployment = mergeDeployment(\n\t\t\t\t'serve',\n\t\t\t\tresolvedCommittedNetworks,\n\t\t\t\tlive,\n\t\t\t\toptions.defaultNetwork,\n\t\t\t);\n\t\t\tconst tags: Array<{\n\t\t\t\ttag: string;\n\t\t\t\tattrs?: Record<string, string | boolean>;\n\t\t\t\tchildren?: string;\n\t\t\t\tinjectTo: 'head' | 'head-prepend';\n\t\t\t}> = [\n\t\t\t\t{\n\t\t\t\t\ttag: 'script',\n\t\t\t\t\tchildren: `globalThis.__DEVSTACK_DEPLOYMENT_LIVE__ = ${escapeForInlineScript(JSON.stringify(liveDeployment ?? null))};`,\n\t\t\t\t\tinjectTo: 'head-prepend',\n\t\t\t\t},\n\t\t\t];\n\t\t\t// Gate the script tag on the app actually carrying the wallet (same\n\t\t\t// condition as the `config` hook's `optimizeDeps` include — reusing the\n\t\t\t// boolean it computed), so a headless app emits zero dev-wallet plumbing\n\t\t\t// rather than a tag that resolves to the no-op virtual module.\n\t\t\tif (injectDevWallet && devWalletPresent) {\n\t\t\t\ttags.push({\n\t\t\t\t\ttag: 'script',\n\t\t\t\t\tattrs: { type: 'module', src: VIRTUAL_DEV_WALLET_SCRIPT_SRC },\n\t\t\t\t\tinjectTo: 'head',\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { html, tags };\n\t\t},\n\t};\n};\n"],"mappings":"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"}
|
|
@@ -70,14 +70,14 @@ const wipeStack = (stack, runtimeRoot, cwd) => new Promise((resolve) => {
|
|
|
70
70
|
resolve();
|
|
71
71
|
}, 12e4).unref?.();
|
|
72
72
|
});
|
|
73
|
-
/** Best-effort, sync read of the booted stack's `codegen.
|
|
73
|
+
/** Best-effort, sync read of the booted stack's `codegen.deploymentFile` from its
|
|
74
74
|
* manifest. The generated `config-runtime.ts` resolver reads the injected
|
|
75
|
-
* `
|
|
75
|
+
* `__DEVSTACK_DEPLOYMENT__` Vite `define`, but under vitest the Vite config (and
|
|
76
76
|
* thus that define) is evaluated BEFORE this `globalSetup` boots the stack,
|
|
77
77
|
* so the define bakes to `null`. The resolver's Node-only fallback reads
|
|
78
|
-
* `process.env.
|
|
79
|
-
* the freshly-booted stack's live
|
|
80
|
-
* plugin's `
|
|
78
|
+
* `process.env.DEVSTACK_DEPLOYMENT_FILE` at id-access time instead — we point it at
|
|
79
|
+
* the freshly-booted stack's live deployment file here. Mirrors the Vite
|
|
80
|
+
* plugin's `readIdsFileFromManifest`; degrades to `null` on any miss. */
|
|
81
81
|
const readIdsFileFromManifest = (manifestPath) => {
|
|
82
82
|
try {
|
|
83
83
|
if (!existsSync(manifestPath)) return null;
|
|
@@ -85,7 +85,7 @@ const readIdsFileFromManifest = (manifestPath) => {
|
|
|
85
85
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
86
86
|
const codegen = parsed.codegen;
|
|
87
87
|
if (typeof codegen !== "object" || codegen === null) return null;
|
|
88
|
-
const value = codegen.
|
|
88
|
+
const value = codegen.deploymentFile;
|
|
89
89
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
90
90
|
} catch {
|
|
91
91
|
return null;
|
|
@@ -94,13 +94,13 @@ const readIdsFileFromManifest = (manifestPath) => {
|
|
|
94
94
|
/** Publish the env handoff vitest forwards to test workers, so per-file
|
|
95
95
|
* `getStackContext()` resolves the stack this setup booted/attached, and the
|
|
96
96
|
* generated `config-runtime.ts` resolver can read the live ids
|
|
97
|
-
* (`
|
|
97
|
+
* (`DEVSTACK_DEPLOYMENT_FILE` — its Node fallback when the Vite `define` is null). */
|
|
98
98
|
const publishHandoff = (stack, runtimeRoot, manifestPath) => {
|
|
99
99
|
process.env[VITEST_ENV_VARS.STACK] = stack;
|
|
100
100
|
process.env[VITEST_ENV_VARS.RUNTIME_ROOT] = runtimeRoot;
|
|
101
101
|
process.env[VITEST_ENV_VARS.MANIFEST_PATH] = manifestPath;
|
|
102
102
|
const idsFile = readIdsFileFromManifest(manifestPath);
|
|
103
|
-
if (idsFile !== null) process.env.
|
|
103
|
+
if (idsFile !== null) process.env.DEVSTACK_DEPLOYMENT_FILE = idsFile;
|
|
104
104
|
};
|
|
105
105
|
/**
|
|
106
106
|
* Build a vitest `globalSetup` that boots (or reuses) a dedicated test
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"global-setup.mjs","names":[],"sources":["../../../src/build-integrations/vitest/global-setup.ts"],"sourcesContent":["// Vitest `globalSetup` that boots a dedicated `test` stack for the run\n// and tears it down on completion.\n//\n// Why this exists (distilled/23-build-integrations.md § Vitest): the\n// per-file `setup.ts` preset is a passive manifest READER — it requires\n// a stack someone already started. This module is the missing boot seam:\n// `pnpm test` brings up its OWN isolated `test` stack so tests run in\n// parallel to a developer's `pnpm dev` (`main`) stack without contending\n// for ports / faucet / wallet, and without requiring the dev stack to be\n// up at all.\n//\n// Lifecycle (the locked design — \"boot fresh + teardown each run\"):\n// setup → load config, runStack(identity.stack='test'), await ready\n// handoff → publish env (DEVSTACK_STACK + DEVSTACK_MANIFEST_PATH) so the\n// per-file workers' `loadStackContext` resolves THIS stack\n// teardown → stop + awaitShutdown (robust: still tears down a half-booted\n// stack so no `devstack.stack=test` containers leak)\n//\n// Escape hatch (reuse an already-running stack — e.g. your dev stack):\n// set `DEVSTACK_TEST_REUSE=1` (and optionally point `DEVSTACK_STACK` at\n// the running stack), or pass `{ reuse: true }`. Reuse mode does NOT\n// boot and does NOT tear down — it only publishes the env handoff to the\n// existing manifest.\n//\n// CRITICAL: vitest runs `globalSetup` in a SEPARATE process from the test\n// workers, so the handoff is via `process.env` (which vitest forwards to\n// workers), NOT an in-memory stash. We reuse the existing\n// `DEVSTACK_MANIFEST_PATH` top-precedence rung (`env.ts`) that the engine\n// already uses when it spawns child processes.\n//\n// This module imports only the CLI-free `api/` surface (`runStack`,\n// `loadDevstackConfig`) so the vitest entrypoint never pulls the CLI/TUI\n// graph into its bundle.\n\nimport { spawn } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { loadDevstackConfig } from '../../api/load-config.ts';\nimport { runStack, type RunHandle } from '../../api/run-stack.ts';\nimport { DEFAULT_RUNTIME_ROOT, RECOMMENDED_TEST_STACK, VITEST_ENV_VARS } from './env.ts';\nimport { loadStackContext } from './stack-context.ts';\n\nexport interface DevstackVitestGlobalSetupOptions {\n\t/** Stack name to boot. Default: `DEVSTACK_STACK` if set, else\n\t * `RECOMMENDED_TEST_STACK` (`'test'`). The booted identity forces\n\t * this stack, so it coexists with a developer's `main` dev stack. */\n\treadonly stack?: string;\n\t/** Path to the app's `devstack.config.ts`. Default: walk-up from cwd. */\n\treadonly configPath?: string;\n\t/** Runtime root holding `stacks/<stack>/manifest.json`. Default:\n\t * `DEVSTACK_RUNTIME_ROOT` / `DEVSTACK_STATE_DIR` / `.devstack`. */\n\treadonly runtimeRoot?: string;\n\t/** Reuse an already-running stack instead of booting a fresh one (no\n\t * boot, no teardown). Default: `false` — also enabled by\n\t * `DEVSTACK_TEST_REUSE=1`. */\n\treadonly reuse?: boolean;\n\t/** Hard cap (ms) for the stack to reach ready. Default 300_000 (5 min)\n\t * — accounts for cold supervisor boot under Docker image pulls. */\n\treadonly bootTimeoutMs?: number;\n}\n\n/** Vitest's `globalSetup` contract: a setup function that optionally\n * returns a teardown function. */\nexport type DevstackVitestGlobalSetup = () => Promise<() => Promise<void>>;\n\nconst DEFAULT_BOOT_TIMEOUT_MS = 300_000;\n\nconst isTruthyEnv = (value: string | undefined): boolean =>\n\tvalue === '1' || value === 'true' || value === 'yes';\n\nconst resolveRuntimeRoot = (\n\topts: DevstackVitestGlobalSetupOptions,\n\tenv: NodeJS.ProcessEnv,\n): string =>\n\topts.runtimeRoot ??\n\t(env[VITEST_ENV_VARS.RUNTIME_ROOT] || env[VITEST_ENV_VARS.RUNTIME_ROOT_LEGACY] || undefined) ??\n\tDEFAULT_RUNTIME_ROOT;\n\nconst resolveStackName = (\n\topts: DevstackVitestGlobalSetupOptions,\n\tenv: NodeJS.ProcessEnv,\n): string => {\n\tconst fromEnv = env[VITEST_ENV_VARS.STACK];\n\treturn opts.stack ?? (fromEnv && fromEnv !== '' ? fromEnv : RECOMMENDED_TEST_STACK);\n};\n\nconst withTimeout = async <A>(promise: Promise<A>, ms: number, label: string): Promise<A> => {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);\n\t});\n\ttry {\n\t\treturn await Promise.race([promise, timeout]);\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t}\n};\n\nconst safeStop = async (handle: RunHandle): Promise<void> => {\n\ttry {\n\t\tawait Effect.runPromise(handle.stop);\n\t} catch {\n\t\t// best-effort — fall through to awaitShutdown\n\t}\n\ttry {\n\t\tawait Effect.runPromise(handle.awaitShutdown);\n\t} catch {\n\t\t// supervisor may have already exited; nothing more to do\n\t}\n};\n\n/** After the in-process supervisor has exited, graceful shutdown only\n * STOPS the stack's containers (devstack keeps them for warm restarts).\n * An ephemeral test stack should leave nothing behind, so run the\n * production `devstack wipe` verb (force-removes containers + networks +\n * volumes + per-stack on-disk state) for the booted stack. Best-effort:\n * failure here never fails the test run, and a leftover stopped stack is\n * force-recreated by the next boot anyway. Skip with `DEVSTACK_TEST_KEEP=1`\n * to leave the stack up for debugging. */\nconst wipeStack = (stack: string, runtimeRoot: string, cwd: string): Promise<void> =>\n\tnew Promise((resolve) => {\n\t\t// The CLI entry ships beside this module in the built package:\n\t\t// dist/build-integrations/vitest/global-setup.mjs → dist/cli/main.mjs.\n\t\tconst cliEntry = fileURLToPath(new URL('../../cli/main.mjs', import.meta.url));\n\t\tif (!existsSync(cliEntry)) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\t\tconst child = spawn(process.execPath, [cliEntry, 'wipe', '--yes'], {\n\t\t\tcwd,\n\t\t\tenv: { ...process.env, DEVSTACK_STACK: stack, DEVSTACK_RUNTIME_ROOT: runtimeRoot },\n\t\t\tstdio: 'ignore',\n\t\t});\n\t\tchild.unref();\n\t\tconst done = () => resolve();\n\t\tchild.on('error', done);\n\t\tchild.on('exit', done);\n\t\tconst timer = setTimeout(() => {\n\t\t\tchild.kill();\n\t\t\tresolve();\n\t\t}, 120_000);\n\t\ttimer.unref?.();\n\t});\n\n/** Best-effort, sync read of the booted stack's `codegen.idsFile` from its\n * manifest. The generated `config-runtime.ts` resolver reads the injected\n * `__DEVSTACK_IDS__` Vite `define`, but under vitest the Vite config (and\n * thus that define) is evaluated BEFORE this `globalSetup` boots the stack,\n * so the define bakes to `null`. The resolver's Node-only fallback reads\n * `process.env.DEVSTACK_IDS_FILE` at id-access time instead — we point it at\n * the freshly-booted stack's live id-config file here. Mirrors the Vite\n * plugin's `readCodegenField`; degrades to `null` on any miss. */\nconst readIdsFileFromManifest = (manifestPath: string): string | null => {\n\ttry {\n\t\tif (!existsSync(manifestPath)) return null;\n\t\tconst parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'));\n\t\tif (typeof parsed !== 'object' || parsed === null) return null;\n\t\tconst codegen = (parsed as { readonly codegen?: unknown }).codegen;\n\t\tif (typeof codegen !== 'object' || codegen === null) return null;\n\t\tconst value = (codegen as Record<string, unknown>).idsFile;\n\t\treturn typeof value === 'string' && value.length > 0 ? value : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Publish the env handoff vitest forwards to test workers, so per-file\n * `getStackContext()` resolves the stack this setup booted/attached, and the\n * generated `config-runtime.ts` resolver can read the live ids\n * (`DEVSTACK_IDS_FILE` — its Node fallback when the Vite `define` is null). */\nconst publishHandoff = (stack: string, runtimeRoot: string, manifestPath: string): void => {\n\tprocess.env[VITEST_ENV_VARS.STACK] = stack;\n\tprocess.env[VITEST_ENV_VARS.RUNTIME_ROOT] = runtimeRoot;\n\tprocess.env[VITEST_ENV_VARS.MANIFEST_PATH] = manifestPath;\n\tconst idsFile = readIdsFileFromManifest(manifestPath);\n\tif (idsFile !== null) process.env.DEVSTACK_IDS_FILE = idsFile;\n};\n\n/**\n * Build a vitest `globalSetup` that boots (or reuses) a dedicated test\n * stack. Wire it via `devstackVitestTestConfig({ autoBoot: true })`, or\n * reference the module path directly in `test.globalSetup`.\n */\nexport const devstackVitestGlobalSetup =\n\t(opts: DevstackVitestGlobalSetupOptions = {}): DevstackVitestGlobalSetup =>\n\tasync () => {\n\t\tconst env = process.env;\n\t\tconst stack = resolveStackName(opts, env);\n\t\tconst runtimeRoot = resolveRuntimeRoot(opts, env);\n\t\tconst reuse = opts.reuse ?? isTruthyEnv(env.DEVSTACK_TEST_REUSE);\n\n\t\t// Reuse path: attach to an already-running stack. No boot, no teardown.\n\t\tif (reuse) {\n\t\t\tconst ctx = loadStackContext({ stack, runtimeRoot, required: true });\n\t\t\t// `required: true` throws VitestManifestNotFoundError on miss.\n\t\t\tpublishHandoff(stack, runtimeRoot, ctx!.manifestPath);\n\t\t\treturn async () => {};\n\t\t}\n\n\t\t// Fresh boot (default). Load the app's stack the same way the CLI does.\n\t\tconst loaded = await Effect.runPromise(loadDevstackConfig(opts.configPath)).catch((cause) => {\n\t\t\tthrow new Error(\n\t\t\t\t`devstack autoBoot: failed to load devstack config — ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t{ cause },\n\t\t\t);\n\t\t});\n\n\t\tconst handle = runStack(loaded.stack, {\n\t\t\tidentity: { stack },\n\t\t\tappRoot: dirname(loaded.resolvedConfigPath),\n\t\t\truntimeRoot,\n\t\t\t...(loaded.engine.options.codegen !== undefined\n\t\t\t\t? { codegen: loaded.engine.options.codegen }\n\t\t\t\t: {}),\n\t\t});\n\n\t\ttry {\n\t\t\tawait withTimeout(\n\t\t\t\tEffect.runPromise(handle.start),\n\t\t\t\topts.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS,\n\t\t\t\t`devstack autoBoot: '${stack}' stack boot`,\n\t\t\t);\n\t\t} catch (cause) {\n\t\t\tawait safeStop(handle);\n\t\t\tthrow new Error(\n\t\t\t\t`devstack autoBoot: failed to boot the '${stack}' stack. ` +\n\t\t\t\t\t`If a '${stack}' stack is already running, stop it with \\`devstack down ${stack}\\` ` +\n\t\t\t\t\t`or set DEVSTACK_TEST_REUSE=1 to run tests against it. ` +\n\t\t\t\t\t`Cause: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t{ cause },\n\t\t\t);\n\t\t}\n\n\t\t// Resolve the freshly-written manifest the workers will read.\n\t\tconst ctx = loadStackContext({\n\t\t\tcwd: dirname(loaded.resolvedConfigPath),\n\t\t\tstack,\n\t\t\truntimeRoot,\n\t\t\trequired: true,\n\t\t});\n\t\tpublishHandoff(stack, runtimeRoot, ctx!.manifestPath);\n\n\t\tconst keep = isTruthyEnv(env.DEVSTACK_TEST_KEEP);\n\t\treturn async () => {\n\t\t\tawait safeStop(handle);\n\t\t\tif (!keep) {\n\t\t\t\tawait wipeStack(stack, runtimeRoot, dirname(loaded.resolvedConfigPath));\n\t\t\t}\n\t\t};\n\t};\n\n/** Default export = a ready-to-use vitest `globalSetup`. This is what\n * `test.globalSetup: '@mysten-incubation/devstack/vitest/global-setup'`\n * resolves to. */\nexport default devstackVitestGlobalSetup();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"global-setup.mjs","names":[],"sources":["../../../src/build-integrations/vitest/global-setup.ts"],"sourcesContent":["// Vitest `globalSetup` that boots a dedicated `test` stack for the run\n// and tears it down on completion.\n//\n// Why this exists (distilled/23-build-integrations.md § Vitest): the\n// per-file `setup.ts` preset is a passive manifest READER — it requires\n// a stack someone already started. This module is the missing boot seam:\n// `pnpm test` brings up its OWN isolated `test` stack so tests run in\n// parallel to a developer's `pnpm dev` (`main`) stack without contending\n// for ports / faucet / wallet, and without requiring the dev stack to be\n// up at all.\n//\n// Lifecycle (the locked design — \"boot fresh + teardown each run\"):\n// setup → load config, runStack(identity.stack='test'), await ready\n// handoff → publish env (DEVSTACK_STACK + DEVSTACK_MANIFEST_PATH) so the\n// per-file workers' `loadStackContext` resolves THIS stack\n// teardown → stop + awaitShutdown (robust: still tears down a half-booted\n// stack so no `devstack.stack=test` containers leak)\n//\n// Escape hatch (reuse an already-running stack — e.g. your dev stack):\n// set `DEVSTACK_TEST_REUSE=1` (and optionally point `DEVSTACK_STACK` at\n// the running stack), or pass `{ reuse: true }`. Reuse mode does NOT\n// boot and does NOT tear down — it only publishes the env handoff to the\n// existing manifest.\n//\n// CRITICAL: vitest runs `globalSetup` in a SEPARATE process from the test\n// workers, so the handoff is via `process.env` (which vitest forwards to\n// workers), NOT an in-memory stash. We reuse the existing\n// `DEVSTACK_MANIFEST_PATH` top-precedence rung (`env.ts`) that the engine\n// already uses when it spawns child processes.\n//\n// This module imports only the CLI-free `api/` surface (`runStack`,\n// `loadDevstackConfig`) so the vitest entrypoint never pulls the CLI/TUI\n// graph into its bundle.\n\nimport { spawn } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { loadDevstackConfig } from '../../api/load-config.ts';\nimport { runStack, type RunHandle } from '../../api/run-stack.ts';\nimport { DEFAULT_RUNTIME_ROOT, RECOMMENDED_TEST_STACK, VITEST_ENV_VARS } from './env.ts';\nimport { loadStackContext } from './stack-context.ts';\n\nexport interface DevstackVitestGlobalSetupOptions {\n\t/** Stack name to boot. Default: `DEVSTACK_STACK` if set, else\n\t * `RECOMMENDED_TEST_STACK` (`'test'`). The booted identity forces\n\t * this stack, so it coexists with a developer's `main` dev stack. */\n\treadonly stack?: string;\n\t/** Path to the app's `devstack.config.ts`. Default: walk-up from cwd. */\n\treadonly configPath?: string;\n\t/** Runtime root holding `stacks/<stack>/manifest.json`. Default:\n\t * `DEVSTACK_RUNTIME_ROOT` / `DEVSTACK_STATE_DIR` / `.devstack`. */\n\treadonly runtimeRoot?: string;\n\t/** Reuse an already-running stack instead of booting a fresh one (no\n\t * boot, no teardown). Default: `false` — also enabled by\n\t * `DEVSTACK_TEST_REUSE=1`. */\n\treadonly reuse?: boolean;\n\t/** Hard cap (ms) for the stack to reach ready. Default 300_000 (5 min)\n\t * — accounts for cold supervisor boot under Docker image pulls. */\n\treadonly bootTimeoutMs?: number;\n}\n\n/** Vitest's `globalSetup` contract: a setup function that optionally\n * returns a teardown function. */\nexport type DevstackVitestGlobalSetup = () => Promise<() => Promise<void>>;\n\nconst DEFAULT_BOOT_TIMEOUT_MS = 300_000;\n\nconst isTruthyEnv = (value: string | undefined): boolean =>\n\tvalue === '1' || value === 'true' || value === 'yes';\n\nconst resolveRuntimeRoot = (\n\topts: DevstackVitestGlobalSetupOptions,\n\tenv: NodeJS.ProcessEnv,\n): string =>\n\topts.runtimeRoot ??\n\t(env[VITEST_ENV_VARS.RUNTIME_ROOT] || env[VITEST_ENV_VARS.RUNTIME_ROOT_LEGACY] || undefined) ??\n\tDEFAULT_RUNTIME_ROOT;\n\nconst resolveStackName = (\n\topts: DevstackVitestGlobalSetupOptions,\n\tenv: NodeJS.ProcessEnv,\n): string => {\n\tconst fromEnv = env[VITEST_ENV_VARS.STACK];\n\treturn opts.stack ?? (fromEnv && fromEnv !== '' ? fromEnv : RECOMMENDED_TEST_STACK);\n};\n\nconst withTimeout = async <A>(promise: Promise<A>, ms: number, label: string): Promise<A> => {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst timeout = new Promise<never>((_, reject) => {\n\t\ttimer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);\n\t});\n\ttry {\n\t\treturn await Promise.race([promise, timeout]);\n\t} finally {\n\t\tif (timer !== undefined) clearTimeout(timer);\n\t}\n};\n\nconst safeStop = async (handle: RunHandle): Promise<void> => {\n\ttry {\n\t\tawait Effect.runPromise(handle.stop);\n\t} catch {\n\t\t// best-effort — fall through to awaitShutdown\n\t}\n\ttry {\n\t\tawait Effect.runPromise(handle.awaitShutdown);\n\t} catch {\n\t\t// supervisor may have already exited; nothing more to do\n\t}\n};\n\n/** After the in-process supervisor has exited, graceful shutdown only\n * STOPS the stack's containers (devstack keeps them for warm restarts).\n * An ephemeral test stack should leave nothing behind, so run the\n * production `devstack wipe` verb (force-removes containers + networks +\n * volumes + per-stack on-disk state) for the booted stack. Best-effort:\n * failure here never fails the test run, and a leftover stopped stack is\n * force-recreated by the next boot anyway. Skip with `DEVSTACK_TEST_KEEP=1`\n * to leave the stack up for debugging. */\nconst wipeStack = (stack: string, runtimeRoot: string, cwd: string): Promise<void> =>\n\tnew Promise((resolve) => {\n\t\t// The CLI entry ships beside this module in the built package:\n\t\t// dist/build-integrations/vitest/global-setup.mjs → dist/cli/main.mjs.\n\t\tconst cliEntry = fileURLToPath(new URL('../../cli/main.mjs', import.meta.url));\n\t\tif (!existsSync(cliEntry)) {\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\t\tconst child = spawn(process.execPath, [cliEntry, 'wipe', '--yes'], {\n\t\t\tcwd,\n\t\t\tenv: { ...process.env, DEVSTACK_STACK: stack, DEVSTACK_RUNTIME_ROOT: runtimeRoot },\n\t\t\tstdio: 'ignore',\n\t\t});\n\t\tchild.unref();\n\t\tconst done = () => resolve();\n\t\tchild.on('error', done);\n\t\tchild.on('exit', done);\n\t\tconst timer = setTimeout(() => {\n\t\t\tchild.kill();\n\t\t\tresolve();\n\t\t}, 120_000);\n\t\ttimer.unref?.();\n\t});\n\n/** Best-effort, sync read of the booted stack's `codegen.deploymentFile` from its\n * manifest. The generated `config-runtime.ts` resolver reads the injected\n * `__DEVSTACK_DEPLOYMENT__` Vite `define`, but under vitest the Vite config (and\n * thus that define) is evaluated BEFORE this `globalSetup` boots the stack,\n * so the define bakes to `null`. The resolver's Node-only fallback reads\n * `process.env.DEVSTACK_DEPLOYMENT_FILE` at id-access time instead — we point it at\n * the freshly-booted stack's live deployment file here. Mirrors the Vite\n * plugin's `readIdsFileFromManifest`; degrades to `null` on any miss. */\nconst readIdsFileFromManifest = (manifestPath: string): string | null => {\n\ttry {\n\t\tif (!existsSync(manifestPath)) return null;\n\t\tconst parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'));\n\t\tif (typeof parsed !== 'object' || parsed === null) return null;\n\t\tconst codegen = (parsed as { readonly codegen?: unknown }).codegen;\n\t\tif (typeof codegen !== 'object' || codegen === null) return null;\n\t\tconst value = (codegen as Record<string, unknown>).deploymentFile;\n\t\treturn typeof value === 'string' && value.length > 0 ? value : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Publish the env handoff vitest forwards to test workers, so per-file\n * `getStackContext()` resolves the stack this setup booted/attached, and the\n * generated `config-runtime.ts` resolver can read the live ids\n * (`DEVSTACK_DEPLOYMENT_FILE` — its Node fallback when the Vite `define` is null). */\nconst publishHandoff = (stack: string, runtimeRoot: string, manifestPath: string): void => {\n\tprocess.env[VITEST_ENV_VARS.STACK] = stack;\n\tprocess.env[VITEST_ENV_VARS.RUNTIME_ROOT] = runtimeRoot;\n\tprocess.env[VITEST_ENV_VARS.MANIFEST_PATH] = manifestPath;\n\tconst idsFile = readIdsFileFromManifest(manifestPath);\n\tif (idsFile !== null) process.env.DEVSTACK_DEPLOYMENT_FILE = idsFile;\n};\n\n/**\n * Build a vitest `globalSetup` that boots (or reuses) a dedicated test\n * stack. Wire it via `devstackVitestTestConfig({ autoBoot: true })`, or\n * reference the module path directly in `test.globalSetup`.\n */\nexport const devstackVitestGlobalSetup =\n\t(opts: DevstackVitestGlobalSetupOptions = {}): DevstackVitestGlobalSetup =>\n\tasync () => {\n\t\tconst env = process.env;\n\t\tconst stack = resolveStackName(opts, env);\n\t\tconst runtimeRoot = resolveRuntimeRoot(opts, env);\n\t\tconst reuse = opts.reuse ?? isTruthyEnv(env.DEVSTACK_TEST_REUSE);\n\n\t\t// Reuse path: attach to an already-running stack. No boot, no teardown.\n\t\tif (reuse) {\n\t\t\tconst ctx = loadStackContext({ stack, runtimeRoot, required: true });\n\t\t\t// `required: true` throws VitestManifestNotFoundError on miss.\n\t\t\tpublishHandoff(stack, runtimeRoot, ctx!.manifestPath);\n\t\t\treturn async () => {};\n\t\t}\n\n\t\t// Fresh boot (default). Load the app's stack the same way the CLI does.\n\t\tconst loaded = await Effect.runPromise(loadDevstackConfig(opts.configPath)).catch((cause) => {\n\t\t\tthrow new Error(\n\t\t\t\t`devstack autoBoot: failed to load devstack config — ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t{ cause },\n\t\t\t);\n\t\t});\n\n\t\tconst handle = runStack(loaded.stack, {\n\t\t\tidentity: { stack },\n\t\t\tappRoot: dirname(loaded.resolvedConfigPath),\n\t\t\truntimeRoot,\n\t\t\t...(loaded.engine.options.codegen !== undefined\n\t\t\t\t? { codegen: loaded.engine.options.codegen }\n\t\t\t\t: {}),\n\t\t});\n\n\t\ttry {\n\t\t\tawait withTimeout(\n\t\t\t\tEffect.runPromise(handle.start),\n\t\t\t\topts.bootTimeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS,\n\t\t\t\t`devstack autoBoot: '${stack}' stack boot`,\n\t\t\t);\n\t\t} catch (cause) {\n\t\t\tawait safeStop(handle);\n\t\t\tthrow new Error(\n\t\t\t\t`devstack autoBoot: failed to boot the '${stack}' stack. ` +\n\t\t\t\t\t`If a '${stack}' stack is already running, stop it with \\`devstack down ${stack}\\` ` +\n\t\t\t\t\t`or set DEVSTACK_TEST_REUSE=1 to run tests against it. ` +\n\t\t\t\t\t`Cause: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t{ cause },\n\t\t\t);\n\t\t}\n\n\t\t// Resolve the freshly-written manifest the workers will read.\n\t\tconst ctx = loadStackContext({\n\t\t\tcwd: dirname(loaded.resolvedConfigPath),\n\t\t\tstack,\n\t\t\truntimeRoot,\n\t\t\trequired: true,\n\t\t});\n\t\tpublishHandoff(stack, runtimeRoot, ctx!.manifestPath);\n\n\t\tconst keep = isTruthyEnv(env.DEVSTACK_TEST_KEEP);\n\t\treturn async () => {\n\t\t\tawait safeStop(handle);\n\t\t\tif (!keep) {\n\t\t\t\tawait wipeStack(stack, runtimeRoot, dirname(loaded.resolvedConfigPath));\n\t\t\t}\n\t\t};\n\t};\n\n/** Default export = a ready-to-use vitest `globalSetup`. This is what\n * `test.globalSetup: '@mysten-incubation/devstack/vitest/global-setup'`\n * resolves to. */\nexport default devstackVitestGlobalSetup();\n"],"mappings":"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"}
|
package/dist/cli/main.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { makeConfigLoader } from "./wirings/config-loader.mjs";
|
|
|
17
17
|
import { runCodegen } from "./wirings/codegen.mjs";
|
|
18
18
|
import { runUpLive } from "./wirings/up.mjs";
|
|
19
19
|
import { runApplyLive } from "./wirings/apply.mjs";
|
|
20
|
-
import {
|
|
20
|
+
import { runDumpDeployment } from "./wirings/dump-deployment.mjs";
|
|
21
21
|
import { runSnapshotCaptureLiveAware, runSnapshotDeleteDirect, runSnapshotRestoreDirect } from "./wirings/snapshot.mjs";
|
|
22
22
|
import { runWipeDirect, runWipePlanDirect } from "./wirings/wipe.mjs";
|
|
23
23
|
import { Effect } from "effect";
|
|
@@ -136,9 +136,10 @@ const buildDirectDeps = (identity) => {
|
|
|
136
136
|
}) },
|
|
137
137
|
apply: { run: (flags) => runApplyLive(flags.configPath, identity) },
|
|
138
138
|
codegen: { run: (flags) => runCodegen(flags.configPath) },
|
|
139
|
-
|
|
139
|
+
dumpDeployment: { run: (flags) => runDumpDeployment(identity, {
|
|
140
140
|
configPath: flags.configPath,
|
|
141
141
|
out: flags.out,
|
|
142
|
+
network: flags.network,
|
|
142
143
|
io: flags.io,
|
|
143
144
|
outputMode: flags.outputMode
|
|
144
145
|
}) },
|
package/dist/cli/main.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.mjs","names":["resolvePath"],"sources":["../../src/cli/main.ts"],"sourcesContent":["// devstack CLI bin entry — argv → identity → deps → dispatch → exit.\n//\n// This file is the executable entry the `bin: { devstack: ... }`\n// package.json field points at. Build output: `dist/cli/main.mjs`.\n//\n// Architecture invariant (surfaces/cli/index.ts header):\n// \"`up` must hand its long-running effect to the outer Node runtime\n// directly, not nest a runtime — otherwise SIGINT cannot reach\n// scope finalizers and container teardown leaks.\"\n//\n// Shape:\n// 1. Pre-parse identity inputs from argv + env (see\n// `identityInputsFromArgv`).\n// 2. Resolve identity (app / stack / network / runtime root) via the\n// shared `api/inference-network` helpers.\n// 3. Build the dispatcher's `CliDeps` bundle from each verb's wiring\n// module in `cli/wirings/`.\n// 4. Hand control to the Stricli-backed dispatcher, which routes to\n// the verb-scoped wiring function.\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, join, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { endpointKey, pluginKey } from '../substrate/brand.ts';\nimport { emptyProjection } from '../substrate/runtime/projection/state-ref.ts';\nimport type { SubscribableState } from '../substrate/projection.ts';\nimport { readStackContext } from '../build-integrations/runtime/read-stack-context.ts';\nimport type { StackContext } from '../build-integrations/runtime/stack-context.ts';\nimport { dispatch, type CliDeps, CliUsageError } from '../surfaces/cli/index.ts';\nimport { emitFailure, nodeProcessIO } from '../surfaces/cli/output.ts';\nimport { ENV_VARS } from '../surfaces/cli/flags.ts';\nimport { defaultProbes } from './doctor-probes.ts';\nimport { nodeConfirmPrompt } from '../surfaces/cli/commands/confirm-node.ts';\nimport type { StatusReader } from '../surfaces/cli/commands/status.ts';\nimport { ExitCode } from '../surfaces/cli/sysexits.ts';\nimport { makeSnapshotReader } from './snapshot-reader.ts';\nimport {\n\tDevstackNetworkParseError,\n\tresolveAppName,\n\tresolveNetworkSync,\n\tresolveStackName,\n\tresolveStateDir,\n} from '../api/inference-network.ts';\nimport { makeDirectPruneDeps } from './prune-direct.ts';\nimport { runUpLive } from './wirings/up.ts';\nimport { runApplyLive } from './wirings/apply.ts';\nimport { runCodegen } from './wirings/codegen.ts';\nimport { runDumpIds } from './wirings/dump-ids.ts';\nimport {\n\trunSnapshotCaptureLiveAware,\n\trunSnapshotDeleteDirect,\n\trunSnapshotRestoreDirect,\n} from './wirings/snapshot.ts';\nimport { runWipeDirect, runWipePlanDirect } from './wirings/wipe.ts';\nimport { makeConfigLoader, resolveConfigPath } from './wirings/config-loader.ts';\nimport type { ResolvedIdentity } from './wirings/identity.ts';\n\n// -----------------------------------------------------------------------------\n// Identity resolution\n// -----------------------------------------------------------------------------\n\n/** Resolve identity from flags + env. App and stack fall through the\n * shared cwd/package metadata resolver before their defaults.\n *\n * State-dir precedence ladder:\n * `--state-dir` flag (`stateDir`) > `config.options.stateDir`\n * (`configStateDir`) > `$DEVSTACK_STATE_DIR` > `<cwd>/.devstack`.\n * The flag maps to `resolveStateDir`'s top `runtimeRoot` rung and the\n * config value to its `stateDir` rung so the flag always wins over a\n * config-declared `defineDevstack({ stateDir })`.\n *\n * Stack-name precedence ladder (the SAME flag-beats-config shape as\n * state-dir, so the two are consistent and reviewable side-by-side):\n * `--stack` flag / `$DEVSTACK_STACK` (`params.stack`)\n * > config's `defineDevstack({ stackName })`\n * > cwd/package inference (default `'main'`).\n * Config-vs-explicit resolution can't happen here (the config isn't\n * loaded yet for the no-config verbs), so we record the explicit value\n * on `ResolvedIdentity.explicitStack` and let the verb wirings apply\n * `config.stackName` ONLY when no explicit stack was given — see\n * `effectiveStackName` in `cli/wirings/identity.ts`. `params.stack` is\n * exactly the explicit `--stack`-or-`$DEVSTACK_STACK` value (the argv\n * pre-parser seeds it from env, then lets the flag overwrite it), and\n * is `undefined` when neither was provided. */\nconst resolveIdentity = (params: {\n\treadonly app: string | undefined;\n\treadonly stack: string | undefined;\n\treadonly network: string | undefined;\n\treadonly stateDir: string | undefined;\n\treadonly configStateDir?: string | undefined;\n\treadonly cwd?: string;\n}): ResolvedIdentity => {\n\tconst cwd = params.cwd ?? process.cwd();\n\tconst app = resolveAppName({\n\t\texplicit: params.app,\n\t\tcwd,\n\t});\n\tconst runtimeRoot = resolveStateDir({\n\t\truntimeRoot: params.stateDir,\n\t\tstateDir: params.configStateDir,\n\t\tenv: process.env.DEVSTACK_STATE_DIR,\n\t\tcwd,\n\t});\n\tconst stacksRoot = resolvePath(runtimeRoot, 'stacks');\n\t// `resolveStackName` here folds explicit (flag/env) > inferred >\n\t// default. Crucially it does NOT see `config.stackName`: a non-empty\n\t// `params.stack` means the operator was explicit, so `stack` already\n\t// equals the explicit value and `config.stackName` must not override\n\t// it downstream. An empty `params.stack` falls through to the\n\t// inferred/default name, which a verb's `config.stackName` may then\n\t// supersede via `effectiveStackName`.\n\tconst explicitStack =\n\t\tparams.stack !== undefined && params.stack.length > 0 ? params.stack : undefined;\n\tconst stack = resolveStackName({\n\t\texplicit: params.stack,\n\t\tcwd,\n\t});\n\t// Centralized explicit > env > default ladder. Throws\n\t// `DevstackNetworkParseError` on a malformed value so the CLI fails\n\t// fast with a structured error instead of a downstream cryptic\n\t// network-probe failure. The identity's network is the canonical name\n\t// (`localnet`, `testnet`, …), independent of how it was spelled.\n\tconst network = resolveNetworkSync({\n\t\texplicit: params.network,\n\t\tenv: process.env.DEVSTACK_NETWORK,\n\t\texplicitSource: '--network',\n\t}).parsed.name;\n\tconst stackRoot = resolvePath(stacksRoot, stack);\n\treturn {\n\t\tapp,\n\t\tstack,\n\t\tnetwork,\n\t\truntimeRoot,\n\t\tstacksRoot,\n\t\tstackRoot,\n\t\trosterFile: resolvePath(stackRoot, 'roster.json'),\n\t\texplicitStack,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Verb deps composition\n// -----------------------------------------------------------------------------\n\n/**\n * Degraded offline-status projection — built from the on-disk manifest,\n * NOT from a persisted projection twin (the projection snapshot file no\n * longer exists). When the stack is DOWN, the manifest is the only\n * durable record of the boot; `status` surfaces identity + endpoints\n * from it and leaves the live-only slices (`rows` / `accounts` /\n * `packages` / `errors`) empty. A freshly-booted stack also has empty\n * `rows`, so the status renderer already tolerates this shape.\n *\n * Pure projector — exported for the focused unit test. Starts from\n * `emptyProjection()` (the closed-vocabulary baseline) and fills only\n * the two manifest-backed slices, so no display vocabulary can leak in.\n * The manifest identity tuple is `{ app, stack, network }`, matching\n * the projection's identity shape directly. Endpoints get re-branded\n * (`endpointKey` / `pluginKey`)\n * to the projection's branded shape; `registeredAt` is unknown offline,\n * so it defaults to `0`.\n */\nexport const degradedStatusFromContext = (ctx: StackContext): SubscribableState => ({\n\t...emptyProjection(),\n\tidentity: {\n\t\tapp: ctx.identity.app,\n\t\tstack: ctx.identity.stack,\n\t\tnetwork: ctx.identity.network,\n\t},\n\tendpoints: ctx.endpoints.all().map((endpoint) => ({\n\t\tendpointKey: endpointKey(endpoint.endpointKey),\n\t\tpluginKey: pluginKey(endpoint.pluginKey),\n\t\tname: endpoint.name,\n\t\turl: endpoint.url,\n\t\tdisplayUrl: endpoint.displayUrl,\n\t\twireProtocol: endpoint.wireProtocol,\n\t\tregisteredAt: 0,\n\t})),\n});\n\nconst projectionStatusReader = (identity: ResolvedIdentity): StatusReader => ({\n\treadState: () =>\n\t\tEffect.sync(() => {\n\t\t\t// Offline read: project the on-disk manifest for THIS stack root.\n\t\t\t// A missing / malformed manifest (no `up` yet, or a hand-edit)\n\t\t\t// is a tolerated \"no state present\" — `readStackContext` throws\n\t\t\t// in that case, so swallow the throw and report `null` (the\n\t\t\t// status command's contract for an absent stack).\n\t\t\ttry {\n\t\t\t\tconst ctx = readStackContext({ manifestPath: join(identity.stackRoot, 'manifest.json') });\n\t\t\t\treturn degradedStatusFromContext(ctx);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n});\n\nconst buildDirectDeps = (identity: ResolvedIdentity): CliDeps => {\n\treturn {\n\t\tup: {\n\t\t\trun: (flags) =>\n\t\t\t\trunUpLive(flags.configPath, identity, {\n\t\t\t\t\trenderer: flags.renderer,\n\t\t\t\t\tstdoutIsTty: Boolean((process.stdout as { isTTY?: boolean }).isTTY),\n\t\t\t\t\tfromSnapshot: flags.fromSnapshot,\n\t\t\t\t\tsnapshotCache: flags.snapshotCache,\n\t\t\t\t\tsnapshotStalePolicy: flags.snapshotStalePolicy,\n\t\t\t\t}),\n\t\t},\n\t\tapply: {\n\t\t\trun: (flags) => runApplyLive(flags.configPath, identity),\n\t\t},\n\t\tcodegen: {\n\t\t\trun: (flags) => runCodegen(flags.configPath),\n\t\t},\n\t\tdumpIds: {\n\t\t\trun: (flags) =>\n\t\t\t\trunDumpIds(identity, {\n\t\t\t\t\tconfigPath: flags.configPath,\n\t\t\t\t\tout: flags.out,\n\t\t\t\t\tio: flags.io,\n\t\t\t\t\toutputMode: flags.outputMode,\n\t\t\t\t}),\n\t\t},\n\t\tstatus: { reader: projectionStatusReader(identity) },\n\t\tsnapshot: {\n\t\t\treader: makeSnapshotReader(identity),\n\t\t\tcapture: (args) => runSnapshotCaptureLiveAware(identity, args),\n\t\t\trestore: (snapshotId) => runSnapshotRestoreDirect(identity, snapshotId),\n\t\t\tdelete: (snapshotId) => runSnapshotDeleteDirect(identity, snapshotId),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t\tprune: makeDirectPruneDeps({ runtimeRoot: identity.runtimeRoot }),\n\t\tdoctor: {\n\t\t\tprobes: defaultProbes({\n\t\t\t\tstateDir: identity.runtimeRoot,\n\t\t\t\tappRoot: identity.stacksRoot,\n\t\t\t}),\n\t\t},\n\t\tconfig: { loader: makeConfigLoader() },\n\t\twipe: {\n\t\t\twipe: () => runWipeDirect(identity),\n\t\t\tplan: () => runWipePlanDirect(identity),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Argv pre-parser\n// -----------------------------------------------------------------------------\n\n/** @internal Exported for tests. Resolves identity flag inputs from a\n * `--app <x>` / `--stack <x>` / `--network <x>` / `--state-dir <x>` /\n * `--config <x>` argv, falling back to `DEVSTACK_*` env vars. Throws\n * on a missing or flag-shaped value so a typo doesn't silently demote\n * a downstream flag, and on a duplicate flag (`--app a --app b`) so\n * the pre-parser does not silently last-write-wins a value that\n * Stricli will later reject outright. */\nexport const identityInputsFromArgv = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n) => {\n\tlet app = env.DEVSTACK_APP;\n\tlet stack = env.DEVSTACK_STACK;\n\tlet network = env.DEVSTACK_NETWORK;\n\t// `stateDir` intentionally captures the `--state-dir` flag ONLY. The\n\t// full ladder — `--state-dir` flag > `config.options.stateDir`\n\t// (`defineDevstack({ stateDir })`) > `$DEVSTACK_STATE_DIR` >\n\t// `<cwd>/.devstack` — is assembled at the `resolveStateDir(...)`\n\t// call-site in `resolveIdentity`: the flag wins (top `runtimeRoot`\n\t// rung), the best-effort config value sits below it (`stateDir`\n\t// rung), then env, then the cwd default. Keeping the pre-parser a\n\t// thin flag-extractor means config + env precedence live in one place.\n\tlet stateDir: string | undefined;\n\tlet configPath = env.DEVSTACK_CONFIG;\n\t// Tracks which flags have been seen on the argv side so a second\n\t// occurrence trips a usage error before the value silently overwrites\n\t// the first. Env-sourced defaults are NOT counted (they are not user\n\t// argv).\n\tconst seenArgvFlags = new Set<string>();\n\tfor (let i = 0; i < argv.length; i += 1) {\n\t\tconst token = argv[i]!;\n\t\tconst readValue = (name: string): string | undefined => {\n\t\t\tlet value: string | undefined;\n\t\t\t// `--name=value` form: trust the literal between `=` and end.\n\t\t\tif (token.startsWith(`--${name}=`)) {\n\t\t\t\tvalue = token.slice(name.length + 3);\n\t\t\t}\n\t\t\t// `--name value` form: peek the next token. Reject another\n\t\t\t// flag token (`--foo`) as the value — it almost certainly\n\t\t\t// means the user meant `--name <empty>` (typo / forgotten\n\t\t\t// argument) and quietly absorbing `--foo` as the value\n\t\t\t// silently demotes a downstream flag.\n\t\t\telse if (token === `--${name}`) {\n\t\t\t\tconst next = argv[i + 1];\n\t\t\t\tif (next === undefined) {\n\t\t\t\t\tthrow new CliUsageError({ message: `flag --${name} requires a value` });\n\t\t\t\t}\n\t\t\t\tif (next.startsWith('--')) {\n\t\t\t\t\tthrow new CliUsageError({\n\t\t\t\t\t\tmessage: `flag --${name} requires a value; got \"${next}\" which looks like a flag`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tvalue = next;\n\t\t\t}\n\t\t\tif (value === undefined) return undefined;\n\t\t\tif (seenArgvFlags.has(name)) {\n\t\t\t\tthrow new CliUsageError({ message: `flag --${name} given more than once` });\n\t\t\t}\n\t\t\tseenArgvFlags.add(name);\n\t\t\treturn value;\n\t\t};\n\t\tapp = readValue('app') ?? app;\n\t\tstack = readValue('stack') ?? stack;\n\t\tnetwork = readValue('network') ?? network;\n\t\tstateDir = readValue('state-dir') ?? stateDir;\n\t\tconfigPath = readValue('config') ?? configPath;\n\t}\n\treturn { app, stack, network, stateDir, configPath };\n};\n\nconst identityCwdFromConfig = (configPath: string | undefined): string => {\n\tconst resolved = resolveConfigPath(configPath);\n\treturn resolved === null ? process.cwd() : dirname(resolved);\n};\n\n/** Whether the resolved argv is a purely-informational invocation that\n * never consumes `identity.runtimeRoot` (the only value the config\n * pre-load feeds). For these we SKIP `configStateDirBestEffort` so we\n * don't dynamic-import (and run the top-level side effects of) the\n * user's `devstack.config.ts` — plus walk parent dirs — for commands\n * the file header itself says must stay cheap:\n *\n * - empty argv → Stricli prints root help\n * - `--help`/`-h` or `--version`/`-v` anywhere → Stricli short-circuits\n * - first token `schema` → emits the static CLI schema (no state dir)\n *\n * An unknown verb also never reaches a state-dir consumer (Stricli\n * fails the parse), but we keep pre-loading on the general path: the\n * pre-parser already accepted the identity flags, and gating on a\n * closed verb set here would duplicate the dispatcher's route table.\n * The two flagged-as-wasteful informational paths (help/version/schema)\n * are the ones with a registered short-circuit, so skipping exactly\n * those removes the eager-import surprise without re-deriving routes. */\nconst argvSkipsConfigPreload = (argv: ReadonlyArray<string>): boolean => {\n\tif (argv.length === 0) return true;\n\tfor (const token of argv) {\n\t\tif (token === '--help' || token === '-h' || token === '--version' || token === '-v') {\n\t\t\treturn true;\n\t\t}\n\t}\n\tconst firstVerb = argv.find((token) => !token.startsWith('-'));\n\treturn firstVerb === 'schema';\n};\n\n/** Best-effort read of `config.options.stateDir` (the value a program\n * sets via `defineDevstack({ stateDir })`) for the state-dir precedence\n * ladder in `resolveIdentity`. Swallows EVERY config-loader failure\n * (not-found AND evaluation errors) and returns `undefined`:\n *\n * - No-config verbs (`prune`, `wipe`) MUST keep resolving identity\n * without a config, so a missing config silently falls through to\n * the flag > env > cwd ladder.\n * - A genuinely malformed config is NOT surfaced here — the verbs that\n * actually consume config (`up` / `apply` via `makeConfigLoader`)\n * re-load it and surface the typed `CliConfig*` error through the\n * normal envelope path, so behavior for those verbs is unchanged.\n *\n * This is one redundant config evaluation for `up` / `apply` (the verb\n * wiring re-loads via its own loader); threading the loaded value into\n * the verb dispatch would require touching the off-limits `wirings/up.ts`\n * signature, so the duplicate import is accepted deliberately. */\nconst configStateDirBestEffort = async (\n\tconfigPath: string | undefined,\n): Promise<string | undefined> => {\n\tconst loaded = await Effect.runPromise(makeConfigLoader().load(configPath).pipe(Effect.option));\n\tif (loaded._tag === 'None') return undefined;\n\tconst options = (loaded.value.stack as { readonly options?: { readonly stateDir?: string } })\n\t\t.options;\n\tconst stateDir = options?.stateDir;\n\treturn stateDir !== undefined && stateDir.length > 0 ? stateDir : undefined;\n};\n\n// -----------------------------------------------------------------------------\n// Bin entry\n// -----------------------------------------------------------------------------\n\nexport const runCli = async (\n\targv: ReadonlyArray<string> = process.argv.slice(2),\n): Promise<void> => {\n\tconst stdinIsTty = Boolean((process.stdin as { isTTY?: boolean }).isTTY);\n\tconst env: Record<string, string | undefined> = { ...process.env };\n\tlet identityInputs: ReturnType<typeof identityInputsFromArgv>;\n\ttry {\n\t\tidentityInputs = identityInputsFromArgv(argv, env);\n\t} catch (cause) {\n\t\tconst error =\n\t\t\tcause instanceof CliUsageError\n\t\t\t\t? cause\n\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(parse-argv)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\t// Best-effort config pre-load for the state-dir ladder. Swallows\n\t// not-found / malformed configs so no-config verbs (prune/wipe) keep\n\t// working; the `--state-dir` flag still wins over the config value.\n\t// Skipped for informational invocations (help/version/schema/empty)\n\t// that never read `identity.runtimeRoot`, so those commands don't\n\t// dynamic-import the user's config (and run its top-level side\n\t// effects) just to print help.\n\tconst configStateDir = argvSkipsConfigPreload(argv)\n\t\t? undefined\n\t\t: await configStateDirBestEffort(identityInputs.configPath);\n\tlet identity: ResolvedIdentity;\n\ttry {\n\t\tidentity = resolveIdentity({\n\t\t\tapp: identityInputs.app,\n\t\t\tstack: identityInputs.stack,\n\t\t\tnetwork: identityInputs.network,\n\t\t\tstateDir: identityInputs.stateDir,\n\t\t\tconfigStateDir,\n\t\t\tcwd: identityCwdFromConfig(identityInputs.configPath),\n\t\t});\n\t} catch (cause) {\n\t\t// `resolveIdentity` -> `resolveNetworkSync` throws\n\t\t// `DevstackNetworkParseError` (a plain Error, NOT a CliError) on a\n\t\t// malformed `--network`/`$DEVSTACK_NETWORK` value. This runs OUTSIDE\n\t\t// the argv pre-parse try/catch above and BEFORE dispatch, so without\n\t\t// this guard the throw escapes to the bin entry's generic `.catch`\n\t\t// (exit 1, no envelope). Convert to `CliUsageError` and route through\n\t\t// the same envelope path the parse-argv block uses so a bad value\n\t\t// exits USAGE (64) with a JSON envelope in `--json` mode — never the\n\t\t// disallowed generic exit 1.\n\t\tconst error =\n\t\t\tcause instanceof DevstackNetworkParseError\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: cause instanceof CliUsageError\n\t\t\t\t\t? cause\n\t\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(resolve-identity)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tconst deps = buildDirectDeps(identity);\n\tawait Effect.runPromise(\n\t\tdispatch(deps, {\n\t\t\targv,\n\t\t\tenv: {\n\t\t\t\t...env,\n\t\t\t\tDEVSTACK_APP: identity.app,\n\t\t\t\tDEVSTACK_STACK: identity.stack,\n\t\t\t\tDEVSTACK_STATE_DIR: identity.runtimeRoot,\n\t\t\t},\n\t\t\tstdinIsTty,\n\t\t}),\n\t);\n};\n\nconst isMainEntrypoint = (): boolean => {\n\tconst argvPath = process.argv[1];\n\tif (argvPath === undefined) return false;\n\ttry {\n\t\treturn realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url));\n\t} catch {\n\t\treturn import.meta.url === pathToFileURL(argvPath).href;\n\t}\n};\n\nif (isMainEntrypoint()) {\n\t// Intentionally do NOT call `process.exit(...)` after `runCli`:\n\t// `process.exit` synchronously terminates the event loop before any\n\t// pending `setImmediate` work flushes. The `up` lifecycle's hard-kill\n\t// path schedules its escalation via `setImmediate(process.exit)` in\n\t// `up-lifecycle.ts:scheduleProcessExit` (the file-header invariant for\n\t// SIGINT → finalizers requires the outer Node fiber to drain\n\t// naturally). Letting Node's natural exit handle the shutdown\n\t// preserves that invariant. We only set `process.exitCode` so the\n\t// final OS exit code reflects the verb's outcome.\n\trunCli().catch((err) => {\n\t\tprocess.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n\t\tprocess.exitCode = ExitCode.GENERIC;\n\t});\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"main.mjs","names":["resolvePath"],"sources":["../../src/cli/main.ts"],"sourcesContent":["// devstack CLI bin entry — argv → identity → deps → dispatch → exit.\n//\n// This file is the executable entry the `bin: { devstack: ... }`\n// package.json field points at. Build output: `dist/cli/main.mjs`.\n//\n// Architecture invariant (surfaces/cli/index.ts header):\n// \"`up` must hand its long-running effect to the outer Node runtime\n// directly, not nest a runtime — otherwise SIGINT cannot reach\n// scope finalizers and container teardown leaks.\"\n//\n// Shape:\n// 1. Pre-parse identity inputs from argv + env (see\n// `identityInputsFromArgv`).\n// 2. Resolve identity (app / stack / network / runtime root) via the\n// shared `api/inference-network` helpers.\n// 3. Build the dispatcher's `CliDeps` bundle from each verb's wiring\n// module in `cli/wirings/`.\n// 4. Hand control to the Stricli-backed dispatcher, which routes to\n// the verb-scoped wiring function.\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, join, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { endpointKey, pluginKey } from '../substrate/brand.ts';\nimport { emptyProjection } from '../substrate/runtime/projection/state-ref.ts';\nimport type { SubscribableState } from '../substrate/projection.ts';\nimport { readStackContext } from '../build-integrations/runtime/read-stack-context.ts';\nimport type { StackContext } from '../build-integrations/runtime/stack-context.ts';\nimport { dispatch, type CliDeps, CliUsageError } from '../surfaces/cli/index.ts';\nimport { emitFailure, nodeProcessIO } from '../surfaces/cli/output.ts';\nimport { ENV_VARS } from '../surfaces/cli/flags.ts';\nimport { defaultProbes } from './doctor-probes.ts';\nimport { nodeConfirmPrompt } from '../surfaces/cli/commands/confirm-node.ts';\nimport type { StatusReader } from '../surfaces/cli/commands/status.ts';\nimport { ExitCode } from '../surfaces/cli/sysexits.ts';\nimport { makeSnapshotReader } from './snapshot-reader.ts';\nimport {\n\tDevstackNetworkParseError,\n\tresolveAppName,\n\tresolveNetworkSync,\n\tresolveStackName,\n\tresolveStateDir,\n} from '../api/inference-network.ts';\nimport { makeDirectPruneDeps } from './prune-direct.ts';\nimport { runUpLive } from './wirings/up.ts';\nimport { runApplyLive } from './wirings/apply.ts';\nimport { runCodegen } from './wirings/codegen.ts';\nimport { runDumpDeployment } from './wirings/dump-deployment.ts';\nimport {\n\trunSnapshotCaptureLiveAware,\n\trunSnapshotDeleteDirect,\n\trunSnapshotRestoreDirect,\n} from './wirings/snapshot.ts';\nimport { runWipeDirect, runWipePlanDirect } from './wirings/wipe.ts';\nimport { makeConfigLoader, resolveConfigPath } from './wirings/config-loader.ts';\nimport type { ResolvedIdentity } from './wirings/identity.ts';\n\n// -----------------------------------------------------------------------------\n// Identity resolution\n// -----------------------------------------------------------------------------\n\n/** Resolve identity from flags + env. App and stack fall through the\n * shared cwd/package metadata resolver before their defaults.\n *\n * State-dir precedence ladder:\n * `--state-dir` flag (`stateDir`) > `config.options.stateDir`\n * (`configStateDir`) > `$DEVSTACK_STATE_DIR` > `<cwd>/.devstack`.\n * The flag maps to `resolveStateDir`'s top `runtimeRoot` rung and the\n * config value to its `stateDir` rung so the flag always wins over a\n * config-declared `defineDevstack({ stateDir })`.\n *\n * Stack-name precedence ladder (the SAME flag-beats-config shape as\n * state-dir, so the two are consistent and reviewable side-by-side):\n * `--stack` flag / `$DEVSTACK_STACK` (`params.stack`)\n * > config's `defineDevstack({ stackName })`\n * > cwd/package inference (default `'main'`).\n * Config-vs-explicit resolution can't happen here (the config isn't\n * loaded yet for the no-config verbs), so we record the explicit value\n * on `ResolvedIdentity.explicitStack` and let the verb wirings apply\n * `config.stackName` ONLY when no explicit stack was given — see\n * `effectiveStackName` in `cli/wirings/identity.ts`. `params.stack` is\n * exactly the explicit `--stack`-or-`$DEVSTACK_STACK` value (the argv\n * pre-parser seeds it from env, then lets the flag overwrite it), and\n * is `undefined` when neither was provided. */\nconst resolveIdentity = (params: {\n\treadonly app: string | undefined;\n\treadonly stack: string | undefined;\n\treadonly network: string | undefined;\n\treadonly stateDir: string | undefined;\n\treadonly configStateDir?: string | undefined;\n\treadonly cwd?: string;\n}): ResolvedIdentity => {\n\tconst cwd = params.cwd ?? process.cwd();\n\tconst app = resolveAppName({\n\t\texplicit: params.app,\n\t\tcwd,\n\t});\n\tconst runtimeRoot = resolveStateDir({\n\t\truntimeRoot: params.stateDir,\n\t\tstateDir: params.configStateDir,\n\t\tenv: process.env.DEVSTACK_STATE_DIR,\n\t\tcwd,\n\t});\n\tconst stacksRoot = resolvePath(runtimeRoot, 'stacks');\n\t// `resolveStackName` here folds explicit (flag/env) > inferred >\n\t// default. Crucially it does NOT see `config.stackName`: a non-empty\n\t// `params.stack` means the operator was explicit, so `stack` already\n\t// equals the explicit value and `config.stackName` must not override\n\t// it downstream. An empty `params.stack` falls through to the\n\t// inferred/default name, which a verb's `config.stackName` may then\n\t// supersede via `effectiveStackName`.\n\tconst explicitStack =\n\t\tparams.stack !== undefined && params.stack.length > 0 ? params.stack : undefined;\n\tconst stack = resolveStackName({\n\t\texplicit: params.stack,\n\t\tcwd,\n\t});\n\t// Centralized explicit > env > default ladder. Throws\n\t// `DevstackNetworkParseError` on a malformed value so the CLI fails\n\t// fast with a structured error instead of a downstream cryptic\n\t// network-probe failure. The identity's network is the canonical name\n\t// (`localnet`, `testnet`, …), independent of how it was spelled.\n\tconst network = resolveNetworkSync({\n\t\texplicit: params.network,\n\t\tenv: process.env.DEVSTACK_NETWORK,\n\t\texplicitSource: '--network',\n\t}).parsed.name;\n\tconst stackRoot = resolvePath(stacksRoot, stack);\n\treturn {\n\t\tapp,\n\t\tstack,\n\t\tnetwork,\n\t\truntimeRoot,\n\t\tstacksRoot,\n\t\tstackRoot,\n\t\trosterFile: resolvePath(stackRoot, 'roster.json'),\n\t\texplicitStack,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Verb deps composition\n// -----------------------------------------------------------------------------\n\n/**\n * Degraded offline-status projection — built from the on-disk manifest,\n * NOT from a persisted projection twin (the projection snapshot file no\n * longer exists). When the stack is DOWN, the manifest is the only\n * durable record of the boot; `status` surfaces identity + endpoints\n * from it and leaves the live-only slices (`rows` / `accounts` /\n * `packages` / `errors`) empty. A freshly-booted stack also has empty\n * `rows`, so the status renderer already tolerates this shape.\n *\n * Pure projector — exported for the focused unit test. Starts from\n * `emptyProjection()` (the closed-vocabulary baseline) and fills only\n * the two manifest-backed slices, so no display vocabulary can leak in.\n * The manifest identity tuple is `{ app, stack, network }`, matching\n * the projection's identity shape directly. Endpoints get re-branded\n * (`endpointKey` / `pluginKey`)\n * to the projection's branded shape; `registeredAt` is unknown offline,\n * so it defaults to `0`.\n */\nexport const degradedStatusFromContext = (ctx: StackContext): SubscribableState => ({\n\t...emptyProjection(),\n\tidentity: {\n\t\tapp: ctx.identity.app,\n\t\tstack: ctx.identity.stack,\n\t\tnetwork: ctx.identity.network,\n\t},\n\tendpoints: ctx.endpoints.all().map((endpoint) => ({\n\t\tendpointKey: endpointKey(endpoint.endpointKey),\n\t\tpluginKey: pluginKey(endpoint.pluginKey),\n\t\tname: endpoint.name,\n\t\turl: endpoint.url,\n\t\tdisplayUrl: endpoint.displayUrl,\n\t\twireProtocol: endpoint.wireProtocol,\n\t\tregisteredAt: 0,\n\t})),\n});\n\nconst projectionStatusReader = (identity: ResolvedIdentity): StatusReader => ({\n\treadState: () =>\n\t\tEffect.sync(() => {\n\t\t\t// Offline read: project the on-disk manifest for THIS stack root.\n\t\t\t// A missing / malformed manifest (no `up` yet, or a hand-edit)\n\t\t\t// is a tolerated \"no state present\" — `readStackContext` throws\n\t\t\t// in that case, so swallow the throw and report `null` (the\n\t\t\t// status command's contract for an absent stack).\n\t\t\ttry {\n\t\t\t\tconst ctx = readStackContext({ manifestPath: join(identity.stackRoot, 'manifest.json') });\n\t\t\t\treturn degradedStatusFromContext(ctx);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n});\n\nconst buildDirectDeps = (identity: ResolvedIdentity): CliDeps => {\n\treturn {\n\t\tup: {\n\t\t\trun: (flags) =>\n\t\t\t\trunUpLive(flags.configPath, identity, {\n\t\t\t\t\trenderer: flags.renderer,\n\t\t\t\t\tstdoutIsTty: Boolean((process.stdout as { isTTY?: boolean }).isTTY),\n\t\t\t\t\tfromSnapshot: flags.fromSnapshot,\n\t\t\t\t\tsnapshotCache: flags.snapshotCache,\n\t\t\t\t\tsnapshotStalePolicy: flags.snapshotStalePolicy,\n\t\t\t\t}),\n\t\t},\n\t\tapply: {\n\t\t\trun: (flags) => runApplyLive(flags.configPath, identity),\n\t\t},\n\t\tcodegen: {\n\t\t\trun: (flags) => runCodegen(flags.configPath),\n\t\t},\n\t\tdumpDeployment: {\n\t\t\trun: (flags) =>\n\t\t\t\trunDumpDeployment(identity, {\n\t\t\t\t\tconfigPath: flags.configPath,\n\t\t\t\t\tout: flags.out,\n\t\t\t\t\tnetwork: flags.network,\n\t\t\t\t\tio: flags.io,\n\t\t\t\t\toutputMode: flags.outputMode,\n\t\t\t\t}),\n\t\t},\n\t\tstatus: { reader: projectionStatusReader(identity) },\n\t\tsnapshot: {\n\t\t\treader: makeSnapshotReader(identity),\n\t\t\tcapture: (args) => runSnapshotCaptureLiveAware(identity, args),\n\t\t\trestore: (snapshotId) => runSnapshotRestoreDirect(identity, snapshotId),\n\t\t\tdelete: (snapshotId) => runSnapshotDeleteDirect(identity, snapshotId),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t\tprune: makeDirectPruneDeps({ runtimeRoot: identity.runtimeRoot }),\n\t\tdoctor: {\n\t\t\tprobes: defaultProbes({\n\t\t\t\tstateDir: identity.runtimeRoot,\n\t\t\t\tappRoot: identity.stacksRoot,\n\t\t\t}),\n\t\t},\n\t\tconfig: { loader: makeConfigLoader() },\n\t\twipe: {\n\t\t\twipe: () => runWipeDirect(identity),\n\t\t\tplan: () => runWipePlanDirect(identity),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Argv pre-parser\n// -----------------------------------------------------------------------------\n\n/** @internal Exported for tests. Resolves identity flag inputs from a\n * `--app <x>` / `--stack <x>` / `--network <x>` / `--state-dir <x>` /\n * `--config <x>` argv, falling back to `DEVSTACK_*` env vars. Throws\n * on a missing or flag-shaped value so a typo doesn't silently demote\n * a downstream flag, and on a duplicate flag (`--app a --app b`) so\n * the pre-parser does not silently last-write-wins a value that\n * Stricli will later reject outright. */\nexport const identityInputsFromArgv = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n) => {\n\tlet app = env.DEVSTACK_APP;\n\tlet stack = env.DEVSTACK_STACK;\n\tlet network = env.DEVSTACK_NETWORK;\n\t// `stateDir` intentionally captures the `--state-dir` flag ONLY. The\n\t// full ladder — `--state-dir` flag > `config.options.stateDir`\n\t// (`defineDevstack({ stateDir })`) > `$DEVSTACK_STATE_DIR` >\n\t// `<cwd>/.devstack` — is assembled at the `resolveStateDir(...)`\n\t// call-site in `resolveIdentity`: the flag wins (top `runtimeRoot`\n\t// rung), the best-effort config value sits below it (`stateDir`\n\t// rung), then env, then the cwd default. Keeping the pre-parser a\n\t// thin flag-extractor means config + env precedence live in one place.\n\tlet stateDir: string | undefined;\n\tlet configPath = env.DEVSTACK_CONFIG;\n\t// Tracks which flags have been seen on the argv side so a second\n\t// occurrence trips a usage error before the value silently overwrites\n\t// the first. Env-sourced defaults are NOT counted (they are not user\n\t// argv).\n\tconst seenArgvFlags = new Set<string>();\n\tfor (let i = 0; i < argv.length; i += 1) {\n\t\tconst token = argv[i]!;\n\t\tconst readValue = (name: string): string | undefined => {\n\t\t\tlet value: string | undefined;\n\t\t\t// `--name=value` form: trust the literal between `=` and end.\n\t\t\tif (token.startsWith(`--${name}=`)) {\n\t\t\t\tvalue = token.slice(name.length + 3);\n\t\t\t}\n\t\t\t// `--name value` form: peek the next token. Reject another\n\t\t\t// flag token (`--foo`) as the value — it almost certainly\n\t\t\t// means the user meant `--name <empty>` (typo / forgotten\n\t\t\t// argument) and quietly absorbing `--foo` as the value\n\t\t\t// silently demotes a downstream flag.\n\t\t\telse if (token === `--${name}`) {\n\t\t\t\tconst next = argv[i + 1];\n\t\t\t\tif (next === undefined) {\n\t\t\t\t\tthrow new CliUsageError({ message: `flag --${name} requires a value` });\n\t\t\t\t}\n\t\t\t\tif (next.startsWith('--')) {\n\t\t\t\t\tthrow new CliUsageError({\n\t\t\t\t\t\tmessage: `flag --${name} requires a value; got \"${next}\" which looks like a flag`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tvalue = next;\n\t\t\t}\n\t\t\tif (value === undefined) return undefined;\n\t\t\tif (seenArgvFlags.has(name)) {\n\t\t\t\tthrow new CliUsageError({ message: `flag --${name} given more than once` });\n\t\t\t}\n\t\t\tseenArgvFlags.add(name);\n\t\t\treturn value;\n\t\t};\n\t\tapp = readValue('app') ?? app;\n\t\tstack = readValue('stack') ?? stack;\n\t\tnetwork = readValue('network') ?? network;\n\t\tstateDir = readValue('state-dir') ?? stateDir;\n\t\tconfigPath = readValue('config') ?? configPath;\n\t}\n\treturn { app, stack, network, stateDir, configPath };\n};\n\nconst identityCwdFromConfig = (configPath: string | undefined): string => {\n\tconst resolved = resolveConfigPath(configPath);\n\treturn resolved === null ? process.cwd() : dirname(resolved);\n};\n\n/** Whether the resolved argv is a purely-informational invocation that\n * never consumes `identity.runtimeRoot` (the only value the config\n * pre-load feeds). For these we SKIP `configStateDirBestEffort` so we\n * don't dynamic-import (and run the top-level side effects of) the\n * user's `devstack.config.ts` — plus walk parent dirs — for commands\n * the file header itself says must stay cheap:\n *\n * - empty argv → Stricli prints root help\n * - `--help`/`-h` or `--version`/`-v` anywhere → Stricli short-circuits\n * - first token `schema` → emits the static CLI schema (no state dir)\n *\n * An unknown verb also never reaches a state-dir consumer (Stricli\n * fails the parse), but we keep pre-loading on the general path: the\n * pre-parser already accepted the identity flags, and gating on a\n * closed verb set here would duplicate the dispatcher's route table.\n * The two flagged-as-wasteful informational paths (help/version/schema)\n * are the ones with a registered short-circuit, so skipping exactly\n * those removes the eager-import surprise without re-deriving routes. */\nconst argvSkipsConfigPreload = (argv: ReadonlyArray<string>): boolean => {\n\tif (argv.length === 0) return true;\n\tfor (const token of argv) {\n\t\tif (token === '--help' || token === '-h' || token === '--version' || token === '-v') {\n\t\t\treturn true;\n\t\t}\n\t}\n\tconst firstVerb = argv.find((token) => !token.startsWith('-'));\n\treturn firstVerb === 'schema';\n};\n\n/** Best-effort read of `config.options.stateDir` (the value a program\n * sets via `defineDevstack({ stateDir })`) for the state-dir precedence\n * ladder in `resolveIdentity`. Swallows EVERY config-loader failure\n * (not-found AND evaluation errors) and returns `undefined`:\n *\n * - No-config verbs (`prune`, `wipe`) MUST keep resolving identity\n * without a config, so a missing config silently falls through to\n * the flag > env > cwd ladder.\n * - A genuinely malformed config is NOT surfaced here — the verbs that\n * actually consume config (`up` / `apply` via `makeConfigLoader`)\n * re-load it and surface the typed `CliConfig*` error through the\n * normal envelope path, so behavior for those verbs is unchanged.\n *\n * This is one redundant config evaluation for `up` / `apply` (the verb\n * wiring re-loads via its own loader); threading the loaded value into\n * the verb dispatch would require touching the off-limits `wirings/up.ts`\n * signature, so the duplicate import is accepted deliberately. */\nconst configStateDirBestEffort = async (\n\tconfigPath: string | undefined,\n): Promise<string | undefined> => {\n\tconst loaded = await Effect.runPromise(makeConfigLoader().load(configPath).pipe(Effect.option));\n\tif (loaded._tag === 'None') return undefined;\n\tconst options = (loaded.value.stack as { readonly options?: { readonly stateDir?: string } })\n\t\t.options;\n\tconst stateDir = options?.stateDir;\n\treturn stateDir !== undefined && stateDir.length > 0 ? stateDir : undefined;\n};\n\n// -----------------------------------------------------------------------------\n// Bin entry\n// -----------------------------------------------------------------------------\n\nexport const runCli = async (\n\targv: ReadonlyArray<string> = process.argv.slice(2),\n): Promise<void> => {\n\tconst stdinIsTty = Boolean((process.stdin as { isTTY?: boolean }).isTTY);\n\tconst env: Record<string, string | undefined> = { ...process.env };\n\tlet identityInputs: ReturnType<typeof identityInputsFromArgv>;\n\ttry {\n\t\tidentityInputs = identityInputsFromArgv(argv, env);\n\t} catch (cause) {\n\t\tconst error =\n\t\t\tcause instanceof CliUsageError\n\t\t\t\t? cause\n\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(parse-argv)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\t// Best-effort config pre-load for the state-dir ladder. Swallows\n\t// not-found / malformed configs so no-config verbs (prune/wipe) keep\n\t// working; the `--state-dir` flag still wins over the config value.\n\t// Skipped for informational invocations (help/version/schema/empty)\n\t// that never read `identity.runtimeRoot`, so those commands don't\n\t// dynamic-import the user's config (and run its top-level side\n\t// effects) just to print help.\n\tconst configStateDir = argvSkipsConfigPreload(argv)\n\t\t? undefined\n\t\t: await configStateDirBestEffort(identityInputs.configPath);\n\tlet identity: ResolvedIdentity;\n\ttry {\n\t\tidentity = resolveIdentity({\n\t\t\tapp: identityInputs.app,\n\t\t\tstack: identityInputs.stack,\n\t\t\tnetwork: identityInputs.network,\n\t\t\tstateDir: identityInputs.stateDir,\n\t\t\tconfigStateDir,\n\t\t\tcwd: identityCwdFromConfig(identityInputs.configPath),\n\t\t});\n\t} catch (cause) {\n\t\t// `resolveIdentity` -> `resolveNetworkSync` throws\n\t\t// `DevstackNetworkParseError` (a plain Error, NOT a CliError) on a\n\t\t// malformed `--network`/`$DEVSTACK_NETWORK` value. This runs OUTSIDE\n\t\t// the argv pre-parse try/catch above and BEFORE dispatch, so without\n\t\t// this guard the throw escapes to the bin entry's generic `.catch`\n\t\t// (exit 1, no envelope). Convert to `CliUsageError` and route through\n\t\t// the same envelope path the parse-argv block uses so a bad value\n\t\t// exits USAGE (64) with a JSON envelope in `--json` mode — never the\n\t\t// disallowed generic exit 1.\n\t\tconst error =\n\t\t\tcause instanceof DevstackNetworkParseError\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: cause instanceof CliUsageError\n\t\t\t\t\t? cause\n\t\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(resolve-identity)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tconst deps = buildDirectDeps(identity);\n\tawait Effect.runPromise(\n\t\tdispatch(deps, {\n\t\t\targv,\n\t\t\tenv: {\n\t\t\t\t...env,\n\t\t\t\tDEVSTACK_APP: identity.app,\n\t\t\t\tDEVSTACK_STACK: identity.stack,\n\t\t\t\tDEVSTACK_STATE_DIR: identity.runtimeRoot,\n\t\t\t},\n\t\t\tstdinIsTty,\n\t\t}),\n\t);\n};\n\nconst isMainEntrypoint = (): boolean => {\n\tconst argvPath = process.argv[1];\n\tif (argvPath === undefined) return false;\n\ttry {\n\t\treturn realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url));\n\t} catch {\n\t\treturn import.meta.url === pathToFileURL(argvPath).href;\n\t}\n};\n\nif (isMainEntrypoint()) {\n\t// Intentionally do NOT call `process.exit(...)` after `runCli`:\n\t// `process.exit` synchronously terminates the event loop before any\n\t// pending `setImmediate` work flushes. The `up` lifecycle's hard-kill\n\t// path schedules its escalation via `setImmediate(process.exit)` in\n\t// `up-lifecycle.ts:scheduleProcessExit` (the file-header invariant for\n\t// SIGINT → finalizers requires the outer Node fiber to drain\n\t// naturally). Letting Node's natural exit handle the shutdown\n\t// preserves that invariant. We only set `process.exitCode` so the\n\t// final OS exit code reflects the verb's outcome.\n\trunCli().catch((err) => {\n\t\tprocess.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n\t\tprocess.exitCode = ExitCode.GENERIC;\n\t});\n}\n"],"mappings":"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"}
|
|
@@ -84,7 +84,6 @@ const runApplyLive = (configPath, identity) => {
|
|
|
84
84
|
options: stack.options
|
|
85
85
|
}, identityValue, state, {
|
|
86
86
|
extras: stack.options.extras,
|
|
87
|
-
networkOptions: stack.options.networkOptions,
|
|
88
87
|
lifetime: "one-shot"
|
|
89
88
|
});
|
|
90
89
|
}).pipe(Effect.provide(substrateLayers), Effect.provide(Logger.layer([Logger.consolePretty()])), Effect.matchCauseEffect({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/apply.ts"],"sourcesContent":["// `devstack apply` verb wiring.\n//\n// Live-aware: if a supervisor owns the selected stack, publish an\n// `apply.requested` command and await its ack. Otherwise run a\n// one-shot supervise that exits when all plugins are ready.\n//\n// Logger layer: `Logger.consolePretty()`. `apply` is the documented\n// one-shot mode (no TUI) — its consumers are CI logs and ad-hoc\n// operator runs that read raw stderr. Structured `Effect.log*` records\n// are the only operator-visible diagnostic for non-TUI surfaces, so we\n// route them through the pretty console layer. The sibling `up` verb\n// silences this layer (see `wirings/up.ts` header) because it owns the\n// TUI and would render duplicate / scrambled output otherwise.\n\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, Logger } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport { StackPathsService } from '../../substrate/runtime/paths.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\trecoverInterruptedRestore,\n\tSnapshotOrchestratorService,\n} from '../../orchestrators/snapshot/index.ts';\nimport { type CliError, CliInternalError } from '../../surfaces/cli/errors.ts';\nimport { type CommandResult, probeSupervisorPresence } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeConfigLoader } from './config-loader.ts';\nimport {\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\tstackRootFor,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { buildVerbLayers } from '../../orchestrators/layers.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst LIVE_APPLY_ACK_TIMEOUT_MILLIS = 10 * 60 * 1000;\n\n/** Outcome of the live-supervisor probe + dispatch:\n * - `kind: 'direct'` — no supervisor; caller runs the direct path.\n * - `kind: 'live-ok'` — live supervisor acked; verb is done.\n * - `kind: 'live-failed'` — live supervisor refused/timed-out; caller\n * surfaces the typed `CliError` via the dispatcher envelope. */\ntype ApplyDispatch =\n\t| { readonly kind: 'direct' }\n\t| { readonly kind: 'live-ok' }\n\t| { readonly kind: 'live-failed'; readonly error: CliError };\n\n/** Probe the roster file; if a supervisor is live, publish\n * `apply.requested` and await its ack. */\nconst runApplyAgainstLiveSupervisor = (\n\tidentity: ResolvedIdentity,\n\tidentityValue: Identity,\n): Effect.Effect<ApplyDispatch> =>\n\tEffect.gen(function* () {\n\t\tconst stackRoot = stackRootFor(identity.runtimeRoot, String(identityValue.stack));\n\t\tconst presence = yield* probeSupervisorPresence(resolvePath(stackRoot, 'roster.json')).pipe(\n\t\t\tEffect.catch(() => Effect.succeed({ live: false, pid: null, hostname: null })),\n\t\t);\n\t\tif (!presence.live) return { kind: 'direct' as const };\n\n\t\tconst exit = yield* Effect.exit(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst publisher = yield* makeCommandChannelPublisher(commandChannelPaths(stackRoot));\n\t\t\t\tconst published = yield* publisher.publish({ tag: 'apply.requested' });\n\t\t\t\tconst reply = yield* publisher.awaitCompletion(published, {\n\t\t\t\t\ttimeoutMillis: LIVE_APPLY_ACK_TIMEOUT_MILLIS,\n\t\t\t\t});\n\t\t\t\tif (!reply.ok) {\n\t\t\t\t\treturn yield* Effect.fail(reply.message);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tif (Exit.isFailure(exit)) {\n\t\t\treturn {\n\t\t\t\tkind: 'live-failed' as const,\n\t\t\t\terror: new CliInternalError({\n\t\t\t\t\tmessage: 'live stack apply failed',\n\t\t\t\t\tcause: Cause.pretty(exit.cause as Cause.Cause<unknown>),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\n\t\treturn { kind: 'live-ok' as const };\n\t});\n\nexport const runApplyLive = (\n\tconfigPath: string | undefined,\n\tidentity: ResolvedIdentity,\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\t// Config-load failures surface as typed `CliError`s; the\n\t\t// dispatcher's outer `emitFailure` renders the envelope.\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 stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\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 live-supervisor probe + roster paths target the same stack\n\t\t// the operator selected — matching `snapshot.ts`. An explicit\n\t\t// `--stack` must NOT be overridden by `config.stackName`, otherwise\n\t\t// `apply` would probe/boot the wrong stack's supervisor.\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst dispatch = yield* runApplyAgainstLiveSupervisor(effectiveIdentity, identityValue);\n\t\tif (dispatch.kind === 'live-failed') {\n\t\t\treturn yield* Effect.fail(dispatch.error);\n\t\t}\n\t\tif (dispatch.kind === 'live-ok') {\n\t\t\treturn { exitCode: ExitCode.OK };\n\t\t}\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\tconst stackPaths = yield* StackPathsService;\n\t\t\tconst computeGraphInput = computeSnapshotGraphInputFromStack({\n\t\t\t\tstack,\n\t\t\t\tdevstackVersion: readDevstackVersion({ fallback: '0.0.0' }),\n\t\t\t});\n\t\t\t// Mirror the up-path recovery: resume any restore interrupted by a\n\t\t\t// hard kill between the atomic swap and the image-promotion handoff\n\t\t\t// completing (the interrupted-restore sentinel rode the swap into the\n\t\t\t// live root and was never cleared) BEFORE the one-shot apply starts\n\t\t\t// the stack. Idempotent + no-op when no sentinel is present.\n\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\tyield* recoverInterruptedRestore({\n\t\t\t\tliveRoot: stackPaths.stackRoot,\n\t\t\t\trestoreSnapshot: (id) =>\n\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\tEffect.flatMap((currentGraphInput) =>\n\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\tcurrentGraphInput,\n\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\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});\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'`). Run-to-completion + result semantics are\n\t\t\t// unchanged: the surrounding `matchCauseEffect` below still maps the\n\t\t\t// supervised cause/success onto the `CommandResult`.\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},\n\t\t\t);\n\t\t});\n\n\t\treturn yield* program.pipe(\n\t\t\tEffect.provide(substrateLayers),\n\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\tEffect.matchCauseEffect({\n\t\t\t\tonFailure: (cause): Effect.Effect<CommandResult, CliError> =>\n\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\tmessage: 'stack apply failed',\n\t\t\t\t\t\t\tcause: Cause.pretty(cause as Cause.Cause<unknown>),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\tonSuccess: (): Effect.Effect<CommandResult, CliError> =>\n\t\t\t\t\tEffect.succeed({ exitCode: ExitCode.OK }),\n\t\t\t}),\n\t\t);\n\t});\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"apply.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/apply.ts"],"sourcesContent":["// `devstack apply` verb wiring.\n//\n// Live-aware: if a supervisor owns the selected stack, publish an\n// `apply.requested` command and await its ack. Otherwise run a\n// one-shot supervise that exits when all plugins are ready.\n//\n// Logger layer: `Logger.consolePretty()`. `apply` is the documented\n// one-shot mode (no TUI) — its consumers are CI logs and ad-hoc\n// operator runs that read raw stderr. Structured `Effect.log*` records\n// are the only operator-visible diagnostic for non-TUI surfaces, so we\n// route them through the pretty console layer. The sibling `up` verb\n// silences this layer (see `wirings/up.ts` header) because it owns the\n// TUI and would render duplicate / scrambled output otherwise.\n\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, Logger } from 'effect';\n\nimport type { Identity } from '../../substrate/identity.ts';\nimport { StackPathsService } from '../../substrate/runtime/paths.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\trecoverInterruptedRestore,\n\tSnapshotOrchestratorService,\n} from '../../orchestrators/snapshot/index.ts';\nimport { type CliError, CliInternalError } from '../../surfaces/cli/errors.ts';\nimport { type CommandResult, probeSupervisorPresence } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeConfigLoader } from './config-loader.ts';\nimport {\n\tidentityValueFor,\n\tresolvedIdentityForStack,\n\tstackRootFor,\n\ttype ResolvedIdentity,\n} from './identity.ts';\nimport { buildVerbLayers } from '../../orchestrators/layers.ts';\nimport { readDevstackVersion } from './read-devstack-version.ts';\n\nconst LIVE_APPLY_ACK_TIMEOUT_MILLIS = 10 * 60 * 1000;\n\n/** Outcome of the live-supervisor probe + dispatch:\n * - `kind: 'direct'` — no supervisor; caller runs the direct path.\n * - `kind: 'live-ok'` — live supervisor acked; verb is done.\n * - `kind: 'live-failed'` — live supervisor refused/timed-out; caller\n * surfaces the typed `CliError` via the dispatcher envelope. */\ntype ApplyDispatch =\n\t| { readonly kind: 'direct' }\n\t| { readonly kind: 'live-ok' }\n\t| { readonly kind: 'live-failed'; readonly error: CliError };\n\n/** Probe the roster file; if a supervisor is live, publish\n * `apply.requested` and await its ack. */\nconst runApplyAgainstLiveSupervisor = (\n\tidentity: ResolvedIdentity,\n\tidentityValue: Identity,\n): Effect.Effect<ApplyDispatch> =>\n\tEffect.gen(function* () {\n\t\tconst stackRoot = stackRootFor(identity.runtimeRoot, String(identityValue.stack));\n\t\tconst presence = yield* probeSupervisorPresence(resolvePath(stackRoot, 'roster.json')).pipe(\n\t\t\tEffect.catch(() => Effect.succeed({ live: false, pid: null, hostname: null })),\n\t\t);\n\t\tif (!presence.live) return { kind: 'direct' as const };\n\n\t\tconst exit = yield* Effect.exit(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst publisher = yield* makeCommandChannelPublisher(commandChannelPaths(stackRoot));\n\t\t\t\tconst published = yield* publisher.publish({ tag: 'apply.requested' });\n\t\t\t\tconst reply = yield* publisher.awaitCompletion(published, {\n\t\t\t\t\ttimeoutMillis: LIVE_APPLY_ACK_TIMEOUT_MILLIS,\n\t\t\t\t});\n\t\t\t\tif (!reply.ok) {\n\t\t\t\t\treturn yield* Effect.fail(reply.message);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\n\t\tif (Exit.isFailure(exit)) {\n\t\t\treturn {\n\t\t\t\tkind: 'live-failed' as const,\n\t\t\t\terror: new CliInternalError({\n\t\t\t\t\tmessage: 'live stack apply failed',\n\t\t\t\t\tcause: Cause.pretty(exit.cause as Cause.Cause<unknown>),\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\n\t\treturn { kind: 'live-ok' as const };\n\t});\n\nexport const runApplyLive = (\n\tconfigPath: string | undefined,\n\tidentity: ResolvedIdentity,\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\t// Config-load failures surface as typed `CliError`s; the\n\t\t// dispatcher's outer `emitFailure` renders the envelope.\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 stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\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 live-supervisor probe + roster paths target the same stack\n\t\t// the operator selected — matching `snapshot.ts`. An explicit\n\t\t// `--stack` must NOT be overridden by `config.stackName`, otherwise\n\t\t// `apply` would probe/boot the wrong stack's supervisor.\n\t\tconst effectiveIdentity = resolvedIdentityForStack(identity, stack);\n\t\tconst identityValue: Identity = identityValueFor(effectiveIdentity);\n\t\tconst dispatch = yield* runApplyAgainstLiveSupervisor(effectiveIdentity, identityValue);\n\t\tif (dispatch.kind === 'live-failed') {\n\t\t\treturn yield* Effect.fail(dispatch.error);\n\t\t}\n\t\tif (dispatch.kind === 'live-ok') {\n\t\t\treturn { exitCode: ExitCode.OK };\n\t\t}\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\tconst stackPaths = yield* StackPathsService;\n\t\t\tconst computeGraphInput = computeSnapshotGraphInputFromStack({\n\t\t\t\tstack,\n\t\t\t\tdevstackVersion: readDevstackVersion({ fallback: '0.0.0' }),\n\t\t\t});\n\t\t\t// Mirror the up-path recovery: resume any restore interrupted by a\n\t\t\t// hard kill between the atomic swap and the image-promotion handoff\n\t\t\t// completing (the interrupted-restore sentinel rode the swap into the\n\t\t\t// live root and was never cleared) BEFORE the one-shot apply starts\n\t\t\t// the stack. Idempotent + no-op when no sentinel is present.\n\t\t\tconst snapshot = yield* SnapshotOrchestratorService;\n\t\t\tyield* recoverInterruptedRestore({\n\t\t\t\tliveRoot: stackPaths.stackRoot,\n\t\t\t\trestoreSnapshot: (id) =>\n\t\t\t\t\tcomputeGraphInput.pipe(\n\t\t\t\t\t\tEffect.flatMap((currentGraphInput) =>\n\t\t\t\t\t\t\tsnapshot.restore({\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\tcurrentGraphInput,\n\t\t\t\t\t\t\t\tgraphInputMismatchPolicy: 'warn',\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});\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'`). Run-to-completion + result semantics are\n\t\t\t// unchanged: the surrounding `matchCauseEffect` below still maps the\n\t\t\t// supervised cause/success onto the `CommandResult`.\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},\n\t\t\t);\n\t\t});\n\n\t\treturn yield* program.pipe(\n\t\t\tEffect.provide(substrateLayers),\n\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\tEffect.matchCauseEffect({\n\t\t\t\tonFailure: (cause): Effect.Effect<CommandResult, CliError> =>\n\t\t\t\t\tEffect.fail(\n\t\t\t\t\t\tnew CliInternalError({\n\t\t\t\t\t\t\tmessage: 'stack apply failed',\n\t\t\t\t\t\t\tcause: Cause.pretty(cause as Cause.Cause<unknown>),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\tonSuccess: (): Effect.Effect<CommandResult, CliError> =>\n\t\t\t\t\tEffect.succeed({ exitCode: ExitCode.OK }),\n\t\t\t}),\n\t\t);\n\t});\n};\n"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import * as NodeChildProcessSpawner from "@effect/platform-node/NodeChildProcess
|
|
|
18
18
|
* hook (skipping plugins that lack one) to derive the committed-tree
|
|
19
19
|
* contributions from config alone. KNOWN package ids come from the
|
|
20
20
|
* declared `networks` literals; LOCAL ids stay the all-zero sentinel
|
|
21
|
-
* (resolved at app build/dev time through `
|
|
21
|
+
* (resolved at app build/dev time through `__DEVSTACK_DEPLOYMENT__`). */
|
|
22
22
|
const deriveContributions = (members) => {
|
|
23
23
|
const decls = [];
|
|
24
24
|
for (const member of members) {
|
|
@@ -62,8 +62,7 @@ const buildCodegenLayer = (appRoot) => {
|
|
|
62
62
|
const platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
|
|
63
63
|
const codegenPaths = layerCodegenPaths.pipe(Layer.provideMerge(layerCodegenRoot({
|
|
64
64
|
outputDir: generatedDir,
|
|
65
|
-
stackSubdir: null
|
|
66
|
-
extrasDir: resolve(appRoot, "src", "generated-extras")
|
|
65
|
+
stackSubdir: null
|
|
67
66
|
})));
|
|
68
67
|
const moveRunner = layerSuiMoveSummaryRunnerHost.pipe(Layer.provideMerge(NodeChildProcessSpawner.layer));
|
|
69
68
|
return Layer.mergeAll(codegenPaths, moveRunner, layerMystenMoveCodegen()).pipe(Layer.provideMerge(platform));
|