@mysten-incubation/devstack 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +1 -1
  2. package/dashboard-ui/assets/{grpc-a4usE3Nk.js → grpc-CpkDu4SA.js} +1 -1
  3. package/dashboard-ui/assets/{index-CRYJ4pru.js → index-jLPRmjst.js} +39 -33
  4. package/dashboard-ui/index.html +1 -1
  5. package/dist/api/inference-network.d.mts +5 -5
  6. package/dist/api/inference-network.mjs +5 -5
  7. package/dist/api/inference-network.mjs.map +1 -1
  8. package/dist/api/run-stack-internal.mjs +2 -3
  9. package/dist/api/run-stack-internal.mjs.map +1 -1
  10. package/dist/build-integrations/dapp-kit/index.d.mts +12 -0
  11. package/dist/build-integrations/dapp-kit/index.mjs +14 -1
  12. package/dist/build-integrations/dapp-kit/index.mjs.map +1 -1
  13. package/dist/build-integrations/playwright/config.d.mts +2 -1
  14. package/dist/build-integrations/playwright/config.mjs +1 -1
  15. package/dist/build-integrations/playwright/config.mjs.map +1 -1
  16. package/dist/build-integrations/playwright/errors.d.mts +1 -1
  17. package/dist/build-integrations/playwright/errors.mjs.map +1 -1
  18. package/dist/build-integrations/playwright/index.d.mts +2 -2
  19. package/dist/build-integrations/playwright/index.mjs +2 -2
  20. package/dist/build-integrations/playwright/wallet-context.d.mts +2 -1
  21. package/dist/build-integrations/playwright/wallet-context.mjs +141 -41
  22. package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
  23. package/dist/build-integrations/runtime/dapp-kit-slot.mjs +1 -1
  24. package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
  25. package/dist/build-integrations/vite/index.d.mts +82 -40
  26. package/dist/build-integrations/vite/index.mjs +281 -112
  27. package/dist/build-integrations/vite/index.mjs.map +1 -1
  28. package/dist/build-integrations/vitest/config.mjs +3 -4
  29. package/dist/build-integrations/vitest/config.mjs.map +1 -1
  30. package/dist/build-integrations/vitest/global-setup.mjs +8 -8
  31. package/dist/build-integrations/vitest/global-setup.mjs.map +1 -1
  32. package/dist/cli/doctor-probes.mjs +7 -2
  33. package/dist/cli/doctor-probes.mjs.map +1 -1
  34. package/dist/cli/main.mjs +5 -4
  35. package/dist/cli/main.mjs.map +1 -1
  36. package/dist/cli/wirings/apply.mjs +0 -1
  37. package/dist/cli/wirings/apply.mjs.map +1 -1
  38. package/dist/cli/wirings/codegen.mjs +37 -38
  39. package/dist/cli/wirings/codegen.mjs.map +1 -1
  40. package/dist/cli/wirings/dump-deployment.mjs +141 -0
  41. package/dist/cli/wirings/dump-deployment.mjs.map +1 -0
  42. package/dist/cli/wirings/snapshot.mjs +0 -1
  43. package/dist/cli/wirings/snapshot.mjs.map +1 -1
  44. package/dist/cli/wirings/up.mjs +7 -0
  45. package/dist/cli/wirings/up.mjs.map +1 -1
  46. package/dist/contracts/codegenable.d.mts +22 -25
  47. package/dist/contracts/codegenable.mjs +35 -4
  48. package/dist/contracts/codegenable.mjs.map +1 -1
  49. package/dist/contracts/config-bindings.mjs +43 -18
  50. package/dist/contracts/config-bindings.mjs.map +1 -1
  51. package/dist/index.mjs +1 -1
  52. package/dist/orchestrators/boot.d.mts +0 -10
  53. package/dist/orchestrators/boot.mjs +24 -54
  54. package/dist/orchestrators/boot.mjs.map +1 -1
  55. package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
  56. package/dist/orchestrators/codegen/config-runtime.mjs +137 -112
  57. package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -1
  58. package/dist/orchestrators/codegen/deployment-network-file.mjs +130 -0
  59. package/dist/orchestrators/codegen/deployment-network-file.mjs.map +1 -0
  60. package/dist/orchestrators/codegen/deployment-strict.mjs +77 -0
  61. package/dist/orchestrators/codegen/deployment-strict.mjs.map +1 -0
  62. package/dist/orchestrators/codegen/deployment.d.mts +63 -0
  63. package/dist/orchestrators/codegen/deployment.mjs +153 -0
  64. package/dist/orchestrators/codegen/deployment.mjs.map +1 -0
  65. package/dist/orchestrators/codegen/errors.mjs +7 -9
  66. package/dist/orchestrators/codegen/errors.mjs.map +1 -1
  67. package/dist/orchestrators/codegen/format.mjs +20 -1
  68. package/dist/orchestrators/codegen/format.mjs.map +1 -1
  69. package/dist/orchestrators/codegen/paths.mjs +1 -8
  70. package/dist/orchestrators/codegen/paths.mjs.map +1 -1
  71. package/dist/orchestrators/codegen/service.mjs +369 -141
  72. package/dist/orchestrators/codegen/service.mjs.map +1 -1
  73. package/dist/orchestrators/layers.mjs +0 -1
  74. package/dist/orchestrators/layers.mjs.map +1 -1
  75. package/dist/orchestrators/network-options.d.mts +37 -35
  76. package/dist/orchestrators/network-options.mjs +13 -10
  77. package/dist/orchestrators/network-options.mjs.map +1 -1
  78. package/dist/plugins/account/codegen.d.mts +3 -4
  79. package/dist/plugins/account/codegen.mjs +5 -7
  80. package/dist/plugins/account/codegen.mjs.map +1 -1
  81. package/dist/plugins/account/index.mjs +1 -3
  82. package/dist/plugins/account/index.mjs.map +1 -1
  83. package/dist/plugins/coin/codegen.mjs +5 -5
  84. package/dist/plugins/coin/codegen.mjs.map +1 -1
  85. package/dist/plugins/coin/index.mjs.map +1 -1
  86. package/dist/plugins/dashboard/domain.mjs +18 -1
  87. package/dist/plugins/dashboard/domain.mjs.map +1 -1
  88. package/dist/plugins/dashboard/schema/root.mjs +5 -3
  89. package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
  90. package/dist/plugins/dashboard/schema/types.mjs +15 -2
  91. package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
  92. package/dist/plugins/deepbook/codegen.mjs +4 -4
  93. package/dist/plugins/deepbook/codegen.mjs.map +1 -1
  94. package/dist/plugins/deepbook/index.d.mts +7 -3
  95. package/dist/plugins/deepbook/index.mjs +3 -3
  96. package/dist/plugins/deepbook/index.mjs.map +1 -1
  97. package/dist/plugins/internal/codegen-helpers.mjs +0 -1
  98. package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
  99. package/dist/plugins/package/codegen.d.mts +1 -12
  100. package/dist/plugins/package/codegen.mjs +43 -25
  101. package/dist/plugins/package/codegen.mjs.map +1 -1
  102. package/dist/plugins/package/dep-resolution.mjs +1 -1
  103. package/dist/plugins/package/dep-resolution.mjs.map +1 -1
  104. package/dist/plugins/package/index.d.mts +14 -11
  105. package/dist/plugins/package/index.mjs +6 -7
  106. package/dist/plugins/package/index.mjs.map +1 -1
  107. package/dist/plugins/seal/codegen.mjs +3 -3
  108. package/dist/plugins/seal/codegen.mjs.map +1 -1
  109. package/dist/plugins/seal/index.mjs.map +1 -1
  110. package/dist/plugins/sui/codegen.mjs +5 -4
  111. package/dist/plugins/sui/codegen.mjs.map +1 -1
  112. package/dist/plugins/sui/index.d.mts +10 -10
  113. package/dist/plugins/sui/index.mjs +1 -2
  114. package/dist/plugins/sui/index.mjs.map +1 -1
  115. package/dist/plugins/sui/move-summary-runner.mjs +29 -2
  116. package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
  117. package/dist/plugins/wallet/codegen.d.mts +8 -12
  118. package/dist/plugins/wallet/codegen.mjs +22 -15
  119. package/dist/plugins/wallet/codegen.mjs.map +1 -1
  120. package/dist/plugins/wallet/index.d.mts +4 -3
  121. package/dist/plugins/wallet/index.mjs +1 -1
  122. package/dist/plugins/wallet/index.mjs.map +1 -1
  123. package/dist/plugins/wallet/service.d.mts +2 -2
  124. package/dist/plugins/wallet/service.mjs +2 -3
  125. package/dist/plugins/wallet/service.mjs.map +1 -1
  126. package/dist/plugins/walrus/codegen.mjs +4 -4
  127. package/dist/plugins/walrus/codegen.mjs.map +1 -1
  128. package/dist/plugins/walrus/index.mjs +1 -1
  129. package/dist/plugins/walrus/index.mjs.map +1 -1
  130. package/dist/substrate/manifest.d.mts +17 -22
  131. package/dist/substrate/manifest.mjs +4 -6
  132. package/dist/substrate/manifest.mjs.map +1 -1
  133. package/dist/substrate/plugin-ctx.d.mts +6 -6
  134. package/dist/substrate/plugin-ctx.mjs.map +1 -1
  135. package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
  136. package/dist/substrate/runtime/index.mjs +1 -0
  137. package/dist/substrate/runtime/lifecycle/file-watcher.d.mts +1 -0
  138. package/dist/substrate/runtime/lifecycle/file-watcher.mjs +54 -0
  139. package/dist/substrate/runtime/lifecycle/file-watcher.mjs.map +1 -0
  140. package/dist/substrate/runtime/lifecycle/index.mjs +1 -0
  141. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +76 -6
  142. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
  143. package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
  144. package/dist/substrate/runtime/paths.mjs +1 -1
  145. package/dist/substrate/runtime/supervisor/start-supervisor.mjs +2 -6
  146. package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
  147. package/dist/surfaces/cli/command-tree.mjs +6 -6
  148. package/dist/surfaces/cli/command-tree.mjs.map +1 -1
  149. package/dist/surfaces/cli/index.mjs +6 -4
  150. package/dist/surfaces/cli/index.mjs.map +1 -1
  151. package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
  152. package/package.json +1 -1
  153. package/dist/cli/wirings/dump-ids.mjs +0 -86
  154. package/dist/cli/wirings/dump-ids.mjs.map +0 -1
  155. package/dist/orchestrators/codegen/id-config.d.mts +0 -1
  156. package/dist/orchestrators/codegen/id-config.mjs +0 -107
  157. package/dist/orchestrators/codegen/id-config.mjs.map +0 -1
  158. package/dist/orchestrators/codegen/output-location.mjs +0 -32
  159. package/dist/orchestrators/codegen/output-location.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\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 values we return\n\t\t\t// (`[...LIT_DEDUPE]`, a freshly built array) are already mutable.\n\t\t\treadonly dedupe?: string[];\n\t\t};\n\t\treadonly optimizeDeps?: { readonly include: string[] };\n\t\t/** Build-time `define` injecting the on-chain ids as the\n\t\t * `__DEVSTACK_IDS__` global (the generated resolver reads it). */\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/** 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 a `<script type=\"module\">` importing the virtual module into\n\t * the dev page's HTML. DEV-only. The return shape mirrors a structural\n\t * subset of Vite's `IndexHtmlTransformResult` (mutable `tags`,\n\t * `injectTo` as the literal `'head'`) so the plugin stays assignable to\n\t * Vite's `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\tinjectTo: 'head';\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/** 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 as a build-time global. The generated\n\t\t\t// `config-runtime.ts` resolver reads `__DEVSTACK_IDS__`\n\t\t\t// synchronously and throws `DevstackConfigMissingError` when an id\n\t\t\t// is unresolved. `define` substitutes it identically in the dev\n\t\t\t// server and the prod build.\n\t\t\tconst injectedIds = resolveInjectedIds(env, root, command, options.ids);\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: [...LIT_DEDUPE],\n\t\t\t\t},\n\t\t\t\tdefine: {\n\t\t\t\t\t__DEVSTACK_IDS__: JSON.stringify(injectedIds ?? null),\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\ttransformIndexHtml: (html: string) => {\n\t\t\tif (!injectDevWallet || !isServe) return undefined;\n\t\t\treturn {\n\t\t\t\thtml,\n\t\t\t\ttags: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttag: 'script',\n\t\t\t\t\t\tattrs: { type: 'module', src: VIRTUAL_DEV_WALLET_SCRIPT_SRC },\n\t\t\t\t\t\tinjectTo: 'head',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\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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"}
@@ -65,14 +65,13 @@ const devstackVitestTestConfig = (options = {}) => {
65
65
  const isE2e = Boolean(options.autoBoot);
66
66
  const autoBootOverride = isE2e ? { globalSetup: [DEVSTACK_GLOBAL_SETUP_MODULE] } : {};
67
67
  return {
68
- include: isE2e ? ["src/**/*.e2e.{test,spec}.ts?(x)", "test/**/*.e2e.{test,spec}.ts?(x)"] : ["src/**/*.{test,spec}.ts?(x)", "test/**/*.{test,spec}.ts?(x)"],
68
+ include: isE2e ? ["tests/e2e/**/*.{test,spec}.ts?(x)"] : ["tests/unit/**/*.{test,spec}.ts?(x)"],
69
69
  exclude: [
70
- "e2e/**",
70
+ "tests/browser/**",
71
71
  "node_modules",
72
72
  "dist",
73
73
  ".turbo",
74
- "**/.devstack/**",
75
- ...isE2e ? [] : ["**/*.e2e.{test,spec}.ts?(x)"]
74
+ "**/.devstack/**"
76
75
  ],
77
76
  passWithNoTests: true,
78
77
  ...setupFiles.length > 0 ? { setupFiles } : {},
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../../../src/build-integrations/vitest/config.ts"],"sourcesContent":["// Vitest build-integration config helpers.\n//\n// Architecture (distilled/23-build-integrations.md § Per-integration\n// requirements → Vitest, § Outputs / capabilities, § Invariants):\n// - Canonical include/exclude: src globs in, e2e/dist/node_modules\n// out (`e2e/**` runs through the Playwright integration).\n// - `passWithNoTests: true` so codegen-derived stacks without unit\n// tests yet don't fail CI.\n// - `@effect/vitest` is declared as an optional peer — the preset\n// itself does not import it. Suites using `it.layer(stack.layer)`\n// bring it in directly.\n// - No-restart on harmless changes: watch ignores `<runtimeRoot>/**`\n// so the ~500ms manifest tick doesn't trigger a reload loop.\n//\n// Optional knobs:\n// - `testSetup: true | TestSetupOptions` wires the devstack\n// setup file (`./setup.ts`) automatically via `test.setupFiles`.\n// - `typecheck: true` opts INTO vitest's typecheck pass (off by\n// default — apps typically run `tsc` separately).\n// - `threads: 'single' | 'multi'` picks `pool: 'threads'` and toggles\n// `fileParallelism` (vitest 4 uses a top-level\n// `fileParallelism: false`) for stack-shared-state suites that\n// can't safely parallelize.\n// - Caller-provided `test` fields win over the preset's defaults.\n//\n// No I/O happens at config-build time (per architecture § Lifecycle\n// states: vitest preset call is synchronous + side-effect free).\n\nimport type { ViteUserConfig } from 'vitest/config';\n\nimport type { TestSetupOptions } from './setup.ts';\n\n// -----------------------------------------------------------------------------\n// Public options\n// -----------------------------------------------------------------------------\n\nexport interface DevstackVitestTestConfigOptions {\n\t/** Extra `test` fields. Caller-provided keys win over the preset's\n\t * defaults; the preset's include/exclude/passWithNoTests are the\n\t * start, the caller's fields are merged shallowly. */\n\treadonly test?: NonNullable<ViteUserConfig['test']>;\n\t/** Wire the devstack `beforeAll` / `afterAll` setup file. `true`\n\t * uses defaults; an options object configures the captured\n\t * fixture (e.g. `{ requireDevstack: true }`). Default: not wired. */\n\treadonly testSetup?: boolean | TestSetupOptions;\n\t/** Marks this as the E2E (full-stack) config: boots a dedicated `test`\n\t * stack for the run via vitest `globalSetup` (tearing it down on\n\t * completion), AND scopes the run to `*.e2e.{test,spec}` files only. A\n\t * plain `devstackVitestTestConfig()` is the UNIT config — it runs the\n\t * other suites and excludes the `*.e2e` ones. So `pnpm test` (unit) never\n\t * touches Docker, and `pnpm test:e2e` boots an isolated stack that runs\n\t * in parallel to a developer's `pnpm dev` without contending.\n\t * Default: not wired (unit config).\n\t *\n\t * The injected boot module reads its knobs from the environment\n\t * (`DEVSTACK_STACK`, `DEVSTACK_RUNTIME_ROOT`, `DEVSTACK_TEST_REUSE`).\n\t * For programmatic options (custom stack name, reuse, timeout), author a\n\t * one-line `globalSetup` module calling `devstackVitestGlobalSetup(opts)`\n\t * and pass its path via `test.globalSetup` instead. */\n\treadonly autoBoot?: boolean;\n\t/** Enable vitest's typecheck pass. Default `false` — most apps\n\t * run `tsc` outside vitest. */\n\treadonly typecheck?: boolean;\n\t/** Threading mode. `'single'` forces a single thread (needed when\n\t * tests share devstack state); `'multi'` keeps vitest's default\n\t * worker pool. Default: vitest default (`undefined`). */\n\treadonly threads?: 'single' | 'multi';\n\t/** Absolute path to a user-authored setup file. When set, it is\n\t * appended to `test.setupFiles` AFTER the devstack setup file (so\n\t * the user file sees the captured StackContext via\n\t * `getStackContext()`). */\n\treadonly setupFile?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Internals\n// -----------------------------------------------------------------------------\n\n/** Path to the bundled devstack setup file. The preset injects this\n * via `test.setupFiles` when `opts.testSetup` is truthy. The literal\n * string is the module's import specifier so vitest resolves it like\n * any other npm package; an apps-level resolver finds it via the\n * package's `./vitest/setup` subpath (added to package.json\n * `exports` alongside the integration). */\nconst DEVSTACK_SETUP_MODULE = '@mysten-incubation/devstack/vitest/setup';\n\n/** Path to the bundled devstack `globalSetup` module. Injected into\n * `test.globalSetup` when `opts.autoBoot` is set — it boots a `test`\n * stack before the run and tears it down after. Resolved like any npm\n * package via the `./vitest/global-setup` subpath in package.json. */\nconst DEVSTACK_GLOBAL_SETUP_MODULE = '@mysten-incubation/devstack/vitest/global-setup';\n\n/** Runtime-root prefix the watcher MUST ignore — per architecture §\n * Invariants → \"No-restart on harmless changes\". The 500ms manifest\n * tick would otherwise re-trigger reloads. */\nconst WATCH_IGNORED_PATTERNS = ['**/.devstack/**', '**/node_modules/**', '**/dist/**'];\n\n// -----------------------------------------------------------------------------\n// Public surface\n// -----------------------------------------------------------------------------\n\n/**\n * Build the devstack-owned part of Vitest's `test` block. Apps keep a\n * normal `defineConfig(...)` call and compose this value into it:\n *\n * import { defineConfig } from 'vitest/config';\n * import { devstackVitestServerConfig, devstackVitestTestConfig }\n * from '@mysten-incubation/devstack/vitest';\n *\n * export default defineConfig({\n * server: devstackVitestServerConfig(),\n * test: devstackVitestTestConfig(),\n * });\n *\n * For chain-mode integration tests against a real devstack, build a\n * devstack handle and pass `handle.layer` to `@effect/vitest`'s\n * `it.layer(...)` directly — here the test file owns its lifecycle.\n * Alternatively, `autoBoot: true` wires the `global-setup.ts` boot seam\n * that boots a dedicated stack for the `*.e2e` run and tears it down\n * after (so `pnpm test:e2e` is self-contained); a plain call boots\n * nothing and just builds the `test` block below.\n *\n * For suites that need a shared StackContext fixture (manifest read\n * once per file), opt into the setup file:\n *\n * test: devstackVitestTestConfig({\n * testSetup: { requireDevstack: true },\n * })\n *\n * The setup file's `beforeAll` populates a captured fixture readable\n * via `getStackContext()` from inside `it`/`test` bodies.\n */\nexport const devstackVitestTestConfig = (\n\toptions: DevstackVitestTestConfigOptions = {},\n): NonNullable<ViteUserConfig['test']> => {\n\tconst setupFiles: Array<string> = [];\n\tif (options.testSetup) {\n\t\tsetupFiles.push(DEVSTACK_SETUP_MODULE);\n\t}\n\tif (options.setupFile !== undefined) {\n\t\tsetupFiles.push(options.setupFile);\n\t}\n\n\t// Vitest 4 removed `poolOptions.threads.singleThread`. Top-level\n\t// `fileParallelism: false` is the replacement for single-threaded\n\t// runs (also pins `maxWorkers` to 1 per vitest's docs). Both\n\t// branches set `pool: 'threads'` explicitly so we don't depend on\n\t// vitest's default pool (which is `forks` in v4).\n\tconst threadOverride =\n\t\toptions.threads === 'single'\n\t\t\t? { pool: 'threads' as const, fileParallelism: false }\n\t\t\t: options.threads === 'multi'\n\t\t\t\t? { pool: 'threads' as const, fileParallelism: true }\n\t\t\t\t: {};\n\n\tconst typecheckOverride = options.typecheck === true ? { typecheck: { enabled: true } } : {};\n\n\t// `autoBoot` marks the E2E (full-stack) config: it both wires the boot\n\t// `globalSetup` AND scopes the run to `*.e2e.{test,spec}` files. A plain\n\t// `devstackVitestTestConfig()` is the UNIT config — it runs the other\n\t// suites and EXCLUDES the `*.e2e` ones (which need a booted stack). So\n\t// `pnpm test` (unit) never boots Docker, and `pnpm test:e2e` runs only\n\t// the stack-requiring suites against a freshly-booted stack.\n\tconst isE2e = Boolean(options.autoBoot);\n\tconst autoBootOverride = isE2e ? { globalSetup: [DEVSTACK_GLOBAL_SETUP_MODULE] } : {};\n\n\tconst include = isE2e\n\t\t? ['src/**/*.e2e.{test,spec}.ts?(x)', 'test/**/*.e2e.{test,spec}.ts?(x)']\n\t\t: ['src/**/*.{test,spec}.ts?(x)', 'test/**/*.{test,spec}.ts?(x)'];\n\tconst exclude = [\n\t\t// `e2e/**` keeps Playwright's browser specs out of the vitest run.\n\t\t'e2e/**',\n\t\t'node_modules',\n\t\t'dist',\n\t\t'.turbo',\n\t\t'**/.devstack/**',\n\t\t// Unit runs skip the full-stack `*.e2e` vitest suites (they boot a\n\t\t// stack); the autoBoot config opts INTO them via `include` above.\n\t\t...(isE2e ? [] : ['**/*.e2e.{test,spec}.ts?(x)']),\n\t];\n\n\treturn {\n\t\tinclude,\n\t\texclude,\n\t\tpassWithNoTests: true,\n\t\t...(setupFiles.length > 0 ? { setupFiles } : {}),\n\t\t...threadOverride,\n\t\t...typecheckOverride,\n\t\t...autoBootOverride,\n\t\t// Caller overrides last. Shallow merge.\n\t\t...options.test,\n\t};\n};\n\n/** Build the devstack-owned part of Vitest's Vite server config.\n * Callers that need additional server options should merge them in\n * their own `defineConfig(...)` file. */\nexport const devstackVitestServerConfig = (): NonNullable<ViteUserConfig['server']> => ({\n\twatch: {\n\t\t// vite's chokidar watcher honors `ignored` even in test mode.\n\t\tignored: WATCH_IGNORED_PATTERNS,\n\t},\n});\n\n// -----------------------------------------------------------------------------\n// Test-visible internals (do not re-export from the integration barrel)\n// -----------------------------------------------------------------------------\n\n/** @internal — used by `test/build-integrations/vitest/config.test.ts`\n * to assert the watcher ignored set without depending on a built\n * config snapshot. */\nexport const _internal = {\n\tDEVSTACK_SETUP_MODULE,\n\tDEVSTACK_GLOBAL_SETUP_MODULE,\n\tWATCH_IGNORED_PATTERNS,\n} as const;\n"],"mappings":";;;;;;;AAoFA,MAAM,wBAAwB;;;;;AAM9B,MAAM,+BAA+B;;;;AAKrC,MAAM,yBAAyB;CAAC;CAAmB;CAAsB;AAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCrF,MAAa,4BACZ,UAA2C,CAAC,MACH;CACzC,MAAM,aAA4B,CAAC;CACnC,IAAI,QAAQ,WACX,WAAW,KAAK,qBAAqB;CAEtC,IAAI,QAAQ,cAAc,KAAA,GACzB,WAAW,KAAK,QAAQ,SAAS;CAQlC,MAAM,iBACL,QAAQ,YAAY,WACjB;EAAE,MAAM;EAAoB,iBAAiB;CAAM,IACnD,QAAQ,YAAY,UACnB;EAAE,MAAM;EAAoB,iBAAiB;CAAK,IAClD,CAAC;CAEN,MAAM,oBAAoB,QAAQ,cAAc,OAAO,EAAE,WAAW,EAAE,SAAS,KAAK,EAAE,IAAI,CAAC;CAQ3F,MAAM,QAAQ,QAAQ,QAAQ,QAAQ;CACtC,MAAM,mBAAmB,QAAQ,EAAE,aAAa,CAAC,4BAA4B,EAAE,IAAI,CAAC;CAiBpF,OAAO;EACN,SAhBe,QACb,CAAC,mCAAmC,kCAAkC,IACtE,CAAC,+BAA+B,8BAA8B;EAehE,SAAA;GAZA;GACA;GACA;GACA;GACA;GAGA,GAAI,QAAQ,CAAC,IAAI,CAAC,6BAA6B;EAKzC;EACN,iBAAiB;EACjB,GAAI,WAAW,SAAS,IAAI,EAAE,WAAW,IAAI,CAAC;EAC9C,GAAG;EACH,GAAG;EACH,GAAG;EAEH,GAAG,QAAQ;CACZ;AACD;;;;AAKA,MAAa,oCAA2E,EACvF,OAAO,EAEN,SAAS,uBACV,EACD"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../../../src/build-integrations/vitest/config.ts"],"sourcesContent":["// Vitest build-integration config helpers.\n//\n// Architecture (distilled/23-build-integrations.md § Per-integration\n// requirements → Vitest, § Outputs / capabilities, § Invariants):\n// - Canonical include/exclude by DIRECTORY: `tests/unit/**` for the\n// unit config, `tests/e2e/**` for the autoBoot (full-stack) config;\n// `tests/browser/**` (Playwright specs) and dist/node_modules out.\n// - `passWithNoTests: true` so codegen-derived stacks without unit\n// tests yet don't fail CI.\n// - `@effect/vitest` is declared as an optional peer — the preset\n// itself does not import it. Suites using `it.layer(stack.layer)`\n// bring it in directly.\n// - No-restart on harmless changes: watch ignores `<runtimeRoot>/**`\n// so the ~500ms manifest tick doesn't trigger a reload loop.\n//\n// Optional knobs:\n// - `testSetup: true | TestSetupOptions` wires the devstack\n// setup file (`./setup.ts`) automatically via `test.setupFiles`.\n// - `typecheck: true` opts INTO vitest's typecheck pass (off by\n// default — apps typically run `tsc` separately).\n// - `threads: 'single' | 'multi'` picks `pool: 'threads'` and toggles\n// `fileParallelism` (vitest 4 uses a top-level\n// `fileParallelism: false`) for stack-shared-state suites that\n// can't safely parallelize.\n// - Caller-provided `test` fields win over the preset's defaults.\n//\n// No I/O happens at config-build time (per architecture § Lifecycle\n// states: vitest preset call is synchronous + side-effect free).\n\nimport type { ViteUserConfig } from 'vitest/config';\n\nimport type { TestSetupOptions } from './setup.ts';\n\n// -----------------------------------------------------------------------------\n// Public options\n// -----------------------------------------------------------------------------\n\nexport interface DevstackVitestTestConfigOptions {\n\t/** Extra `test` fields. Caller-provided keys win over the preset's\n\t * defaults; the preset's include/exclude/passWithNoTests are the\n\t * start, the caller's fields are merged shallowly. */\n\treadonly test?: NonNullable<ViteUserConfig['test']>;\n\t/** Wire the devstack `beforeAll` / `afterAll` setup file. `true`\n\t * uses defaults; an options object configures the captured\n\t * fixture (e.g. `{ requireDevstack: true }`). Default: not wired. */\n\treadonly testSetup?: boolean | TestSetupOptions;\n\t/** Marks this as the E2E (full-stack) config: boots a dedicated `test`\n\t * stack for the run via vitest `globalSetup` (tearing it down on\n\t * completion), AND scopes the run to `*.e2e.{test,spec}` files only. A\n\t * plain `devstackVitestTestConfig()` is the UNIT config — it runs the\n\t * other suites and excludes the `*.e2e` ones. So `pnpm test` (unit) never\n\t * touches Docker, and `pnpm test:e2e` boots an isolated stack that runs\n\t * in parallel to a developer's `pnpm dev` without contending.\n\t * Default: not wired (unit config).\n\t *\n\t * The injected boot module reads its knobs from the environment\n\t * (`DEVSTACK_STACK`, `DEVSTACK_RUNTIME_ROOT`, `DEVSTACK_TEST_REUSE`).\n\t * For programmatic options (custom stack name, reuse, timeout), author a\n\t * one-line `globalSetup` module calling `devstackVitestGlobalSetup(opts)`\n\t * and pass its path via `test.globalSetup` instead. */\n\treadonly autoBoot?: boolean;\n\t/** Enable vitest's typecheck pass. Default `false` — most apps\n\t * run `tsc` outside vitest. */\n\treadonly typecheck?: boolean;\n\t/** Threading mode. `'single'` forces a single thread (needed when\n\t * tests share devstack state); `'multi'` keeps vitest's default\n\t * worker pool. Default: vitest default (`undefined`). */\n\treadonly threads?: 'single' | 'multi';\n\t/** Absolute path to a user-authored setup file. When set, it is\n\t * appended to `test.setupFiles` AFTER the devstack setup file (so\n\t * the user file sees the captured StackContext via\n\t * `getStackContext()`). */\n\treadonly setupFile?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Internals\n// -----------------------------------------------------------------------------\n\n/** Path to the bundled devstack setup file. The preset injects this\n * via `test.setupFiles` when `opts.testSetup` is truthy. The literal\n * string is the module's import specifier so vitest resolves it like\n * any other npm package; an apps-level resolver finds it via the\n * package's `./vitest/setup` subpath (added to package.json\n * `exports` alongside the integration). */\nconst DEVSTACK_SETUP_MODULE = '@mysten-incubation/devstack/vitest/setup';\n\n/** Path to the bundled devstack `globalSetup` module. Injected into\n * `test.globalSetup` when `opts.autoBoot` is set — it boots a `test`\n * stack before the run and tears it down after. Resolved like any npm\n * package via the `./vitest/global-setup` subpath in package.json. */\nconst DEVSTACK_GLOBAL_SETUP_MODULE = '@mysten-incubation/devstack/vitest/global-setup';\n\n/** Runtime-root prefix the watcher MUST ignore — per architecture §\n * Invariants → \"No-restart on harmless changes\". The 500ms manifest\n * tick would otherwise re-trigger reloads. */\nconst WATCH_IGNORED_PATTERNS = ['**/.devstack/**', '**/node_modules/**', '**/dist/**'];\n\n// -----------------------------------------------------------------------------\n// Public surface\n// -----------------------------------------------------------------------------\n\n/**\n * Build the devstack-owned part of Vitest's `test` block. Apps keep a\n * normal `defineConfig(...)` call and compose this value into it:\n *\n * import { defineConfig } from 'vitest/config';\n * import { devstackVitestServerConfig, devstackVitestTestConfig }\n * from '@mysten-incubation/devstack/vitest';\n *\n * export default defineConfig({\n * server: devstackVitestServerConfig(),\n * test: devstackVitestTestConfig(),\n * });\n *\n * For chain-mode integration tests against a real devstack, build a\n * devstack handle and pass `handle.layer` to `@effect/vitest`'s\n * `it.layer(...)` directly — here the test file owns its lifecycle.\n * Alternatively, `autoBoot: true` wires the `global-setup.ts` boot seam\n * that boots a dedicated stack for the `*.e2e` run and tears it down\n * after (so `pnpm test:e2e` is self-contained); a plain call boots\n * nothing and just builds the `test` block below.\n *\n * For suites that need a shared StackContext fixture (manifest read\n * once per file), opt into the setup file:\n *\n * test: devstackVitestTestConfig({\n * testSetup: { requireDevstack: true },\n * })\n *\n * The setup file's `beforeAll` populates a captured fixture readable\n * via `getStackContext()` from inside `it`/`test` bodies.\n */\nexport const devstackVitestTestConfig = (\n\toptions: DevstackVitestTestConfigOptions = {},\n): NonNullable<ViteUserConfig['test']> => {\n\tconst setupFiles: Array<string> = [];\n\tif (options.testSetup) {\n\t\tsetupFiles.push(DEVSTACK_SETUP_MODULE);\n\t}\n\tif (options.setupFile !== undefined) {\n\t\tsetupFiles.push(options.setupFile);\n\t}\n\n\t// Vitest 4 removed `poolOptions.threads.singleThread`. Top-level\n\t// `fileParallelism: false` is the replacement for single-threaded\n\t// runs (also pins `maxWorkers` to 1 per vitest's docs). Both\n\t// branches set `pool: 'threads'` explicitly so we don't depend on\n\t// vitest's default pool (which is `forks` in v4).\n\tconst threadOverride =\n\t\toptions.threads === 'single'\n\t\t\t? { pool: 'threads' as const, fileParallelism: false }\n\t\t\t: options.threads === 'multi'\n\t\t\t\t? { pool: 'threads' as const, fileParallelism: true }\n\t\t\t\t: {};\n\n\tconst typecheckOverride = options.typecheck === true ? { typecheck: { enabled: true } } : {};\n\n\t// `autoBoot` marks the E2E (full-stack) config: it both wires the boot\n\t// `globalSetup` AND scopes the run to the `tests/e2e/` directory. A plain\n\t// `devstackVitestTestConfig()` is the UNIT config — it scopes the run to\n\t// `tests/unit/`. Directory layout (not a filename infix) separates the two\n\t// suites: `tests/unit/` (fast, no Docker), `tests/e2e/` (boots a stack),\n\t// and `tests/browser/` (Playwright browser specs, run by the Playwright\n\t// integration — never by vitest). So `pnpm test` never touches Docker, and\n\t// `pnpm test:e2e` boots an isolated stack for the `tests/e2e/` suites only.\n\tconst isE2e = Boolean(options.autoBoot);\n\tconst autoBootOverride = isE2e ? { globalSetup: [DEVSTACK_GLOBAL_SETUP_MODULE] } : {};\n\n\tconst include = isE2e\n\t\t? ['tests/e2e/**/*.{test,spec}.ts?(x)']\n\t\t: ['tests/unit/**/*.{test,spec}.ts?(x)'];\n\tconst exclude = [\n\t\t// `tests/browser/**` are Playwright browser specs — run by the\n\t\t// Playwright integration, never the vitest run.\n\t\t'tests/browser/**',\n\t\t'node_modules',\n\t\t'dist',\n\t\t'.turbo',\n\t\t'**/.devstack/**',\n\t];\n\n\treturn {\n\t\tinclude,\n\t\texclude,\n\t\tpassWithNoTests: true,\n\t\t...(setupFiles.length > 0 ? { setupFiles } : {}),\n\t\t...threadOverride,\n\t\t...typecheckOverride,\n\t\t...autoBootOverride,\n\t\t// Caller overrides last. Shallow merge.\n\t\t...options.test,\n\t};\n};\n\n/** Build the devstack-owned part of Vitest's Vite server config.\n * Callers that need additional server options should merge them in\n * their own `defineConfig(...)` file. */\nexport const devstackVitestServerConfig = (): NonNullable<ViteUserConfig['server']> => ({\n\twatch: {\n\t\t// vite's chokidar watcher honors `ignored` even in test mode.\n\t\tignored: WATCH_IGNORED_PATTERNS,\n\t},\n});\n\n// -----------------------------------------------------------------------------\n// Test-visible internals (do not re-export from the integration barrel)\n// -----------------------------------------------------------------------------\n\n/** @internal — used by `test/build-integrations/vitest/config.test.ts`\n * to assert the watcher ignored set without depending on a built\n * config snapshot. */\nexport const _internal = {\n\tDEVSTACK_SETUP_MODULE,\n\tDEVSTACK_GLOBAL_SETUP_MODULE,\n\tWATCH_IGNORED_PATTERNS,\n} as const;\n"],"mappings":";;;;;;;AAqFA,MAAM,wBAAwB;;;;;AAM9B,MAAM,+BAA+B;;;;AAKrC,MAAM,yBAAyB;CAAC;CAAmB;CAAsB;AAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCrF,MAAa,4BACZ,UAA2C,CAAC,MACH;CACzC,MAAM,aAA4B,CAAC;CACnC,IAAI,QAAQ,WACX,WAAW,KAAK,qBAAqB;CAEtC,IAAI,QAAQ,cAAc,KAAA,GACzB,WAAW,KAAK,QAAQ,SAAS;CAQlC,MAAM,iBACL,QAAQ,YAAY,WACjB;EAAE,MAAM;EAAoB,iBAAiB;CAAM,IACnD,QAAQ,YAAY,UACnB;EAAE,MAAM;EAAoB,iBAAiB;CAAK,IAClD,CAAC;CAEN,MAAM,oBAAoB,QAAQ,cAAc,OAAO,EAAE,WAAW,EAAE,SAAS,KAAK,EAAE,IAAI,CAAC;CAU3F,MAAM,QAAQ,QAAQ,QAAQ,QAAQ;CACtC,MAAM,mBAAmB,QAAQ,EAAE,aAAa,CAAC,4BAA4B,EAAE,IAAI,CAAC;CAepF,OAAO;EACN,SAde,QACb,CAAC,mCAAmC,IACpC,CAAC,oCAAoC;EAavC,SAAA;GATA;GACA;GACA;GACA;GACA;EAKM;EACN,iBAAiB;EACjB,GAAI,WAAW,SAAS,IAAI,EAAE,WAAW,IAAI,CAAC;EAC9C,GAAG;EACH,GAAG;EACH,GAAG;EAEH,GAAG,QAAQ;CACZ;AACD;;;;AAKA,MAAa,oCAA2E,EACvF,OAAO,EAEN,SAAS,uBACV,EACD"}
@@ -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.idsFile` from its
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
- * `__DEVSTACK_IDS__` Vite `define`, but under vitest the Vite config (and
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.DEVSTACK_IDS_FILE` at id-access time instead — we point it at
79
- * the freshly-booted stack's live id-config file here. Mirrors the Vite
80
- * plugin's `readCodegenField`; degrades to `null` on any miss. */
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.idsFile;
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
- * (`DEVSTACK_IDS_FILE` — its Node fallback when the Vite `define` is null). */
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.DEVSTACK_IDS_FILE = idsFile;
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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"}
@@ -56,7 +56,12 @@ const dockerProbe = {
56
56
  return okOutcome(`server ${result.out}`);
57
57
  })
58
58
  };
59
- /** Probe: required CLIs on PATH (just sui-cli for fork-aware flows). */
59
+ /** Probe: the `sui` CLI on PATH. Needed for `devstack codegen` (the
60
+ * `sui move summary` step has no Docker fallback) and for live/fork
61
+ * modes. Kept a non-required WARN, not a hard failure: a stack-free
62
+ * `pnpm build` works without `sui` via the committed (pre-baked)
63
+ * `src/generated` tree, so a missing `sui` shouldn't fail `doctor`
64
+ * outright — it only blocks codegen + live/fork. */
60
65
  const suiCliProbe = {
61
66
  name: "sui-cli",
62
67
  description: "`sui` CLI on PATH",
@@ -65,7 +70,7 @@ const suiCliProbe = {
65
70
  const result = captureCommand("sui", ["--version"]);
66
71
  return result.ok ? okOutcome(result.out) : {
67
72
  status: "warn",
68
- detail: "sui CLI not on PATH (only needed for live/fork modes)"
73
+ detail: "sui CLI not on PATH (needed for `devstack codegen` and live/fork modes)"
69
74
  };
70
75
  })
71
76
  };
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-probes.mjs","names":[],"sources":["../../src/cli/doctor-probes.ts"],"sourcesContent":["// L4-adjacent CLI infrastructure: doctor probe definitions.\n//\n// Per STYLE_GUIDE §7: `cli/*.ts` modules sit alongside the bin entry\n// (`cli/main.ts`) and may import L3 orchestrator + L2 plugin barrels —\n// they are NOT L4 surfaces proper. The router-profile probe needs\n// both the L3 router orchestrator helpers (parsing dispatch files,\n// matching profile labels, sorting entrypoint ports) and the built-in\n// L2 router entrypoint composition; that wiring lives here.\n//\n// The probes themselves are read-only diagnostics: Docker reachable,\n// `sui` CLI on PATH, state-dir writable, router profile state +\n// dispatch leases + entrypoint listeners, orphan cross-process locks,\n// fork-cache health. Each returns a typed `ProbeOutcome` and never\n// throws. `required: true` means a `fail | unavailable` projects to\n// `CliUnavailableError` and exits 69; non-required probes are\n// informational.\n\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { createConnection } from 'node:net';\nimport { join } from 'node:path';\n\nimport { Effect } from 'effect';\n\nimport {\n\tdispatchFileIdFromFilename,\n\tmakeDefaultRouterProfile,\n\tparseDispatchRouteFile,\n\trouterProfileLabelsMatch,\n\tuniqueSortedEntrypointPorts,\n\ttype Entrypoint,\n\ttype RouterProfile,\n} from '../orchestrators/router/index.ts';\nimport { BUILT_IN_ENTRYPOINTS } from '../plugins/router-entrypoints.ts';\nimport { parseJsonTextSync } from '../substrate/runtime/runtime-decode.ts';\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\treadRoster,\n} from '../substrate/runtime/cross-process/index.ts';\nimport type { RosterHolder } from '../substrate/cross-process.ts';\nimport type { Probe, ProbeOutcome } from '../surfaces/cli/commands/doctor.ts';\n\nconst okOutcome = (detail?: string): ProbeOutcome =>\n\tdetail !== undefined ? { status: 'ok', detail } : { status: 'ok' };\n\ntype CommandResult = { ok: true; out: string } | { ok: false; err: string };\n\nconst captureCommand = (\n\tcmd: string,\n\targs: ReadonlyArray<string>,\n\ttimeoutMs = 3000,\n): CommandResult => {\n\ttry {\n\t\tconst out = execFileSync(cmd, args as string[], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: timeoutMs,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t});\n\t\treturn { ok: true, out: out.trim() };\n\t} catch (err) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terr: err instanceof Error ? err.message : String(err),\n\t\t};\n\t}\n};\n\n/** Probe: `docker version` resolves. */\nexport const dockerProbe: Probe = {\n\tname: 'docker',\n\tdescription: 'Docker daemon reachable',\n\trequired: true,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst result = captureCommand('docker', ['version', '--format', '{{.Server.Version}}']);\n\t\t\tif (!result.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\tdetail: `docker version failed: ${result.err}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn okOutcome(`server ${result.out}`);\n\t\t}),\n};\n\n/** Probe: required CLIs on PATH (just sui-cli for fork-aware flows). */\nexport const suiCliProbe: Probe = {\n\tname: 'sui-cli',\n\tdescription: '`sui` CLI on PATH',\n\trequired: false,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst result = captureCommand('sui', ['--version']);\n\t\t\treturn result.ok\n\t\t\t\t? okOutcome(result.out)\n\t\t\t\t: {\n\t\t\t\t\t\tstatus: 'warn',\n\t\t\t\t\t\tdetail: 'sui CLI not on PATH (only needed for live/fork modes)',\n\t\t\t\t\t};\n\t\t}),\n};\n\n/** Probe: a TCP port is FREE on localhost (resolves `true` when nothing\n * is listening — connection refused/timeout — and `false` when a\n * listener answers). Backs `routerProfileProbe`'s entrypoint-listener\n * check. */\nconst probePortFree = (port: number, timeoutMs = 500): Promise<boolean> =>\n\tnew Promise((resolve) => {\n\t\tconst socket = createConnection({ host: '127.0.0.1', port });\n\t\tconst finish = (free: boolean): void => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(free);\n\t\t};\n\t\tconst timer = setTimeout(() => finish(true), timeoutMs);\n\t\tsocket.on('connect', () => {\n\t\t\tclearTimeout(timer);\n\t\t\tfinish(false);\n\t\t});\n\t\tsocket.on('error', () => {\n\t\t\tclearTimeout(timer);\n\t\t\tfinish(true);\n\t\t});\n\t});\n\nexport type DoctorCommandRunner = typeof captureCommand;\nexport type PortAvailabilityProbe = typeof probePortFree;\n\nexport interface RouterProfileProbeOptions {\n\treadonly profile?: RouterProfile;\n\treadonly entrypoints?: ReadonlyArray<Entrypoint>;\n\treadonly command?: DoctorCommandRunner;\n\treadonly probePort?: PortAvailabilityProbe;\n}\n\ninterface RouterDispatchScan {\n\treadonly status: 'absent' | 'ok' | 'not-directory' | 'unreadable';\n\treadonly files: number;\n\treadonly liveRoutes: number;\n\treadonly staleRoutes: number;\n\treadonly unknownOwnerRoutes: number;\n\treadonly corruptRouteFiles: number;\n\treadonly diagnostics: number;\n\treadonly safeToPrune: boolean;\n}\n\ninterface RouterContainerStatus {\n\treadonly status: 'absent' | 'present' | 'invalid' | 'unavailable';\n\treadonly detail?: string;\n\treadonly running?: boolean;\n\treadonly labelsMatch?: boolean;\n\treadonly attachedToNetwork?: boolean;\n\treadonly publishedPorts?: ReadonlyArray<number>;\n}\n\ninterface RouterNetworkStatus {\n\treadonly status: 'absent' | 'present' | 'invalid' | 'unavailable';\n\treadonly detail?: string;\n\treadonly id?: string;\n}\n\nconst unknownRecord = (value: unknown): Record<string, unknown> | null =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: null;\n\nconst stringRecord = (value: unknown): Record<string, string> => {\n\tconst rec = unknownRecord(value);\n\tif (rec === null) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, raw] of Object.entries(rec)) {\n\t\tif (typeof raw === 'string') out[key] = raw;\n\t}\n\treturn out;\n};\n\nconst fieldRecord = (\n\trecord: Record<string, unknown>,\n\tkey: string,\n): Record<string, unknown> | null => unknownRecord(record[key]);\n\nconst fieldString = (record: Record<string, unknown>, key: string): string | null =>\n\ttypeof record[key] === 'string' ? (record[key] as string) : null;\n\nconst fieldBoolean = (record: Record<string, unknown>, key: string): boolean | null =>\n\ttypeof record[key] === 'boolean' ? (record[key] as boolean) : null;\n\nconst parseDockerInspectFirst = (out: string): Record<string, unknown> | null => {\n\ttry {\n\t\tconst parsed = parseJsonTextSync(out, {\n\t\t\tsource: 'docker inspect',\n\t\t\tmkError: (issue) => issue,\n\t\t});\n\t\tif (Array.isArray(parsed)) return unknownRecord(parsed[0]);\n\t\treturn unknownRecord(parsed);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerObjectIsAbsent = (err: string): boolean =>\n\t/No such (object|container|network)|not found|No such/i.test(err);\n\nconst uniqueNumbers = (values: ReadonlyArray<number>): ReadonlyArray<number> =>\n\t[...new Set(values)].sort((a, b) => a - b);\n\nconst inspectRouterContainer = (\n\tprofile: RouterProfile,\n\tcommand: DoctorCommandRunner,\n): RouterContainerStatus => {\n\tconst result = command('docker', ['container', 'inspect', profile.containerName]);\n\tif (!result.ok) {\n\t\treturn dockerObjectIsAbsent(result.err)\n\t\t\t? { status: 'absent' }\n\t\t\t: { status: 'unavailable', detail: result.err };\n\t}\n\tconst doc = parseDockerInspectFirst(result.out);\n\tif (doc === null) return { status: 'invalid', detail: 'container inspect JSON was invalid' };\n\tconst state = fieldRecord(doc, 'State') ?? {};\n\tconst config = fieldRecord(doc, 'Config') ?? {};\n\tconst networkSettings = fieldRecord(doc, 'NetworkSettings') ?? {};\n\tconst networks = fieldRecord(networkSettings, 'Networks') ?? {};\n\tconst ports = fieldRecord(networkSettings, 'Ports') ?? {};\n\tconst publishedPorts: Array<number> = [];\n\tfor (const bindings of Object.values(ports)) {\n\t\tif (!Array.isArray(bindings)) continue;\n\t\tfor (const binding of bindings) {\n\t\t\tconst rec = unknownRecord(binding);\n\t\t\tif (rec === null) continue;\n\t\t\tconst rawPort = fieldString(rec, 'HostPort');\n\t\t\tif (rawPort !== null && /^\\d+$/.test(rawPort)) {\n\t\t\t\tpublishedPorts.push(Number.parseInt(rawPort, 10));\n\t\t\t}\n\t\t}\n\t}\n\tconst labels = stringRecord(config.Labels);\n\treturn {\n\t\tstatus: 'present',\n\t\trunning: fieldBoolean(state, 'Running') === true,\n\t\tlabelsMatch: routerProfileLabelsMatch(labels, profile),\n\t\tattachedToNetwork: Object.keys(networks).includes(profile.networkName),\n\t\tpublishedPorts: uniqueNumbers(publishedPorts),\n\t};\n};\n\nconst inspectRouterNetwork = (\n\tprofile: RouterProfile,\n\tcommand: DoctorCommandRunner,\n): RouterNetworkStatus => {\n\tconst result = command('docker', ['network', 'inspect', profile.networkName]);\n\tif (!result.ok) {\n\t\treturn dockerObjectIsAbsent(result.err)\n\t\t\t? { status: 'absent' }\n\t\t\t: { status: 'unavailable', detail: result.err };\n\t}\n\tconst doc = parseDockerInspectFirst(result.out);\n\tif (doc === null) return { status: 'invalid', detail: 'network inspect JSON was invalid' };\n\tconst id = fieldString(doc, 'Id');\n\treturn id === null ? { status: 'present' } : { status: 'present', id };\n};\n\nconst inspectRouterDispatch = (profile: RouterProfile): Effect.Effect<RouterDispatchScan> =>\n\t// Yield a fresh `LivenessProbeScope` so repeated lease owners across\n\t// many dispatch route files fork the OS probe once per pid.\n\tEffect.gen(function* () {\n\t\tif (!existsSync(profile.dispatchDir)) {\n\t\t\treturn {\n\t\t\t\tstatus: 'absent' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 0,\n\t\t\t\tcorruptRouteFiles: 0,\n\t\t\t\tdiagnostics: 0,\n\t\t\t\tsafeToPrune: true,\n\t\t\t};\n\t\t}\n\t\ttry {\n\t\t\tif (!statSync(profile.dispatchDir).isDirectory()) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'not-directory' as const,\n\t\t\t\t\tfiles: 0,\n\t\t\t\t\tliveRoutes: 0,\n\t\t\t\t\tstaleRoutes: 0,\n\t\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\t\tdiagnostics: 1,\n\t\t\t\t\tsafeToPrune: false,\n\t\t\t\t};\n\t\t\t}\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tstatus: 'unreadable' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\tdiagnostics: 1,\n\t\t\t\tsafeToPrune: false,\n\t\t\t};\n\t\t}\n\t\tlet files: ReadonlyArray<string>;\n\t\ttry {\n\t\t\tfiles = readdirSync(profile.dispatchDir);\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tstatus: 'unreadable' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\tdiagnostics: 1,\n\t\t\t\tsafeToPrune: false,\n\t\t\t};\n\t\t}\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tlet routeFiles = 0;\n\t\tlet liveRoutes = 0;\n\t\tlet staleRoutes = 0;\n\t\tlet unknownOwnerRoutes = 0;\n\t\tlet corruptRouteFiles = 0;\n\t\tlet diagnostics = 0;\n\t\tfor (const filename of files) {\n\t\t\tconst dispatchFileId = dispatchFileIdFromFilename(filename);\n\t\t\tif (dispatchFileId === null) continue;\n\t\t\trouteFiles += 1;\n\t\t\tlet body: string;\n\t\t\ttry {\n\t\t\t\tbody = readFileSync(join(profile.dispatchDir, filename), 'utf8');\n\t\t\t} catch {\n\t\t\t\tcorruptRouteFiles += 1;\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst parsed = parseDispatchRouteFile(body, dispatchFileId);\n\t\t\tdiagnostics += parsed.diagnostics.length;\n\t\t\tif (parsed._tag === 'invalid') {\n\t\t\t\tcorruptRouteFiles += 1;\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (parsed.route.lease === null) {\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst leaseStatus = yield* probe\n\t\t\t\t.probeHolderLiveness(parsed.route.lease.owner)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\tif (leaseStatus === 'dead') staleRoutes += 1;\n\t\t\telse liveRoutes += 1;\n\t\t}\n\t\tconst safeToPrune = liveRoutes === 0 && unknownOwnerRoutes === 0 && corruptRouteFiles === 0;\n\t\treturn {\n\t\t\tstatus: 'ok' as const,\n\t\t\tfiles: routeFiles,\n\t\t\tliveRoutes,\n\t\t\tstaleRoutes,\n\t\t\tunknownOwnerRoutes,\n\t\t\tcorruptRouteFiles,\n\t\t\tdiagnostics,\n\t\t\tsafeToPrune,\n\t\t};\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n\nconst inspectRouterStateDir = (profile: RouterProfile): 'absent' | 'present' | 'not-directory' => {\n\tif (!existsSync(profile.stateDir)) return 'absent';\n\ttry {\n\t\treturn statSync(profile.stateDir).isDirectory() ? 'present' : 'not-directory';\n\t} catch {\n\t\treturn 'not-directory';\n\t}\n};\n\nconst summarizeRouterContainer = (container: RouterContainerStatus): string => {\n\tif (container.status !== 'present') return container.status;\n\treturn [\n\t\tcontainer.running === true ? 'running' : 'stopped',\n\t\t`labels=${container.labelsMatch === true ? 'ok' : 'mismatch'}`,\n\t\t`network=${container.attachedToNetwork === true ? 'attached' : 'missing'}`,\n\t\t`ports=${(container.publishedPorts ?? []).join(',') || 'none'}`,\n\t].join('/');\n};\n\nexport const routerProfileProbe = (options: RouterProfileProbeOptions = {}): Probe => ({\n\tname: 'router-profile',\n\tdescription: 'router profile state, dispatch leases, and entrypoint listeners',\n\trequired: false,\n\trun: () =>\n\t\tEffect.gen(function* () {\n\t\t\tconst profile = options.profile ?? makeDefaultRouterProfile();\n\t\t\tconst entrypoints = options.entrypoints ?? BUILT_IN_ENTRYPOINTS;\n\t\t\tconst command = options.command ?? captureCommand;\n\t\t\tconst probePort = options.probePort ?? probePortFree;\n\t\t\tconst ports = uniqueSortedEntrypointPorts(entrypoints);\n\t\t\tconst stateDirStatus = inspectRouterStateDir(profile);\n\t\t\tconst dispatch = yield* inspectRouterDispatch(profile);\n\t\t\tconst container = inspectRouterContainer(profile, command);\n\t\t\tconst network = inspectRouterNetwork(profile, command);\n\t\t\tconst listenerResults = yield* Effect.tryPromise({\n\t\t\t\ttry: () =>\n\t\t\t\t\tPromise.all(\n\t\t\t\t\t\tports.map(async (port) => ({\n\t\t\t\t\t\t\tport,\n\t\t\t\t\t\t\tfree: await probePort(port),\n\t\t\t\t\t\t})),\n\t\t\t\t\t),\n\t\t\t\tcatch: () => 'router entrypoint listener probe failed',\n\t\t\t}).pipe(Effect.catch(() => Effect.succeed(ports.map((port) => ({ port, free: true })))));\n\t\t\tconst freePorts = listenerResults.filter((r) => r.free).map((r) => r.port);\n\t\t\tconst occupiedPorts = listenerResults.filter((r) => !r.free).map((r) => r.port);\n\t\t\tconst protectedRoutes =\n\t\t\t\tdispatch.liveRoutes + dispatch.unknownOwnerRoutes + dispatch.corruptRouteFiles;\n\t\t\tconst problems: string[] = [];\n\t\t\tif (stateDirStatus === 'not-directory')\n\t\t\t\tproblems.push(`${profile.stateDir} is not a directory`);\n\t\t\tif (dispatch.status === 'not-directory' || dispatch.status === 'unreadable') {\n\t\t\t\tproblems.push(`dispatch dir is ${dispatch.status}`);\n\t\t\t}\n\t\t\tif (dispatch.unknownOwnerRoutes > 0 || dispatch.corruptRouteFiles > 0) {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`unknown/corrupt dispatch leases=${dispatch.unknownOwnerRoutes + dispatch.corruptRouteFiles}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (dispatch.staleRoutes > 0) problems.push(`stale dispatch leases=${dispatch.staleRoutes}`);\n\t\t\tif (protectedRoutes > 0 && (container.status !== 'present' || container.running !== true)) {\n\t\t\t\tproblems.push('protected dispatch leases exist without a running router container');\n\t\t\t}\n\t\t\tif (container.status === 'invalid' || container.status === 'unavailable') {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`router container inspect ${container.status}: ${container.detail ?? 'unknown'}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (container.status === 'present') {\n\t\t\t\tif (container.labelsMatch !== true)\n\t\t\t\t\tproblems.push('router container labels do not match profile');\n\t\t\t\tif (container.attachedToNetwork !== true) {\n\t\t\t\t\tproblems.push('router container is not attached to profile network');\n\t\t\t\t}\n\t\t\t\tif (container.running === true && freePorts.length > 0) {\n\t\t\t\t\tproblems.push(`router entrypoint listeners missing: ${freePorts.join(', ')}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (container.status !== 'present' && occupiedPorts.length > 0) {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`router entrypoint ports already in use without profile container: ${occupiedPorts.join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (network.status === 'invalid' || network.status === 'unavailable') {\n\t\t\t\tproblems.push(`router network inspect ${network.status}: ${network.detail ?? 'unknown'}`);\n\t\t\t}\n\t\t\tconst detail = [\n\t\t\t\t`profile=${profile.id}`,\n\t\t\t\t`state=${stateDirStatus}`,\n\t\t\t\t`dispatch=${dispatch.status}:files=${dispatch.files},live=${dispatch.liveRoutes},stale=${dispatch.staleRoutes},unknown=${dispatch.unknownOwnerRoutes},corrupt=${dispatch.corruptRouteFiles},diagnostics=${dispatch.diagnostics},pruneSafe=${dispatch.safeToPrune ? 'yes' : 'no'}`,\n\t\t\t\t`container=${summarizeRouterContainer(container)}`,\n\t\t\t\t`network=${network.status}`,\n\t\t\t\t`entrypoints=${ports.join(', ') || 'none'}`,\n\t\t\t].join('; ');\n\t\t\tif (problems.length === 0) return okOutcome(detail);\n\t\t\treturn {\n\t\t\t\tstatus: 'warn' as const,\n\t\t\t\tdetail: `${detail}; ${problems.join('; ')}`,\n\t\t\t};\n\t\t}),\n});\n\n/** Probe: list orphaned stack locks under `<runtimeRoot>/<app>/`. A\n * stack lock whose owner PID is dead is a stale-lock candidate that\n * `--clean-locks` would reclaim. */\nexport const locksProbe = (appRoot: string): Probe => ({\n\tname: 'locks',\n\tdescription: 'stale cross-process locks',\n\trequired: false,\n\trun: () =>\n\t\tEffect.gen(function* () {\n\t\t\tif (!existsSync(appRoot)) return okOutcome('(no app root yet)');\n\t\t\t// Yield a fresh `LivenessProbeScope` so a single pid that\n\t\t\t// shows up in multiple stack rosters under this app root is\n\t\t\t// probed AT MOST once across the full lock scan.\n\t\t\tconst probe = yield* LivenessProbeScope;\n\t\t\tlet orphans = 0;\n\t\t\tlet totalLive = 0;\n\t\t\ttry {\n\t\t\t\tfor (const entry of readdirSync(appRoot)) {\n\t\t\t\t\tif (entry.startsWith('.')) continue;\n\t\t\t\t\tconst stackRoot = join(appRoot, entry);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (!statSync(stackRoot).isDirectory()) continue;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst rosterFile = join(stackRoot, 'roster.json');\n\t\t\t\t\tif (!existsSync(rosterFile)) continue;\n\t\t\t\t\tconst doc = yield* readRoster(rosterFile).pipe(\n\t\t\t\t\t\tEffect.catch(() =>\n\t\t\t\t\t\t\tEffect.succeed({ version: 1 as const, holders: [] as RosterHolder[] }),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tfor (const holder of doc.holders) {\n\t\t\t\t\t\tconst liveness = yield* probe\n\t\t\t\t\t\t\t.probeHolderLiveness(holder)\n\t\t\t\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\t\t\t\tif (liveness === 'alive') totalLive += 1;\n\t\t\t\t\t\telse orphans += 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (cause) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'warn' as const,\n\t\t\t\t\tdetail: `lock scan failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (orphans === 0) {\n\t\t\t\treturn okOutcome(`${totalLive} live, 0 orphan`);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: 'warn' as const,\n\t\t\t\tdetail: `${orphans} orphan holder(s); rerun with --clean-locks`,\n\t\t\t};\n\t\t}).pipe(Effect.provide(layerLivenessProbeScope)),\n});\n\n/** Probe: state-dir is writable. Validates DEVSTACK_STATE_DIR resolves\n * to a usable path (or the default ~/.devstack does). */\nexport const stateDirProbe = (stateDir: string): Probe => ({\n\tname: 'state-dir',\n\tdescription: 'state directory',\n\trequired: true,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tif (!existsSync(stateDir)) {\n\t\t\t\treturn okOutcome(`${stateDir} (will be created)`);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst s = statSync(stateDir);\n\t\t\t\tif (!s.isDirectory()) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\t\tdetail: `${stateDir} is not a directory`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn okOutcome(stateDir);\n\t\t\t} catch (cause) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\tdetail: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t};\n\t\t\t}\n\t\t}),\n});\n\n/** Probe: optional fork-cache directory inspection. Returns the size\n * of the cache (informational). */\nexport const forkCacheProbe = (appRoot: string): Probe => ({\n\tname: 'fork-cache',\n\tdescription: 'sui-fork cache',\n\trequired: false,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst cacheDir = join(appRoot, '.fork-cache');\n\t\t\tif (!existsSync(cacheDir)) return okOutcome('(absent)');\n\t\t\ttry {\n\t\t\t\tconst size = countTreeSize(cacheDir);\n\t\t\t\treturn okOutcome(`${size} entries`);\n\t\t\t} catch {\n\t\t\t\treturn { status: 'warn', detail: 'fork cache scan failed' };\n\t\t\t}\n\t\t}),\n});\n\nconst countTreeSize = (dir: string, depth = 0): number => {\n\tif (depth > 3) return 0;\n\tlet n = 0;\n\tfor (const entry of readdirSync(dir)) {\n\t\tconst p = join(dir, entry);\n\t\ttry {\n\t\t\tconst s = statSync(p);\n\t\t\tif (s.isFile()) n += 1;\n\t\t\telse if (s.isDirectory()) n += countTreeSize(p, depth + 1);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\treturn n;\n};\n\n/** Default probe set. `cli/main.ts` composes this with the resolved\n * app root, runtime state dir, router profile, and router entrypoints. */\nexport const defaultProbes = (params: {\n\treadonly stateDir: string;\n\treadonly appRoot: string;\n\treadonly routerProfile?: RouterProfile;\n\treadonly routerEntrypoints?: ReadonlyArray<Entrypoint>;\n}): ReadonlyArray<Probe> => [\n\tdockerProbe,\n\tsuiCliProbe,\n\tstateDirProbe(params.stateDir),\n\trouterProfileProbe({\n\t\t...(params.routerProfile === undefined ? {} : { profile: params.routerProfile }),\n\t\t...(params.routerEntrypoints === undefined ? {} : { entrypoints: params.routerEntrypoints }),\n\t}),\n\tlocksProbe(params.appRoot),\n\tforkCacheProbe(params.appRoot),\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"doctor-probes.mjs","names":[],"sources":["../../src/cli/doctor-probes.ts"],"sourcesContent":["// L4-adjacent CLI infrastructure: doctor probe definitions.\n//\n// Per STYLE_GUIDE §7: `cli/*.ts` modules sit alongside the bin entry\n// (`cli/main.ts`) and may import L3 orchestrator + L2 plugin barrels —\n// they are NOT L4 surfaces proper. The router-profile probe needs\n// both the L3 router orchestrator helpers (parsing dispatch files,\n// matching profile labels, sorting entrypoint ports) and the built-in\n// L2 router entrypoint composition; that wiring lives here.\n//\n// The probes themselves are read-only diagnostics: Docker reachable,\n// `sui` CLI on PATH, state-dir writable, router profile state +\n// dispatch leases + entrypoint listeners, orphan cross-process locks,\n// fork-cache health. Each returns a typed `ProbeOutcome` and never\n// throws. `required: true` means a `fail | unavailable` projects to\n// `CliUnavailableError` and exits 69; non-required probes are\n// informational.\n\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { createConnection } from 'node:net';\nimport { join } from 'node:path';\n\nimport { Effect } from 'effect';\n\nimport {\n\tdispatchFileIdFromFilename,\n\tmakeDefaultRouterProfile,\n\tparseDispatchRouteFile,\n\trouterProfileLabelsMatch,\n\tuniqueSortedEntrypointPorts,\n\ttype Entrypoint,\n\ttype RouterProfile,\n} from '../orchestrators/router/index.ts';\nimport { BUILT_IN_ENTRYPOINTS } from '../plugins/router-entrypoints.ts';\nimport { parseJsonTextSync } from '../substrate/runtime/runtime-decode.ts';\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\treadRoster,\n} from '../substrate/runtime/cross-process/index.ts';\nimport type { RosterHolder } from '../substrate/cross-process.ts';\nimport type { Probe, ProbeOutcome } from '../surfaces/cli/commands/doctor.ts';\n\nconst okOutcome = (detail?: string): ProbeOutcome =>\n\tdetail !== undefined ? { status: 'ok', detail } : { status: 'ok' };\n\ntype CommandResult = { ok: true; out: string } | { ok: false; err: string };\n\nconst captureCommand = (\n\tcmd: string,\n\targs: ReadonlyArray<string>,\n\ttimeoutMs = 3000,\n): CommandResult => {\n\ttry {\n\t\tconst out = execFileSync(cmd, args as string[], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: timeoutMs,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t});\n\t\treturn { ok: true, out: out.trim() };\n\t} catch (err) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terr: err instanceof Error ? err.message : String(err),\n\t\t};\n\t}\n};\n\n/** Probe: `docker version` resolves. */\nexport const dockerProbe: Probe = {\n\tname: 'docker',\n\tdescription: 'Docker daemon reachable',\n\trequired: true,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst result = captureCommand('docker', ['version', '--format', '{{.Server.Version}}']);\n\t\t\tif (!result.ok) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\tdetail: `docker version failed: ${result.err}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn okOutcome(`server ${result.out}`);\n\t\t}),\n};\n\n/** Probe: the `sui` CLI on PATH. Needed for `devstack codegen` (the\n * `sui move summary` step has no Docker fallback) and for live/fork\n * modes. Kept a non-required WARN, not a hard failure: a stack-free\n * `pnpm build` works without `sui` via the committed (pre-baked)\n * `src/generated` tree, so a missing `sui` shouldn't fail `doctor`\n * outright — it only blocks codegen + live/fork. */\nexport const suiCliProbe: Probe = {\n\tname: 'sui-cli',\n\tdescription: '`sui` CLI on PATH',\n\trequired: false,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst result = captureCommand('sui', ['--version']);\n\t\t\treturn result.ok\n\t\t\t\t? okOutcome(result.out)\n\t\t\t\t: {\n\t\t\t\t\t\tstatus: 'warn',\n\t\t\t\t\t\tdetail: 'sui CLI not on PATH (needed for `devstack codegen` and live/fork modes)',\n\t\t\t\t\t};\n\t\t}),\n};\n\n/** Probe: a TCP port is FREE on localhost (resolves `true` when nothing\n * is listening — connection refused/timeout — and `false` when a\n * listener answers). Backs `routerProfileProbe`'s entrypoint-listener\n * check. */\nconst probePortFree = (port: number, timeoutMs = 500): Promise<boolean> =>\n\tnew Promise((resolve) => {\n\t\tconst socket = createConnection({ host: '127.0.0.1', port });\n\t\tconst finish = (free: boolean): void => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(free);\n\t\t};\n\t\tconst timer = setTimeout(() => finish(true), timeoutMs);\n\t\tsocket.on('connect', () => {\n\t\t\tclearTimeout(timer);\n\t\t\tfinish(false);\n\t\t});\n\t\tsocket.on('error', () => {\n\t\t\tclearTimeout(timer);\n\t\t\tfinish(true);\n\t\t});\n\t});\n\nexport type DoctorCommandRunner = typeof captureCommand;\nexport type PortAvailabilityProbe = typeof probePortFree;\n\nexport interface RouterProfileProbeOptions {\n\treadonly profile?: RouterProfile;\n\treadonly entrypoints?: ReadonlyArray<Entrypoint>;\n\treadonly command?: DoctorCommandRunner;\n\treadonly probePort?: PortAvailabilityProbe;\n}\n\ninterface RouterDispatchScan {\n\treadonly status: 'absent' | 'ok' | 'not-directory' | 'unreadable';\n\treadonly files: number;\n\treadonly liveRoutes: number;\n\treadonly staleRoutes: number;\n\treadonly unknownOwnerRoutes: number;\n\treadonly corruptRouteFiles: number;\n\treadonly diagnostics: number;\n\treadonly safeToPrune: boolean;\n}\n\ninterface RouterContainerStatus {\n\treadonly status: 'absent' | 'present' | 'invalid' | 'unavailable';\n\treadonly detail?: string;\n\treadonly running?: boolean;\n\treadonly labelsMatch?: boolean;\n\treadonly attachedToNetwork?: boolean;\n\treadonly publishedPorts?: ReadonlyArray<number>;\n}\n\ninterface RouterNetworkStatus {\n\treadonly status: 'absent' | 'present' | 'invalid' | 'unavailable';\n\treadonly detail?: string;\n\treadonly id?: string;\n}\n\nconst unknownRecord = (value: unknown): Record<string, unknown> | null =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: null;\n\nconst stringRecord = (value: unknown): Record<string, string> => {\n\tconst rec = unknownRecord(value);\n\tif (rec === null) return {};\n\tconst out: Record<string, string> = {};\n\tfor (const [key, raw] of Object.entries(rec)) {\n\t\tif (typeof raw === 'string') out[key] = raw;\n\t}\n\treturn out;\n};\n\nconst fieldRecord = (\n\trecord: Record<string, unknown>,\n\tkey: string,\n): Record<string, unknown> | null => unknownRecord(record[key]);\n\nconst fieldString = (record: Record<string, unknown>, key: string): string | null =>\n\ttypeof record[key] === 'string' ? (record[key] as string) : null;\n\nconst fieldBoolean = (record: Record<string, unknown>, key: string): boolean | null =>\n\ttypeof record[key] === 'boolean' ? (record[key] as boolean) : null;\n\nconst parseDockerInspectFirst = (out: string): Record<string, unknown> | null => {\n\ttry {\n\t\tconst parsed = parseJsonTextSync(out, {\n\t\t\tsource: 'docker inspect',\n\t\t\tmkError: (issue) => issue,\n\t\t});\n\t\tif (Array.isArray(parsed)) return unknownRecord(parsed[0]);\n\t\treturn unknownRecord(parsed);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerObjectIsAbsent = (err: string): boolean =>\n\t/No such (object|container|network)|not found|No such/i.test(err);\n\nconst uniqueNumbers = (values: ReadonlyArray<number>): ReadonlyArray<number> =>\n\t[...new Set(values)].sort((a, b) => a - b);\n\nconst inspectRouterContainer = (\n\tprofile: RouterProfile,\n\tcommand: DoctorCommandRunner,\n): RouterContainerStatus => {\n\tconst result = command('docker', ['container', 'inspect', profile.containerName]);\n\tif (!result.ok) {\n\t\treturn dockerObjectIsAbsent(result.err)\n\t\t\t? { status: 'absent' }\n\t\t\t: { status: 'unavailable', detail: result.err };\n\t}\n\tconst doc = parseDockerInspectFirst(result.out);\n\tif (doc === null) return { status: 'invalid', detail: 'container inspect JSON was invalid' };\n\tconst state = fieldRecord(doc, 'State') ?? {};\n\tconst config = fieldRecord(doc, 'Config') ?? {};\n\tconst networkSettings = fieldRecord(doc, 'NetworkSettings') ?? {};\n\tconst networks = fieldRecord(networkSettings, 'Networks') ?? {};\n\tconst ports = fieldRecord(networkSettings, 'Ports') ?? {};\n\tconst publishedPorts: Array<number> = [];\n\tfor (const bindings of Object.values(ports)) {\n\t\tif (!Array.isArray(bindings)) continue;\n\t\tfor (const binding of bindings) {\n\t\t\tconst rec = unknownRecord(binding);\n\t\t\tif (rec === null) continue;\n\t\t\tconst rawPort = fieldString(rec, 'HostPort');\n\t\t\tif (rawPort !== null && /^\\d+$/.test(rawPort)) {\n\t\t\t\tpublishedPorts.push(Number.parseInt(rawPort, 10));\n\t\t\t}\n\t\t}\n\t}\n\tconst labels = stringRecord(config.Labels);\n\treturn {\n\t\tstatus: 'present',\n\t\trunning: fieldBoolean(state, 'Running') === true,\n\t\tlabelsMatch: routerProfileLabelsMatch(labels, profile),\n\t\tattachedToNetwork: Object.keys(networks).includes(profile.networkName),\n\t\tpublishedPorts: uniqueNumbers(publishedPorts),\n\t};\n};\n\nconst inspectRouterNetwork = (\n\tprofile: RouterProfile,\n\tcommand: DoctorCommandRunner,\n): RouterNetworkStatus => {\n\tconst result = command('docker', ['network', 'inspect', profile.networkName]);\n\tif (!result.ok) {\n\t\treturn dockerObjectIsAbsent(result.err)\n\t\t\t? { status: 'absent' }\n\t\t\t: { status: 'unavailable', detail: result.err };\n\t}\n\tconst doc = parseDockerInspectFirst(result.out);\n\tif (doc === null) return { status: 'invalid', detail: 'network inspect JSON was invalid' };\n\tconst id = fieldString(doc, 'Id');\n\treturn id === null ? { status: 'present' } : { status: 'present', id };\n};\n\nconst inspectRouterDispatch = (profile: RouterProfile): Effect.Effect<RouterDispatchScan> =>\n\t// Yield a fresh `LivenessProbeScope` so repeated lease owners across\n\t// many dispatch route files fork the OS probe once per pid.\n\tEffect.gen(function* () {\n\t\tif (!existsSync(profile.dispatchDir)) {\n\t\t\treturn {\n\t\t\t\tstatus: 'absent' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 0,\n\t\t\t\tcorruptRouteFiles: 0,\n\t\t\t\tdiagnostics: 0,\n\t\t\t\tsafeToPrune: true,\n\t\t\t};\n\t\t}\n\t\ttry {\n\t\t\tif (!statSync(profile.dispatchDir).isDirectory()) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'not-directory' as const,\n\t\t\t\t\tfiles: 0,\n\t\t\t\t\tliveRoutes: 0,\n\t\t\t\t\tstaleRoutes: 0,\n\t\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\t\tdiagnostics: 1,\n\t\t\t\t\tsafeToPrune: false,\n\t\t\t\t};\n\t\t\t}\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tstatus: 'unreadable' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\tdiagnostics: 1,\n\t\t\t\tsafeToPrune: false,\n\t\t\t};\n\t\t}\n\t\tlet files: ReadonlyArray<string>;\n\t\ttry {\n\t\t\tfiles = readdirSync(profile.dispatchDir);\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tstatus: 'unreadable' as const,\n\t\t\t\tfiles: 0,\n\t\t\t\tliveRoutes: 0,\n\t\t\t\tstaleRoutes: 0,\n\t\t\t\tunknownOwnerRoutes: 1,\n\t\t\t\tcorruptRouteFiles: 1,\n\t\t\t\tdiagnostics: 1,\n\t\t\t\tsafeToPrune: false,\n\t\t\t};\n\t\t}\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tlet routeFiles = 0;\n\t\tlet liveRoutes = 0;\n\t\tlet staleRoutes = 0;\n\t\tlet unknownOwnerRoutes = 0;\n\t\tlet corruptRouteFiles = 0;\n\t\tlet diagnostics = 0;\n\t\tfor (const filename of files) {\n\t\t\tconst dispatchFileId = dispatchFileIdFromFilename(filename);\n\t\t\tif (dispatchFileId === null) continue;\n\t\t\trouteFiles += 1;\n\t\t\tlet body: string;\n\t\t\ttry {\n\t\t\t\tbody = readFileSync(join(profile.dispatchDir, filename), 'utf8');\n\t\t\t} catch {\n\t\t\t\tcorruptRouteFiles += 1;\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst parsed = parseDispatchRouteFile(body, dispatchFileId);\n\t\t\tdiagnostics += parsed.diagnostics.length;\n\t\t\tif (parsed._tag === 'invalid') {\n\t\t\t\tcorruptRouteFiles += 1;\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (parsed.route.lease === null) {\n\t\t\t\tunknownOwnerRoutes += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst leaseStatus = yield* probe\n\t\t\t\t.probeHolderLiveness(parsed.route.lease.owner)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\tif (leaseStatus === 'dead') staleRoutes += 1;\n\t\t\telse liveRoutes += 1;\n\t\t}\n\t\tconst safeToPrune = liveRoutes === 0 && unknownOwnerRoutes === 0 && corruptRouteFiles === 0;\n\t\treturn {\n\t\t\tstatus: 'ok' as const,\n\t\t\tfiles: routeFiles,\n\t\t\tliveRoutes,\n\t\t\tstaleRoutes,\n\t\t\tunknownOwnerRoutes,\n\t\t\tcorruptRouteFiles,\n\t\t\tdiagnostics,\n\t\t\tsafeToPrune,\n\t\t};\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n\nconst inspectRouterStateDir = (profile: RouterProfile): 'absent' | 'present' | 'not-directory' => {\n\tif (!existsSync(profile.stateDir)) return 'absent';\n\ttry {\n\t\treturn statSync(profile.stateDir).isDirectory() ? 'present' : 'not-directory';\n\t} catch {\n\t\treturn 'not-directory';\n\t}\n};\n\nconst summarizeRouterContainer = (container: RouterContainerStatus): string => {\n\tif (container.status !== 'present') return container.status;\n\treturn [\n\t\tcontainer.running === true ? 'running' : 'stopped',\n\t\t`labels=${container.labelsMatch === true ? 'ok' : 'mismatch'}`,\n\t\t`network=${container.attachedToNetwork === true ? 'attached' : 'missing'}`,\n\t\t`ports=${(container.publishedPorts ?? []).join(',') || 'none'}`,\n\t].join('/');\n};\n\nexport const routerProfileProbe = (options: RouterProfileProbeOptions = {}): Probe => ({\n\tname: 'router-profile',\n\tdescription: 'router profile state, dispatch leases, and entrypoint listeners',\n\trequired: false,\n\trun: () =>\n\t\tEffect.gen(function* () {\n\t\t\tconst profile = options.profile ?? makeDefaultRouterProfile();\n\t\t\tconst entrypoints = options.entrypoints ?? BUILT_IN_ENTRYPOINTS;\n\t\t\tconst command = options.command ?? captureCommand;\n\t\t\tconst probePort = options.probePort ?? probePortFree;\n\t\t\tconst ports = uniqueSortedEntrypointPorts(entrypoints);\n\t\t\tconst stateDirStatus = inspectRouterStateDir(profile);\n\t\t\tconst dispatch = yield* inspectRouterDispatch(profile);\n\t\t\tconst container = inspectRouterContainer(profile, command);\n\t\t\tconst network = inspectRouterNetwork(profile, command);\n\t\t\tconst listenerResults = yield* Effect.tryPromise({\n\t\t\t\ttry: () =>\n\t\t\t\t\tPromise.all(\n\t\t\t\t\t\tports.map(async (port) => ({\n\t\t\t\t\t\t\tport,\n\t\t\t\t\t\t\tfree: await probePort(port),\n\t\t\t\t\t\t})),\n\t\t\t\t\t),\n\t\t\t\tcatch: () => 'router entrypoint listener probe failed',\n\t\t\t}).pipe(Effect.catch(() => Effect.succeed(ports.map((port) => ({ port, free: true })))));\n\t\t\tconst freePorts = listenerResults.filter((r) => r.free).map((r) => r.port);\n\t\t\tconst occupiedPorts = listenerResults.filter((r) => !r.free).map((r) => r.port);\n\t\t\tconst protectedRoutes =\n\t\t\t\tdispatch.liveRoutes + dispatch.unknownOwnerRoutes + dispatch.corruptRouteFiles;\n\t\t\tconst problems: string[] = [];\n\t\t\tif (stateDirStatus === 'not-directory')\n\t\t\t\tproblems.push(`${profile.stateDir} is not a directory`);\n\t\t\tif (dispatch.status === 'not-directory' || dispatch.status === 'unreadable') {\n\t\t\t\tproblems.push(`dispatch dir is ${dispatch.status}`);\n\t\t\t}\n\t\t\tif (dispatch.unknownOwnerRoutes > 0 || dispatch.corruptRouteFiles > 0) {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`unknown/corrupt dispatch leases=${dispatch.unknownOwnerRoutes + dispatch.corruptRouteFiles}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (dispatch.staleRoutes > 0) problems.push(`stale dispatch leases=${dispatch.staleRoutes}`);\n\t\t\tif (protectedRoutes > 0 && (container.status !== 'present' || container.running !== true)) {\n\t\t\t\tproblems.push('protected dispatch leases exist without a running router container');\n\t\t\t}\n\t\t\tif (container.status === 'invalid' || container.status === 'unavailable') {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`router container inspect ${container.status}: ${container.detail ?? 'unknown'}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (container.status === 'present') {\n\t\t\t\tif (container.labelsMatch !== true)\n\t\t\t\t\tproblems.push('router container labels do not match profile');\n\t\t\t\tif (container.attachedToNetwork !== true) {\n\t\t\t\t\tproblems.push('router container is not attached to profile network');\n\t\t\t\t}\n\t\t\t\tif (container.running === true && freePorts.length > 0) {\n\t\t\t\t\tproblems.push(`router entrypoint listeners missing: ${freePorts.join(', ')}`);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (container.status !== 'present' && occupiedPorts.length > 0) {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`router entrypoint ports already in use without profile container: ${occupiedPorts.join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (network.status === 'invalid' || network.status === 'unavailable') {\n\t\t\t\tproblems.push(`router network inspect ${network.status}: ${network.detail ?? 'unknown'}`);\n\t\t\t}\n\t\t\tconst detail = [\n\t\t\t\t`profile=${profile.id}`,\n\t\t\t\t`state=${stateDirStatus}`,\n\t\t\t\t`dispatch=${dispatch.status}:files=${dispatch.files},live=${dispatch.liveRoutes},stale=${dispatch.staleRoutes},unknown=${dispatch.unknownOwnerRoutes},corrupt=${dispatch.corruptRouteFiles},diagnostics=${dispatch.diagnostics},pruneSafe=${dispatch.safeToPrune ? 'yes' : 'no'}`,\n\t\t\t\t`container=${summarizeRouterContainer(container)}`,\n\t\t\t\t`network=${network.status}`,\n\t\t\t\t`entrypoints=${ports.join(', ') || 'none'}`,\n\t\t\t].join('; ');\n\t\t\tif (problems.length === 0) return okOutcome(detail);\n\t\t\treturn {\n\t\t\t\tstatus: 'warn' as const,\n\t\t\t\tdetail: `${detail}; ${problems.join('; ')}`,\n\t\t\t};\n\t\t}),\n});\n\n/** Probe: list orphaned stack locks under `<runtimeRoot>/<app>/`. A\n * stack lock whose owner PID is dead is a stale-lock candidate that\n * `--clean-locks` would reclaim. */\nexport const locksProbe = (appRoot: string): Probe => ({\n\tname: 'locks',\n\tdescription: 'stale cross-process locks',\n\trequired: false,\n\trun: () =>\n\t\tEffect.gen(function* () {\n\t\t\tif (!existsSync(appRoot)) return okOutcome('(no app root yet)');\n\t\t\t// Yield a fresh `LivenessProbeScope` so a single pid that\n\t\t\t// shows up in multiple stack rosters under this app root is\n\t\t\t// probed AT MOST once across the full lock scan.\n\t\t\tconst probe = yield* LivenessProbeScope;\n\t\t\tlet orphans = 0;\n\t\t\tlet totalLive = 0;\n\t\t\ttry {\n\t\t\t\tfor (const entry of readdirSync(appRoot)) {\n\t\t\t\t\tif (entry.startsWith('.')) continue;\n\t\t\t\t\tconst stackRoot = join(appRoot, entry);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (!statSync(stackRoot).isDirectory()) continue;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst rosterFile = join(stackRoot, 'roster.json');\n\t\t\t\t\tif (!existsSync(rosterFile)) continue;\n\t\t\t\t\tconst doc = yield* readRoster(rosterFile).pipe(\n\t\t\t\t\t\tEffect.catch(() =>\n\t\t\t\t\t\t\tEffect.succeed({ version: 1 as const, holders: [] as RosterHolder[] }),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tfor (const holder of doc.holders) {\n\t\t\t\t\t\tconst liveness = yield* probe\n\t\t\t\t\t\t\t.probeHolderLiveness(holder)\n\t\t\t\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\t\t\t\tif (liveness === 'alive') totalLive += 1;\n\t\t\t\t\t\telse orphans += 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (cause) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'warn' as const,\n\t\t\t\t\tdetail: `lock scan failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (orphans === 0) {\n\t\t\t\treturn okOutcome(`${totalLive} live, 0 orphan`);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: 'warn' as const,\n\t\t\t\tdetail: `${orphans} orphan holder(s); rerun with --clean-locks`,\n\t\t\t};\n\t\t}).pipe(Effect.provide(layerLivenessProbeScope)),\n});\n\n/** Probe: state-dir is writable. Validates DEVSTACK_STATE_DIR resolves\n * to a usable path (or the default ~/.devstack does). */\nexport const stateDirProbe = (stateDir: string): Probe => ({\n\tname: 'state-dir',\n\tdescription: 'state directory',\n\trequired: true,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tif (!existsSync(stateDir)) {\n\t\t\t\treturn okOutcome(`${stateDir} (will be created)`);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst s = statSync(stateDir);\n\t\t\t\tif (!s.isDirectory()) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\t\tdetail: `${stateDir} is not a directory`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn okOutcome(stateDir);\n\t\t\t} catch (cause) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: 'fail',\n\t\t\t\t\tdetail: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t};\n\t\t\t}\n\t\t}),\n});\n\n/** Probe: optional fork-cache directory inspection. Returns the size\n * of the cache (informational). */\nexport const forkCacheProbe = (appRoot: string): Probe => ({\n\tname: 'fork-cache',\n\tdescription: 'sui-fork cache',\n\trequired: false,\n\trun: () =>\n\t\tEffect.sync(() => {\n\t\t\tconst cacheDir = join(appRoot, '.fork-cache');\n\t\t\tif (!existsSync(cacheDir)) return okOutcome('(absent)');\n\t\t\ttry {\n\t\t\t\tconst size = countTreeSize(cacheDir);\n\t\t\t\treturn okOutcome(`${size} entries`);\n\t\t\t} catch {\n\t\t\t\treturn { status: 'warn', detail: 'fork cache scan failed' };\n\t\t\t}\n\t\t}),\n});\n\nconst countTreeSize = (dir: string, depth = 0): number => {\n\tif (depth > 3) return 0;\n\tlet n = 0;\n\tfor (const entry of readdirSync(dir)) {\n\t\tconst p = join(dir, entry);\n\t\ttry {\n\t\t\tconst s = statSync(p);\n\t\t\tif (s.isFile()) n += 1;\n\t\t\telse if (s.isDirectory()) n += countTreeSize(p, depth + 1);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\treturn n;\n};\n\n/** Default probe set. `cli/main.ts` composes this with the resolved\n * app root, runtime state dir, router profile, and router entrypoints. */\nexport const defaultProbes = (params: {\n\treadonly stateDir: string;\n\treadonly appRoot: string;\n\treadonly routerProfile?: RouterProfile;\n\treadonly routerEntrypoints?: ReadonlyArray<Entrypoint>;\n}): ReadonlyArray<Probe> => [\n\tdockerProbe,\n\tsuiCliProbe,\n\tstateDirProbe(params.stateDir),\n\trouterProfileProbe({\n\t\t...(params.routerProfile === undefined ? {} : { profile: params.routerProfile }),\n\t\t...(params.routerEntrypoints === undefined ? {} : { entrypoints: params.routerEntrypoints }),\n\t}),\n\tlocksProbe(params.appRoot),\n\tforkCacheProbe(params.appRoot),\n];\n"],"mappings":"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"}