@mysten-incubation/devstack 0.2.0 → 0.3.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 (47) hide show
  1. package/dashboard-ui/assets/{grpc-a4usE3Nk.js → grpc-Clz0oOtc.js} +1 -1
  2. package/dashboard-ui/assets/{index-CRYJ4pru.js → index-Cmqv9kiU.js} +39 -33
  3. package/dashboard-ui/index.html +1 -1
  4. package/dist/api/run-stack-internal.mjs +1 -0
  5. package/dist/api/run-stack-internal.mjs.map +1 -1
  6. package/dist/build-integrations/playwright/config.d.mts +2 -1
  7. package/dist/build-integrations/playwright/config.mjs +1 -1
  8. package/dist/build-integrations/playwright/config.mjs.map +1 -1
  9. package/dist/build-integrations/vite/index.d.mts +39 -9
  10. package/dist/build-integrations/vite/index.mjs +47 -11
  11. package/dist/build-integrations/vite/index.mjs.map +1 -1
  12. package/dist/build-integrations/vitest/config.mjs +3 -4
  13. package/dist/build-integrations/vitest/config.mjs.map +1 -1
  14. package/dist/cli/doctor-probes.mjs +7 -2
  15. package/dist/cli/doctor-probes.mjs.map +1 -1
  16. package/dist/cli/main.mjs +2 -2
  17. package/dist/cli/main.mjs.map +1 -1
  18. package/dist/cli/wirings/codegen.mjs +35 -35
  19. package/dist/cli/wirings/codegen.mjs.map +1 -1
  20. package/dist/cli/wirings/up.mjs +7 -0
  21. package/dist/cli/wirings/up.mjs.map +1 -1
  22. package/dist/orchestrators/boot.mjs +12 -2
  23. package/dist/orchestrators/boot.mjs.map +1 -1
  24. package/dist/orchestrators/codegen/service.mjs +23 -8
  25. package/dist/orchestrators/codegen/service.mjs.map +1 -1
  26. package/dist/plugins/dashboard/domain.mjs +18 -1
  27. package/dist/plugins/dashboard/domain.mjs.map +1 -1
  28. package/dist/plugins/dashboard/schema/root.mjs +2 -2
  29. package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
  30. package/dist/plugins/dashboard/schema/types.mjs +15 -2
  31. package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
  32. package/dist/plugins/deepbook/index.d.mts +7 -3
  33. package/dist/plugins/deepbook/index.mjs +3 -3
  34. package/dist/plugins/deepbook/index.mjs.map +1 -1
  35. package/dist/plugins/sui/index.d.mts +10 -10
  36. package/dist/plugins/sui/move-summary-runner.mjs +29 -2
  37. package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
  38. package/dist/substrate/runtime/index.mjs +1 -0
  39. package/dist/substrate/runtime/lifecycle/file-watcher.d.mts +1 -0
  40. package/dist/substrate/runtime/lifecycle/file-watcher.mjs +54 -0
  41. package/dist/substrate/runtime/lifecycle/file-watcher.mjs.map +1 -0
  42. package/dist/substrate/runtime/lifecycle/index.mjs +1 -0
  43. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +76 -6
  44. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
  45. package/dist/substrate/runtime/supervisor/start-supervisor.mjs +2 -6
  46. package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
  47. package/package.json +1 -1
@@ -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"}
@@ -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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"}
package/dist/cli/main.mjs CHANGED
@@ -14,9 +14,9 @@ import { nodeConfirmPrompt } from "../surfaces/cli/commands/confirm-node.mjs";
14
14
  import { makeSnapshotReader } from "./snapshot-reader.mjs";
15
15
  import { makeDirectPruneDeps } from "./prune-direct.mjs";
16
16
  import { makeConfigLoader } from "./wirings/config-loader.mjs";
17
+ import { runCodegen } from "./wirings/codegen.mjs";
17
18
  import { runUpLive } from "./wirings/up.mjs";
18
19
  import { runApplyLive } from "./wirings/apply.mjs";
19
- import { runCodegen } from "./wirings/codegen.mjs";
20
20
  import { runDumpIds } from "./wirings/dump-ids.mjs";
21
21
  import { runSnapshotCaptureLiveAware, runSnapshotDeleteDirect, runSnapshotRestoreDirect } from "./wirings/snapshot.mjs";
22
22
  import { runWipeDirect, runWipePlanDirect } from "./wirings/wipe.mjs";
@@ -135,7 +135,7 @@ const buildDirectDeps = (identity) => {
135
135
  snapshotStalePolicy: flags.snapshotStalePolicy
136
136
  }) },
137
137
  apply: { run: (flags) => runApplyLive(flags.configPath, identity) },
138
- codegen: { run: (flags) => runCodegen(flags.configPath, identity) },
138
+ codegen: { run: (flags) => runCodegen(flags.configPath) },
139
139
  dumpIds: { run: (flags) => runDumpIds(identity, {
140
140
  configPath: flags.configPath,
141
141
  out: flags.out,
@@ -1 +1 @@
1
- {"version":3,"file":"main.mjs","names":["resolvePath"],"sources":["../../src/cli/main.ts"],"sourcesContent":["// devstack CLI bin entry — argv → identity → deps → dispatch → exit.\n//\n// This file is the executable entry the `bin: { devstack: ... }`\n// package.json field points at. Build output: `dist/cli/main.mjs`.\n//\n// Architecture invariant (surfaces/cli/index.ts header):\n// \"`up` must hand its long-running effect to the outer Node runtime\n// directly, not nest a runtime — otherwise SIGINT cannot reach\n// scope finalizers and container teardown leaks.\"\n//\n// Shape:\n// 1. Pre-parse identity inputs from argv + env (see\n// `identityInputsFromArgv`).\n// 2. Resolve identity (app / stack / network / runtime root) via the\n// shared `api/inference-network` helpers.\n// 3. Build the dispatcher's `CliDeps` bundle from each verb's wiring\n// module in `cli/wirings/`.\n// 4. Hand control to the Stricli-backed dispatcher, which routes to\n// the verb-scoped wiring function.\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, join, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { endpointKey, pluginKey } from '../substrate/brand.ts';\nimport { emptyProjection } from '../substrate/runtime/projection/state-ref.ts';\nimport type { SubscribableState } from '../substrate/projection.ts';\nimport { readStackContext } from '../build-integrations/runtime/read-stack-context.ts';\nimport type { StackContext } from '../build-integrations/runtime/stack-context.ts';\nimport { dispatch, type CliDeps, CliUsageError } from '../surfaces/cli/index.ts';\nimport { emitFailure, nodeProcessIO } from '../surfaces/cli/output.ts';\nimport { ENV_VARS } from '../surfaces/cli/flags.ts';\nimport { defaultProbes } from './doctor-probes.ts';\nimport { nodeConfirmPrompt } from '../surfaces/cli/commands/confirm-node.ts';\nimport type { StatusReader } from '../surfaces/cli/commands/status.ts';\nimport { ExitCode } from '../surfaces/cli/sysexits.ts';\nimport { makeSnapshotReader } from './snapshot-reader.ts';\nimport {\n\tDevstackNetworkParseError,\n\tresolveAppName,\n\tresolveNetworkSync,\n\tresolveStackName,\n\tresolveStateDir,\n} from '../api/inference-network.ts';\nimport { makeDirectPruneDeps } from './prune-direct.ts';\nimport { runUpLive } from './wirings/up.ts';\nimport { runApplyLive } from './wirings/apply.ts';\nimport { runCodegen } from './wirings/codegen.ts';\nimport { runDumpIds } from './wirings/dump-ids.ts';\nimport {\n\trunSnapshotCaptureLiveAware,\n\trunSnapshotDeleteDirect,\n\trunSnapshotRestoreDirect,\n} from './wirings/snapshot.ts';\nimport { runWipeDirect, runWipePlanDirect } from './wirings/wipe.ts';\nimport { makeConfigLoader, resolveConfigPath } from './wirings/config-loader.ts';\nimport type { ResolvedIdentity } from './wirings/identity.ts';\n\n// -----------------------------------------------------------------------------\n// Identity resolution\n// -----------------------------------------------------------------------------\n\n/** Resolve identity from flags + env. App and stack fall through the\n * shared cwd/package metadata resolver before their defaults.\n *\n * State-dir precedence ladder:\n * `--state-dir` flag (`stateDir`) > `config.options.stateDir`\n * (`configStateDir`) > `$DEVSTACK_STATE_DIR` > `<cwd>/.devstack`.\n * The flag maps to `resolveStateDir`'s top `runtimeRoot` rung and the\n * config value to its `stateDir` rung so the flag always wins over a\n * config-declared `defineDevstack({ stateDir })`.\n *\n * Stack-name precedence ladder (the SAME flag-beats-config shape as\n * state-dir, so the two are consistent and reviewable side-by-side):\n * `--stack` flag / `$DEVSTACK_STACK` (`params.stack`)\n * > config's `defineDevstack({ stackName })`\n * > cwd/package inference (default `'main'`).\n * Config-vs-explicit resolution can't happen here (the config isn't\n * loaded yet for the no-config verbs), so we record the explicit value\n * on `ResolvedIdentity.explicitStack` and let the verb wirings apply\n * `config.stackName` ONLY when no explicit stack was given — see\n * `effectiveStackName` in `cli/wirings/identity.ts`. `params.stack` is\n * exactly the explicit `--stack`-or-`$DEVSTACK_STACK` value (the argv\n * pre-parser seeds it from env, then lets the flag overwrite it), and\n * is `undefined` when neither was provided. */\nconst resolveIdentity = (params: {\n\treadonly app: string | undefined;\n\treadonly stack: string | undefined;\n\treadonly network: string | undefined;\n\treadonly stateDir: string | undefined;\n\treadonly configStateDir?: string | undefined;\n\treadonly cwd?: string;\n}): ResolvedIdentity => {\n\tconst cwd = params.cwd ?? process.cwd();\n\tconst app = resolveAppName({\n\t\texplicit: params.app,\n\t\tcwd,\n\t});\n\tconst runtimeRoot = resolveStateDir({\n\t\truntimeRoot: params.stateDir,\n\t\tstateDir: params.configStateDir,\n\t\tenv: process.env.DEVSTACK_STATE_DIR,\n\t\tcwd,\n\t});\n\tconst stacksRoot = resolvePath(runtimeRoot, 'stacks');\n\t// `resolveStackName` here folds explicit (flag/env) > inferred >\n\t// default. Crucially it does NOT see `config.stackName`: a non-empty\n\t// `params.stack` means the operator was explicit, so `stack` already\n\t// equals the explicit value and `config.stackName` must not override\n\t// it downstream. An empty `params.stack` falls through to the\n\t// inferred/default name, which a verb's `config.stackName` may then\n\t// supersede via `effectiveStackName`.\n\tconst explicitStack =\n\t\tparams.stack !== undefined && params.stack.length > 0 ? params.stack : undefined;\n\tconst stack = resolveStackName({\n\t\texplicit: params.stack,\n\t\tcwd,\n\t});\n\t// Centralized explicit > env > default ladder. Throws\n\t// `DevstackNetworkParseError` on a malformed value so the CLI fails\n\t// fast with a structured error instead of a downstream cryptic\n\t// network-probe failure. The identity's network is the canonical name\n\t// (`localnet`, `testnet`, …), independent of how it was spelled.\n\tconst network = resolveNetworkSync({\n\t\texplicit: params.network,\n\t\tenv: process.env.DEVSTACK_NETWORK,\n\t\texplicitSource: '--network',\n\t}).parsed.name;\n\tconst stackRoot = resolvePath(stacksRoot, stack);\n\treturn {\n\t\tapp,\n\t\tstack,\n\t\tnetwork,\n\t\truntimeRoot,\n\t\tstacksRoot,\n\t\tstackRoot,\n\t\trosterFile: resolvePath(stackRoot, 'roster.json'),\n\t\texplicitStack,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Verb deps composition\n// -----------------------------------------------------------------------------\n\n/**\n * Degraded offline-status projection — built from the on-disk manifest,\n * NOT from a persisted projection twin (the projection snapshot file no\n * longer exists). When the stack is DOWN, the manifest is the only\n * durable record of the boot; `status` surfaces identity + endpoints\n * from it and leaves the live-only slices (`rows` / `accounts` /\n * `packages` / `errors`) empty. A freshly-booted stack also has empty\n * `rows`, so the status renderer already tolerates this shape.\n *\n * Pure projector — exported for the focused unit test. Starts from\n * `emptyProjection()` (the closed-vocabulary baseline) and fills only\n * the two manifest-backed slices, so no display vocabulary can leak in.\n * The manifest identity tuple is `{ app, stack, network }`, matching\n * the projection's identity shape directly. Endpoints get re-branded\n * (`endpointKey` / `pluginKey`)\n * to the projection's branded shape; `registeredAt` is unknown offline,\n * so it defaults to `0`.\n */\nexport const degradedStatusFromContext = (ctx: StackContext): SubscribableState => ({\n\t...emptyProjection(),\n\tidentity: {\n\t\tapp: ctx.identity.app,\n\t\tstack: ctx.identity.stack,\n\t\tnetwork: ctx.identity.network,\n\t},\n\tendpoints: ctx.endpoints.all().map((endpoint) => ({\n\t\tendpointKey: endpointKey(endpoint.endpointKey),\n\t\tpluginKey: pluginKey(endpoint.pluginKey),\n\t\tname: endpoint.name,\n\t\turl: endpoint.url,\n\t\tdisplayUrl: endpoint.displayUrl,\n\t\twireProtocol: endpoint.wireProtocol,\n\t\tregisteredAt: 0,\n\t})),\n});\n\nconst projectionStatusReader = (identity: ResolvedIdentity): StatusReader => ({\n\treadState: () =>\n\t\tEffect.sync(() => {\n\t\t\t// Offline read: project the on-disk manifest for THIS stack root.\n\t\t\t// A missing / malformed manifest (no `up` yet, or a hand-edit)\n\t\t\t// is a tolerated \"no state present\" — `readStackContext` throws\n\t\t\t// in that case, so swallow the throw and report `null` (the\n\t\t\t// status command's contract for an absent stack).\n\t\t\ttry {\n\t\t\t\tconst ctx = readStackContext({ manifestPath: join(identity.stackRoot, 'manifest.json') });\n\t\t\t\treturn degradedStatusFromContext(ctx);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n});\n\nconst buildDirectDeps = (identity: ResolvedIdentity): CliDeps => {\n\treturn {\n\t\tup: {\n\t\t\trun: (flags) =>\n\t\t\t\trunUpLive(flags.configPath, identity, {\n\t\t\t\t\trenderer: flags.renderer,\n\t\t\t\t\tstdoutIsTty: Boolean((process.stdout as { isTTY?: boolean }).isTTY),\n\t\t\t\t\tfromSnapshot: flags.fromSnapshot,\n\t\t\t\t\tsnapshotCache: flags.snapshotCache,\n\t\t\t\t\tsnapshotStalePolicy: flags.snapshotStalePolicy,\n\t\t\t\t}),\n\t\t},\n\t\tapply: {\n\t\t\trun: (flags) => runApplyLive(flags.configPath, identity),\n\t\t},\n\t\tcodegen: {\n\t\t\trun: (flags) => runCodegen(flags.configPath, identity),\n\t\t},\n\t\tdumpIds: {\n\t\t\trun: (flags) =>\n\t\t\t\trunDumpIds(identity, {\n\t\t\t\t\tconfigPath: flags.configPath,\n\t\t\t\t\tout: flags.out,\n\t\t\t\t\tio: flags.io,\n\t\t\t\t\toutputMode: flags.outputMode,\n\t\t\t\t}),\n\t\t},\n\t\tstatus: { reader: projectionStatusReader(identity) },\n\t\tsnapshot: {\n\t\t\treader: makeSnapshotReader(identity),\n\t\t\tcapture: (args) => runSnapshotCaptureLiveAware(identity, args),\n\t\t\trestore: (snapshotId) => runSnapshotRestoreDirect(identity, snapshotId),\n\t\t\tdelete: (snapshotId) => runSnapshotDeleteDirect(identity, snapshotId),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t\tprune: makeDirectPruneDeps({ runtimeRoot: identity.runtimeRoot }),\n\t\tdoctor: {\n\t\t\tprobes: defaultProbes({\n\t\t\t\tstateDir: identity.runtimeRoot,\n\t\t\t\tappRoot: identity.stacksRoot,\n\t\t\t}),\n\t\t},\n\t\tconfig: { loader: makeConfigLoader() },\n\t\twipe: {\n\t\t\twipe: () => runWipeDirect(identity),\n\t\t\tplan: () => runWipePlanDirect(identity),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Argv pre-parser\n// -----------------------------------------------------------------------------\n\n/** @internal Exported for tests. Resolves identity flag inputs from a\n * `--app <x>` / `--stack <x>` / `--network <x>` / `--state-dir <x>` /\n * `--config <x>` argv, falling back to `DEVSTACK_*` env vars. Throws\n * on a missing or flag-shaped value so a typo doesn't silently demote\n * a downstream flag, and on a duplicate flag (`--app a --app b`) so\n * the pre-parser does not silently last-write-wins a value that\n * Stricli will later reject outright. */\nexport const identityInputsFromArgv = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n) => {\n\tlet app = env.DEVSTACK_APP;\n\tlet stack = env.DEVSTACK_STACK;\n\tlet network = env.DEVSTACK_NETWORK;\n\t// `stateDir` intentionally captures the `--state-dir` flag ONLY. The\n\t// full ladder — `--state-dir` flag > `config.options.stateDir`\n\t// (`defineDevstack({ stateDir })`) > `$DEVSTACK_STATE_DIR` >\n\t// `<cwd>/.devstack` — is assembled at the `resolveStateDir(...)`\n\t// call-site in `resolveIdentity`: the flag wins (top `runtimeRoot`\n\t// rung), the best-effort config value sits below it (`stateDir`\n\t// rung), then env, then the cwd default. Keeping the pre-parser a\n\t// thin flag-extractor means config + env precedence live in one place.\n\tlet stateDir: string | undefined;\n\tlet configPath = env.DEVSTACK_CONFIG;\n\t// Tracks which flags have been seen on the argv side so a second\n\t// occurrence trips a usage error before the value silently overwrites\n\t// the first. Env-sourced defaults are NOT counted (they are not user\n\t// argv).\n\tconst seenArgvFlags = new Set<string>();\n\tfor (let i = 0; i < argv.length; i += 1) {\n\t\tconst token = argv[i]!;\n\t\tconst readValue = (name: string): string | undefined => {\n\t\t\tlet value: string | undefined;\n\t\t\t// `--name=value` form: trust the literal between `=` and end.\n\t\t\tif (token.startsWith(`--${name}=`)) {\n\t\t\t\tvalue = token.slice(name.length + 3);\n\t\t\t}\n\t\t\t// `--name value` form: peek the next token. Reject another\n\t\t\t// flag token (`--foo`) as the value — it almost certainly\n\t\t\t// means the user meant `--name <empty>` (typo / forgotten\n\t\t\t// argument) and quietly absorbing `--foo` as the value\n\t\t\t// silently demotes a downstream flag.\n\t\t\telse if (token === `--${name}`) {\n\t\t\t\tconst next = argv[i + 1];\n\t\t\t\tif (next === undefined) {\n\t\t\t\t\tthrow new CliUsageError({ message: `flag --${name} requires a value` });\n\t\t\t\t}\n\t\t\t\tif (next.startsWith('--')) {\n\t\t\t\t\tthrow new CliUsageError({\n\t\t\t\t\t\tmessage: `flag --${name} requires a value; got \"${next}\" which looks like a flag`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tvalue = next;\n\t\t\t}\n\t\t\tif (value === undefined) return undefined;\n\t\t\tif (seenArgvFlags.has(name)) {\n\t\t\t\tthrow new CliUsageError({ message: `flag --${name} given more than once` });\n\t\t\t}\n\t\t\tseenArgvFlags.add(name);\n\t\t\treturn value;\n\t\t};\n\t\tapp = readValue('app') ?? app;\n\t\tstack = readValue('stack') ?? stack;\n\t\tnetwork = readValue('network') ?? network;\n\t\tstateDir = readValue('state-dir') ?? stateDir;\n\t\tconfigPath = readValue('config') ?? configPath;\n\t}\n\treturn { app, stack, network, stateDir, configPath };\n};\n\nconst identityCwdFromConfig = (configPath: string | undefined): string => {\n\tconst resolved = resolveConfigPath(configPath);\n\treturn resolved === null ? process.cwd() : dirname(resolved);\n};\n\n/** Whether the resolved argv is a purely-informational invocation that\n * never consumes `identity.runtimeRoot` (the only value the config\n * pre-load feeds). For these we SKIP `configStateDirBestEffort` so we\n * don't dynamic-import (and run the top-level side effects of) the\n * user's `devstack.config.ts` — plus walk parent dirs — for commands\n * the file header itself says must stay cheap:\n *\n * - empty argv → Stricli prints root help\n * - `--help`/`-h` or `--version`/`-v` anywhere → Stricli short-circuits\n * - first token `schema` → emits the static CLI schema (no state dir)\n *\n * An unknown verb also never reaches a state-dir consumer (Stricli\n * fails the parse), but we keep pre-loading on the general path: the\n * pre-parser already accepted the identity flags, and gating on a\n * closed verb set here would duplicate the dispatcher's route table.\n * The two flagged-as-wasteful informational paths (help/version/schema)\n * are the ones with a registered short-circuit, so skipping exactly\n * those removes the eager-import surprise without re-deriving routes. */\nconst argvSkipsConfigPreload = (argv: ReadonlyArray<string>): boolean => {\n\tif (argv.length === 0) return true;\n\tfor (const token of argv) {\n\t\tif (token === '--help' || token === '-h' || token === '--version' || token === '-v') {\n\t\t\treturn true;\n\t\t}\n\t}\n\tconst firstVerb = argv.find((token) => !token.startsWith('-'));\n\treturn firstVerb === 'schema';\n};\n\n/** Best-effort read of `config.options.stateDir` (the value a program\n * sets via `defineDevstack({ stateDir })`) for the state-dir precedence\n * ladder in `resolveIdentity`. Swallows EVERY config-loader failure\n * (not-found AND evaluation errors) and returns `undefined`:\n *\n * - No-config verbs (`prune`, `wipe`) MUST keep resolving identity\n * without a config, so a missing config silently falls through to\n * the flag > env > cwd ladder.\n * - A genuinely malformed config is NOT surfaced here — the verbs that\n * actually consume config (`up` / `apply` via `makeConfigLoader`)\n * re-load it and surface the typed `CliConfig*` error through the\n * normal envelope path, so behavior for those verbs is unchanged.\n *\n * This is one redundant config evaluation for `up` / `apply` (the verb\n * wiring re-loads via its own loader); threading the loaded value into\n * the verb dispatch would require touching the off-limits `wirings/up.ts`\n * signature, so the duplicate import is accepted deliberately. */\nconst configStateDirBestEffort = async (\n\tconfigPath: string | undefined,\n): Promise<string | undefined> => {\n\tconst loaded = await Effect.runPromise(makeConfigLoader().load(configPath).pipe(Effect.option));\n\tif (loaded._tag === 'None') return undefined;\n\tconst options = (loaded.value.stack as { readonly options?: { readonly stateDir?: string } })\n\t\t.options;\n\tconst stateDir = options?.stateDir;\n\treturn stateDir !== undefined && stateDir.length > 0 ? stateDir : undefined;\n};\n\n// -----------------------------------------------------------------------------\n// Bin entry\n// -----------------------------------------------------------------------------\n\nexport const runCli = async (\n\targv: ReadonlyArray<string> = process.argv.slice(2),\n): Promise<void> => {\n\tconst stdinIsTty = Boolean((process.stdin as { isTTY?: boolean }).isTTY);\n\tconst env: Record<string, string | undefined> = { ...process.env };\n\tlet identityInputs: ReturnType<typeof identityInputsFromArgv>;\n\ttry {\n\t\tidentityInputs = identityInputsFromArgv(argv, env);\n\t} catch (cause) {\n\t\tconst error =\n\t\t\tcause instanceof CliUsageError\n\t\t\t\t? cause\n\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(parse-argv)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\t// Best-effort config pre-load for the state-dir ladder. Swallows\n\t// not-found / malformed configs so no-config verbs (prune/wipe) keep\n\t// working; the `--state-dir` flag still wins over the config value.\n\t// Skipped for informational invocations (help/version/schema/empty)\n\t// that never read `identity.runtimeRoot`, so those commands don't\n\t// dynamic-import the user's config (and run its top-level side\n\t// effects) just to print help.\n\tconst configStateDir = argvSkipsConfigPreload(argv)\n\t\t? undefined\n\t\t: await configStateDirBestEffort(identityInputs.configPath);\n\tlet identity: ResolvedIdentity;\n\ttry {\n\t\tidentity = resolveIdentity({\n\t\t\tapp: identityInputs.app,\n\t\t\tstack: identityInputs.stack,\n\t\t\tnetwork: identityInputs.network,\n\t\t\tstateDir: identityInputs.stateDir,\n\t\t\tconfigStateDir,\n\t\t\tcwd: identityCwdFromConfig(identityInputs.configPath),\n\t\t});\n\t} catch (cause) {\n\t\t// `resolveIdentity` -> `resolveNetworkSync` throws\n\t\t// `DevstackNetworkParseError` (a plain Error, NOT a CliError) on a\n\t\t// malformed `--network`/`$DEVSTACK_NETWORK` value. This runs OUTSIDE\n\t\t// the argv pre-parse try/catch above and BEFORE dispatch, so without\n\t\t// this guard the throw escapes to the bin entry's generic `.catch`\n\t\t// (exit 1, no envelope). Convert to `CliUsageError` and route through\n\t\t// the same envelope path the parse-argv block uses so a bad value\n\t\t// exits USAGE (64) with a JSON envelope in `--json` mode — never the\n\t\t// disallowed generic exit 1.\n\t\tconst error =\n\t\t\tcause instanceof DevstackNetworkParseError\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: cause instanceof CliUsageError\n\t\t\t\t\t? cause\n\t\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(resolve-identity)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tconst deps = buildDirectDeps(identity);\n\tawait Effect.runPromise(\n\t\tdispatch(deps, {\n\t\t\targv,\n\t\t\tenv: {\n\t\t\t\t...env,\n\t\t\t\tDEVSTACK_APP: identity.app,\n\t\t\t\tDEVSTACK_STACK: identity.stack,\n\t\t\t\tDEVSTACK_STATE_DIR: identity.runtimeRoot,\n\t\t\t},\n\t\t\tstdinIsTty,\n\t\t}),\n\t);\n};\n\nconst isMainEntrypoint = (): boolean => {\n\tconst argvPath = process.argv[1];\n\tif (argvPath === undefined) return false;\n\ttry {\n\t\treturn realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url));\n\t} catch {\n\t\treturn import.meta.url === pathToFileURL(argvPath).href;\n\t}\n};\n\nif (isMainEntrypoint()) {\n\t// Intentionally do NOT call `process.exit(...)` after `runCli`:\n\t// `process.exit` synchronously terminates the event loop before any\n\t// pending `setImmediate` work flushes. The `up` lifecycle's hard-kill\n\t// path schedules its escalation via `setImmediate(process.exit)` in\n\t// `up-lifecycle.ts:scheduleProcessExit` (the file-header invariant for\n\t// SIGINT → finalizers requires the outer Node fiber to drain\n\t// naturally). Letting Node's natural exit handle the shutdown\n\t// preserves that invariant. We only set `process.exitCode` so the\n\t// final OS exit code reflects the verb's outcome.\n\trunCli().catch((err) => {\n\t\tprocess.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n\t\tprocess.exitCode = ExitCode.GENERIC;\n\t});\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"main.mjs","names":["resolvePath"],"sources":["../../src/cli/main.ts"],"sourcesContent":["// devstack CLI bin entry — argv → identity → deps → dispatch → exit.\n//\n// This file is the executable entry the `bin: { devstack: ... }`\n// package.json field points at. Build output: `dist/cli/main.mjs`.\n//\n// Architecture invariant (surfaces/cli/index.ts header):\n// \"`up` must hand its long-running effect to the outer Node runtime\n// directly, not nest a runtime — otherwise SIGINT cannot reach\n// scope finalizers and container teardown leaks.\"\n//\n// Shape:\n// 1. Pre-parse identity inputs from argv + env (see\n// `identityInputsFromArgv`).\n// 2. Resolve identity (app / stack / network / runtime root) via the\n// shared `api/inference-network` helpers.\n// 3. Build the dispatcher's `CliDeps` bundle from each verb's wiring\n// module in `cli/wirings/`.\n// 4. Hand control to the Stricli-backed dispatcher, which routes to\n// the verb-scoped wiring function.\n\nimport { realpathSync } from 'node:fs';\nimport { dirname, join, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\nimport { Effect } from 'effect';\n\nimport { endpointKey, pluginKey } from '../substrate/brand.ts';\nimport { emptyProjection } from '../substrate/runtime/projection/state-ref.ts';\nimport type { SubscribableState } from '../substrate/projection.ts';\nimport { readStackContext } from '../build-integrations/runtime/read-stack-context.ts';\nimport type { StackContext } from '../build-integrations/runtime/stack-context.ts';\nimport { dispatch, type CliDeps, CliUsageError } from '../surfaces/cli/index.ts';\nimport { emitFailure, nodeProcessIO } from '../surfaces/cli/output.ts';\nimport { ENV_VARS } from '../surfaces/cli/flags.ts';\nimport { defaultProbes } from './doctor-probes.ts';\nimport { nodeConfirmPrompt } from '../surfaces/cli/commands/confirm-node.ts';\nimport type { StatusReader } from '../surfaces/cli/commands/status.ts';\nimport { ExitCode } from '../surfaces/cli/sysexits.ts';\nimport { makeSnapshotReader } from './snapshot-reader.ts';\nimport {\n\tDevstackNetworkParseError,\n\tresolveAppName,\n\tresolveNetworkSync,\n\tresolveStackName,\n\tresolveStateDir,\n} from '../api/inference-network.ts';\nimport { makeDirectPruneDeps } from './prune-direct.ts';\nimport { runUpLive } from './wirings/up.ts';\nimport { runApplyLive } from './wirings/apply.ts';\nimport { runCodegen } from './wirings/codegen.ts';\nimport { runDumpIds } from './wirings/dump-ids.ts';\nimport {\n\trunSnapshotCaptureLiveAware,\n\trunSnapshotDeleteDirect,\n\trunSnapshotRestoreDirect,\n} from './wirings/snapshot.ts';\nimport { runWipeDirect, runWipePlanDirect } from './wirings/wipe.ts';\nimport { makeConfigLoader, resolveConfigPath } from './wirings/config-loader.ts';\nimport type { ResolvedIdentity } from './wirings/identity.ts';\n\n// -----------------------------------------------------------------------------\n// Identity resolution\n// -----------------------------------------------------------------------------\n\n/** Resolve identity from flags + env. App and stack fall through the\n * shared cwd/package metadata resolver before their defaults.\n *\n * State-dir precedence ladder:\n * `--state-dir` flag (`stateDir`) > `config.options.stateDir`\n * (`configStateDir`) > `$DEVSTACK_STATE_DIR` > `<cwd>/.devstack`.\n * The flag maps to `resolveStateDir`'s top `runtimeRoot` rung and the\n * config value to its `stateDir` rung so the flag always wins over a\n * config-declared `defineDevstack({ stateDir })`.\n *\n * Stack-name precedence ladder (the SAME flag-beats-config shape as\n * state-dir, so the two are consistent and reviewable side-by-side):\n * `--stack` flag / `$DEVSTACK_STACK` (`params.stack`)\n * > config's `defineDevstack({ stackName })`\n * > cwd/package inference (default `'main'`).\n * Config-vs-explicit resolution can't happen here (the config isn't\n * loaded yet for the no-config verbs), so we record the explicit value\n * on `ResolvedIdentity.explicitStack` and let the verb wirings apply\n * `config.stackName` ONLY when no explicit stack was given — see\n * `effectiveStackName` in `cli/wirings/identity.ts`. `params.stack` is\n * exactly the explicit `--stack`-or-`$DEVSTACK_STACK` value (the argv\n * pre-parser seeds it from env, then lets the flag overwrite it), and\n * is `undefined` when neither was provided. */\nconst resolveIdentity = (params: {\n\treadonly app: string | undefined;\n\treadonly stack: string | undefined;\n\treadonly network: string | undefined;\n\treadonly stateDir: string | undefined;\n\treadonly configStateDir?: string | undefined;\n\treadonly cwd?: string;\n}): ResolvedIdentity => {\n\tconst cwd = params.cwd ?? process.cwd();\n\tconst app = resolveAppName({\n\t\texplicit: params.app,\n\t\tcwd,\n\t});\n\tconst runtimeRoot = resolveStateDir({\n\t\truntimeRoot: params.stateDir,\n\t\tstateDir: params.configStateDir,\n\t\tenv: process.env.DEVSTACK_STATE_DIR,\n\t\tcwd,\n\t});\n\tconst stacksRoot = resolvePath(runtimeRoot, 'stacks');\n\t// `resolveStackName` here folds explicit (flag/env) > inferred >\n\t// default. Crucially it does NOT see `config.stackName`: a non-empty\n\t// `params.stack` means the operator was explicit, so `stack` already\n\t// equals the explicit value and `config.stackName` must not override\n\t// it downstream. An empty `params.stack` falls through to the\n\t// inferred/default name, which a verb's `config.stackName` may then\n\t// supersede via `effectiveStackName`.\n\tconst explicitStack =\n\t\tparams.stack !== undefined && params.stack.length > 0 ? params.stack : undefined;\n\tconst stack = resolveStackName({\n\t\texplicit: params.stack,\n\t\tcwd,\n\t});\n\t// Centralized explicit > env > default ladder. Throws\n\t// `DevstackNetworkParseError` on a malformed value so the CLI fails\n\t// fast with a structured error instead of a downstream cryptic\n\t// network-probe failure. The identity's network is the canonical name\n\t// (`localnet`, `testnet`, …), independent of how it was spelled.\n\tconst network = resolveNetworkSync({\n\t\texplicit: params.network,\n\t\tenv: process.env.DEVSTACK_NETWORK,\n\t\texplicitSource: '--network',\n\t}).parsed.name;\n\tconst stackRoot = resolvePath(stacksRoot, stack);\n\treturn {\n\t\tapp,\n\t\tstack,\n\t\tnetwork,\n\t\truntimeRoot,\n\t\tstacksRoot,\n\t\tstackRoot,\n\t\trosterFile: resolvePath(stackRoot, 'roster.json'),\n\t\texplicitStack,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Verb deps composition\n// -----------------------------------------------------------------------------\n\n/**\n * Degraded offline-status projection — built from the on-disk manifest,\n * NOT from a persisted projection twin (the projection snapshot file no\n * longer exists). When the stack is DOWN, the manifest is the only\n * durable record of the boot; `status` surfaces identity + endpoints\n * from it and leaves the live-only slices (`rows` / `accounts` /\n * `packages` / `errors`) empty. A freshly-booted stack also has empty\n * `rows`, so the status renderer already tolerates this shape.\n *\n * Pure projector — exported for the focused unit test. Starts from\n * `emptyProjection()` (the closed-vocabulary baseline) and fills only\n * the two manifest-backed slices, so no display vocabulary can leak in.\n * The manifest identity tuple is `{ app, stack, network }`, matching\n * the projection's identity shape directly. Endpoints get re-branded\n * (`endpointKey` / `pluginKey`)\n * to the projection's branded shape; `registeredAt` is unknown offline,\n * so it defaults to `0`.\n */\nexport const degradedStatusFromContext = (ctx: StackContext): SubscribableState => ({\n\t...emptyProjection(),\n\tidentity: {\n\t\tapp: ctx.identity.app,\n\t\tstack: ctx.identity.stack,\n\t\tnetwork: ctx.identity.network,\n\t},\n\tendpoints: ctx.endpoints.all().map((endpoint) => ({\n\t\tendpointKey: endpointKey(endpoint.endpointKey),\n\t\tpluginKey: pluginKey(endpoint.pluginKey),\n\t\tname: endpoint.name,\n\t\turl: endpoint.url,\n\t\tdisplayUrl: endpoint.displayUrl,\n\t\twireProtocol: endpoint.wireProtocol,\n\t\tregisteredAt: 0,\n\t})),\n});\n\nconst projectionStatusReader = (identity: ResolvedIdentity): StatusReader => ({\n\treadState: () =>\n\t\tEffect.sync(() => {\n\t\t\t// Offline read: project the on-disk manifest for THIS stack root.\n\t\t\t// A missing / malformed manifest (no `up` yet, or a hand-edit)\n\t\t\t// is a tolerated \"no state present\" — `readStackContext` throws\n\t\t\t// in that case, so swallow the throw and report `null` (the\n\t\t\t// status command's contract for an absent stack).\n\t\t\ttry {\n\t\t\t\tconst ctx = readStackContext({ manifestPath: join(identity.stackRoot, 'manifest.json') });\n\t\t\t\treturn degradedStatusFromContext(ctx);\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n});\n\nconst buildDirectDeps = (identity: ResolvedIdentity): CliDeps => {\n\treturn {\n\t\tup: {\n\t\t\trun: (flags) =>\n\t\t\t\trunUpLive(flags.configPath, identity, {\n\t\t\t\t\trenderer: flags.renderer,\n\t\t\t\t\tstdoutIsTty: Boolean((process.stdout as { isTTY?: boolean }).isTTY),\n\t\t\t\t\tfromSnapshot: flags.fromSnapshot,\n\t\t\t\t\tsnapshotCache: flags.snapshotCache,\n\t\t\t\t\tsnapshotStalePolicy: flags.snapshotStalePolicy,\n\t\t\t\t}),\n\t\t},\n\t\tapply: {\n\t\t\trun: (flags) => runApplyLive(flags.configPath, identity),\n\t\t},\n\t\tcodegen: {\n\t\t\trun: (flags) => runCodegen(flags.configPath),\n\t\t},\n\t\tdumpIds: {\n\t\t\trun: (flags) =>\n\t\t\t\trunDumpIds(identity, {\n\t\t\t\t\tconfigPath: flags.configPath,\n\t\t\t\t\tout: flags.out,\n\t\t\t\t\tio: flags.io,\n\t\t\t\t\toutputMode: flags.outputMode,\n\t\t\t\t}),\n\t\t},\n\t\tstatus: { reader: projectionStatusReader(identity) },\n\t\tsnapshot: {\n\t\t\treader: makeSnapshotReader(identity),\n\t\t\tcapture: (args) => runSnapshotCaptureLiveAware(identity, args),\n\t\t\trestore: (snapshotId) => runSnapshotRestoreDirect(identity, snapshotId),\n\t\t\tdelete: (snapshotId) => runSnapshotDeleteDirect(identity, snapshotId),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t\tprune: makeDirectPruneDeps({ runtimeRoot: identity.runtimeRoot }),\n\t\tdoctor: {\n\t\t\tprobes: defaultProbes({\n\t\t\t\tstateDir: identity.runtimeRoot,\n\t\t\t\tappRoot: identity.stacksRoot,\n\t\t\t}),\n\t\t},\n\t\tconfig: { loader: makeConfigLoader() },\n\t\twipe: {\n\t\t\twipe: () => runWipeDirect(identity),\n\t\t\tplan: () => runWipePlanDirect(identity),\n\t\t\tconfirm: nodeConfirmPrompt,\n\t\t},\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Argv pre-parser\n// -----------------------------------------------------------------------------\n\n/** @internal Exported for tests. Resolves identity flag inputs from a\n * `--app <x>` / `--stack <x>` / `--network <x>` / `--state-dir <x>` /\n * `--config <x>` argv, falling back to `DEVSTACK_*` env vars. Throws\n * on a missing or flag-shaped value so a typo doesn't silently demote\n * a downstream flag, and on a duplicate flag (`--app a --app b`) so\n * the pre-parser does not silently last-write-wins a value that\n * Stricli will later reject outright. */\nexport const identityInputsFromArgv = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n) => {\n\tlet app = env.DEVSTACK_APP;\n\tlet stack = env.DEVSTACK_STACK;\n\tlet network = env.DEVSTACK_NETWORK;\n\t// `stateDir` intentionally captures the `--state-dir` flag ONLY. The\n\t// full ladder — `--state-dir` flag > `config.options.stateDir`\n\t// (`defineDevstack({ stateDir })`) > `$DEVSTACK_STATE_DIR` >\n\t// `<cwd>/.devstack` — is assembled at the `resolveStateDir(...)`\n\t// call-site in `resolveIdentity`: the flag wins (top `runtimeRoot`\n\t// rung), the best-effort config value sits below it (`stateDir`\n\t// rung), then env, then the cwd default. Keeping the pre-parser a\n\t// thin flag-extractor means config + env precedence live in one place.\n\tlet stateDir: string | undefined;\n\tlet configPath = env.DEVSTACK_CONFIG;\n\t// Tracks which flags have been seen on the argv side so a second\n\t// occurrence trips a usage error before the value silently overwrites\n\t// the first. Env-sourced defaults are NOT counted (they are not user\n\t// argv).\n\tconst seenArgvFlags = new Set<string>();\n\tfor (let i = 0; i < argv.length; i += 1) {\n\t\tconst token = argv[i]!;\n\t\tconst readValue = (name: string): string | undefined => {\n\t\t\tlet value: string | undefined;\n\t\t\t// `--name=value` form: trust the literal between `=` and end.\n\t\t\tif (token.startsWith(`--${name}=`)) {\n\t\t\t\tvalue = token.slice(name.length + 3);\n\t\t\t}\n\t\t\t// `--name value` form: peek the next token. Reject another\n\t\t\t// flag token (`--foo`) as the value — it almost certainly\n\t\t\t// means the user meant `--name <empty>` (typo / forgotten\n\t\t\t// argument) and quietly absorbing `--foo` as the value\n\t\t\t// silently demotes a downstream flag.\n\t\t\telse if (token === `--${name}`) {\n\t\t\t\tconst next = argv[i + 1];\n\t\t\t\tif (next === undefined) {\n\t\t\t\t\tthrow new CliUsageError({ message: `flag --${name} requires a value` });\n\t\t\t\t}\n\t\t\t\tif (next.startsWith('--')) {\n\t\t\t\t\tthrow new CliUsageError({\n\t\t\t\t\t\tmessage: `flag --${name} requires a value; got \"${next}\" which looks like a flag`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tvalue = next;\n\t\t\t}\n\t\t\tif (value === undefined) return undefined;\n\t\t\tif (seenArgvFlags.has(name)) {\n\t\t\t\tthrow new CliUsageError({ message: `flag --${name} given more than once` });\n\t\t\t}\n\t\t\tseenArgvFlags.add(name);\n\t\t\treturn value;\n\t\t};\n\t\tapp = readValue('app') ?? app;\n\t\tstack = readValue('stack') ?? stack;\n\t\tnetwork = readValue('network') ?? network;\n\t\tstateDir = readValue('state-dir') ?? stateDir;\n\t\tconfigPath = readValue('config') ?? configPath;\n\t}\n\treturn { app, stack, network, stateDir, configPath };\n};\n\nconst identityCwdFromConfig = (configPath: string | undefined): string => {\n\tconst resolved = resolveConfigPath(configPath);\n\treturn resolved === null ? process.cwd() : dirname(resolved);\n};\n\n/** Whether the resolved argv is a purely-informational invocation that\n * never consumes `identity.runtimeRoot` (the only value the config\n * pre-load feeds). For these we SKIP `configStateDirBestEffort` so we\n * don't dynamic-import (and run the top-level side effects of) the\n * user's `devstack.config.ts` — plus walk parent dirs — for commands\n * the file header itself says must stay cheap:\n *\n * - empty argv → Stricli prints root help\n * - `--help`/`-h` or `--version`/`-v` anywhere → Stricli short-circuits\n * - first token `schema` → emits the static CLI schema (no state dir)\n *\n * An unknown verb also never reaches a state-dir consumer (Stricli\n * fails the parse), but we keep pre-loading on the general path: the\n * pre-parser already accepted the identity flags, and gating on a\n * closed verb set here would duplicate the dispatcher's route table.\n * The two flagged-as-wasteful informational paths (help/version/schema)\n * are the ones with a registered short-circuit, so skipping exactly\n * those removes the eager-import surprise without re-deriving routes. */\nconst argvSkipsConfigPreload = (argv: ReadonlyArray<string>): boolean => {\n\tif (argv.length === 0) return true;\n\tfor (const token of argv) {\n\t\tif (token === '--help' || token === '-h' || token === '--version' || token === '-v') {\n\t\t\treturn true;\n\t\t}\n\t}\n\tconst firstVerb = argv.find((token) => !token.startsWith('-'));\n\treturn firstVerb === 'schema';\n};\n\n/** Best-effort read of `config.options.stateDir` (the value a program\n * sets via `defineDevstack({ stateDir })`) for the state-dir precedence\n * ladder in `resolveIdentity`. Swallows EVERY config-loader failure\n * (not-found AND evaluation errors) and returns `undefined`:\n *\n * - No-config verbs (`prune`, `wipe`) MUST keep resolving identity\n * without a config, so a missing config silently falls through to\n * the flag > env > cwd ladder.\n * - A genuinely malformed config is NOT surfaced here — the verbs that\n * actually consume config (`up` / `apply` via `makeConfigLoader`)\n * re-load it and surface the typed `CliConfig*` error through the\n * normal envelope path, so behavior for those verbs is unchanged.\n *\n * This is one redundant config evaluation for `up` / `apply` (the verb\n * wiring re-loads via its own loader); threading the loaded value into\n * the verb dispatch would require touching the off-limits `wirings/up.ts`\n * signature, so the duplicate import is accepted deliberately. */\nconst configStateDirBestEffort = async (\n\tconfigPath: string | undefined,\n): Promise<string | undefined> => {\n\tconst loaded = await Effect.runPromise(makeConfigLoader().load(configPath).pipe(Effect.option));\n\tif (loaded._tag === 'None') return undefined;\n\tconst options = (loaded.value.stack as { readonly options?: { readonly stateDir?: string } })\n\t\t.options;\n\tconst stateDir = options?.stateDir;\n\treturn stateDir !== undefined && stateDir.length > 0 ? stateDir : undefined;\n};\n\n// -----------------------------------------------------------------------------\n// Bin entry\n// -----------------------------------------------------------------------------\n\nexport const runCli = async (\n\targv: ReadonlyArray<string> = process.argv.slice(2),\n): Promise<void> => {\n\tconst stdinIsTty = Boolean((process.stdin as { isTTY?: boolean }).isTTY);\n\tconst env: Record<string, string | undefined> = { ...process.env };\n\tlet identityInputs: ReturnType<typeof identityInputsFromArgv>;\n\ttry {\n\t\tidentityInputs = identityInputsFromArgv(argv, env);\n\t} catch (cause) {\n\t\tconst error =\n\t\t\tcause instanceof CliUsageError\n\t\t\t\t? cause\n\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(parse-argv)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\t// Best-effort config pre-load for the state-dir ladder. Swallows\n\t// not-found / malformed configs so no-config verbs (prune/wipe) keep\n\t// working; the `--state-dir` flag still wins over the config value.\n\t// Skipped for informational invocations (help/version/schema/empty)\n\t// that never read `identity.runtimeRoot`, so those commands don't\n\t// dynamic-import the user's config (and run its top-level side\n\t// effects) just to print help.\n\tconst configStateDir = argvSkipsConfigPreload(argv)\n\t\t? undefined\n\t\t: await configStateDirBestEffort(identityInputs.configPath);\n\tlet identity: ResolvedIdentity;\n\ttry {\n\t\tidentity = resolveIdentity({\n\t\t\tapp: identityInputs.app,\n\t\t\tstack: identityInputs.stack,\n\t\t\tnetwork: identityInputs.network,\n\t\t\tstateDir: identityInputs.stateDir,\n\t\t\tconfigStateDir,\n\t\t\tcwd: identityCwdFromConfig(identityInputs.configPath),\n\t\t});\n\t} catch (cause) {\n\t\t// `resolveIdentity` -> `resolveNetworkSync` throws\n\t\t// `DevstackNetworkParseError` (a plain Error, NOT a CliError) on a\n\t\t// malformed `--network`/`$DEVSTACK_NETWORK` value. This runs OUTSIDE\n\t\t// the argv pre-parse try/catch above and BEFORE dispatch, so without\n\t\t// this guard the throw escapes to the bin entry's generic `.catch`\n\t\t// (exit 1, no envelope). Convert to `CliUsageError` and route through\n\t\t// the same envelope path the parse-argv block uses so a bad value\n\t\t// exits USAGE (64) with a JSON envelope in `--json` mode — never the\n\t\t// disallowed generic exit 1.\n\t\tconst error =\n\t\t\tcause instanceof DevstackNetworkParseError\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: cause instanceof CliUsageError\n\t\t\t\t\t? cause\n\t\t\t\t\t: new CliUsageError({\n\t\t\t\t\t\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t\t\t});\n\t\tconst jsonMode = env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(nodeProcessIO, jsonMode ? 'json' : 'human', {\n\t\t\t\tcommand: '(resolve-identity)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tconst deps = buildDirectDeps(identity);\n\tawait Effect.runPromise(\n\t\tdispatch(deps, {\n\t\t\targv,\n\t\t\tenv: {\n\t\t\t\t...env,\n\t\t\t\tDEVSTACK_APP: identity.app,\n\t\t\t\tDEVSTACK_STACK: identity.stack,\n\t\t\t\tDEVSTACK_STATE_DIR: identity.runtimeRoot,\n\t\t\t},\n\t\t\tstdinIsTty,\n\t\t}),\n\t);\n};\n\nconst isMainEntrypoint = (): boolean => {\n\tconst argvPath = process.argv[1];\n\tif (argvPath === undefined) return false;\n\ttry {\n\t\treturn realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url));\n\t} catch {\n\t\treturn import.meta.url === pathToFileURL(argvPath).href;\n\t}\n};\n\nif (isMainEntrypoint()) {\n\t// Intentionally do NOT call `process.exit(...)` after `runCli`:\n\t// `process.exit` synchronously terminates the event loop before any\n\t// pending `setImmediate` work flushes. The `up` lifecycle's hard-kill\n\t// path schedules its escalation via `setImmediate(process.exit)` in\n\t// `up-lifecycle.ts:scheduleProcessExit` (the file-header invariant for\n\t// SIGINT → finalizers requires the outer Node fiber to drain\n\t// naturally). Letting Node's natural exit handle the shutdown\n\t// preserves that invariant. We only set `process.exitCode` so the\n\t// final OS exit code reflects the verb's outcome.\n\trunCli().catch((err) => {\n\t\tprocess.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\\n`);\n\t\tprocess.exitCode = ExitCode.GENERIC;\n\t});\n}\n"],"mappings":"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"}
@@ -1,14 +1,12 @@
1
1
  import { layerMystenMoveCodegen } from "../../orchestrators/codegen/bindings.mjs";
2
- import { layerSuiMoveSummaryRunnerDocker, layerSuiMoveSummaryRunnerHost } from "../../plugins/sui/move-summary-runner.mjs";
2
+ import { layerSuiMoveSummaryRunnerHost } from "../../plugins/sui/move-summary-runner.mjs";
3
3
  import { layerCodegenPaths, layerCodegenRoot } from "../../orchestrators/codegen/paths.mjs";
4
4
  import { runEmitCycle } from "../../orchestrators/codegen/service.mjs";
5
- import { buildSubstrateLayers } from "../../orchestrators/boot.mjs";
6
5
  import { ExitCode } from "../../surfaces/cli/sysexits.mjs";
7
- import { CliInternalError } from "../../surfaces/cli/errors.mjs";
6
+ import { CliInternalError, CliUnavailableError } from "../../surfaces/cli/errors.mjs";
8
7
  import "../../surfaces/cli/commands/index.mjs";
9
8
  import { cliErrorFromConfigExit } from "../bail.mjs";
10
9
  import { makeConfigLoader } from "./config-loader.mjs";
11
- import { identityValueFor } from "./identity.mjs";
12
10
  import { Cause, Effect, Exit, Layer, Logger } from "effect";
13
11
  import { dirname, resolve } from "node:path";
14
12
  import { spawnSync } from "node:child_process";
@@ -29,26 +27,24 @@ const deriveContributions = (members) => {
29
27
  }
30
28
  return decls;
31
29
  };
32
- /** True if a `sui` binary is resolvable on PATH (cheap `--version` probe). */
33
- const hostSuiAvailable = () => {
30
+ /** Probe the host `sui` CLI with a cheap `--version` call. Returns the
31
+ * trimmed version line on success (e.g. `sui 1.39.0-abcdef0`), or `null`
32
+ * when no `sui` is resolvable on PATH (missing binary, non-zero exit, or
33
+ * spawn failure). The version is surfaced in codegen output so the build
34
+ * that produced the committed tree is visible. */
35
+ const probeHostSui = () => {
34
36
  try {
35
- return spawnSync("sui", ["--version"], {
37
+ const probe = spawnSync("sui", ["--version"], {
36
38
  timeout: 5e3,
37
- stdio: [
38
- "ignore",
39
- "ignore",
40
- "ignore"
41
- ]
42
- }).status === 0;
39
+ encoding: "utf8"
40
+ });
41
+ if (probe.status !== 0) return null;
42
+ const line = (probe.stdout ?? "").trim() || (probe.stderr ?? "").trim();
43
+ return line.length > 0 ? line : "sui (version unknown)";
43
44
  } catch {
44
- return false;
45
+ return null;
45
46
  }
46
47
  };
47
- const selectCodegenRunner = () => {
48
- const requested = process.env["DEVSTACK_CODEGEN_RUNNER"]?.trim().toLowerCase();
49
- if (requested === "docker" || requested === "host") return requested;
50
- return hostSuiAvailable() ? "host" : "docker";
51
- };
52
48
  /**
53
49
  * Build the codegen service layer: `CodegenPathsService` +
54
50
  * `MoveSummaryRunnerService` + `MoveCodegenService`, with the Node
@@ -57,15 +53,11 @@ const selectCodegenRunner = () => {
57
53
  * the services, with `Logger` supplied as a separate outer layer over the
58
54
  * program (`consolePretty` needs no FileSystem).
59
55
  *
60
- * The Move-summary runner is selected per `selectCodegenRunner`. The
61
- * `host` runner only needs a child-process spawner; the `docker` runner
62
- * needs the full Docker `ContainerRuntimeService`, so it provides the
63
- * substrate layer stack (`buildSubstrateLayers`) UNDER it. The Docker
64
- * runner produces byte-identical bindings to the host runner — it runs the
65
- * SAME `sui move summary` argv, just inside the pinned CLI image — so the
66
- * committed `src/generated` tree is independent of which runner emitted it.
56
+ * The Move-summary runner is the HOST runner — it invokes the `sui` CLI on
57
+ * PATH and only needs a child-process spawner (no Docker, no substrate
58
+ * layer stack). The caller guarantees `sui` is present (see `runCodegen`).
67
59
  */
68
- const buildCodegenLayer = (appRoot, runner, identity) => {
60
+ const buildCodegenLayer = (appRoot) => {
69
61
  const generatedDir = resolve(appRoot, "src", "generated");
70
62
  const platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
71
63
  const codegenPaths = layerCodegenPaths.pipe(Layer.provideMerge(layerCodegenRoot({
@@ -73,7 +65,7 @@ const buildCodegenLayer = (appRoot, runner, identity) => {
73
65
  stackSubdir: null,
74
66
  extrasDir: resolve(appRoot, "src", "generated-extras")
75
67
  })));
76
- const moveRunner = runner === "docker" ? layerSuiMoveSummaryRunnerDocker.pipe(Layer.provideMerge(buildSubstrateLayers(identityValueFor(identity), identity.runtimeRoot))) : layerSuiMoveSummaryRunnerHost.pipe(Layer.provideMerge(NodeChildProcessSpawner.layer));
68
+ const moveRunner = layerSuiMoveSummaryRunnerHost.pipe(Layer.provideMerge(NodeChildProcessSpawner.layer));
77
69
  return Layer.mergeAll(codegenPaths, moveRunner, layerMystenMoveCodegen()).pipe(Layer.provideMerge(platform));
78
70
  };
79
71
  /**
@@ -81,20 +73,28 @@ const buildCodegenLayer = (appRoot, runner, identity) => {
81
73
  * path. No stack boots; the committed `src/generated` tree is rewritten
82
74
  * deterministically from Move source.
83
75
  */
84
- const runCodegen = (configPath, identity) => {
76
+ const runCodegen = (configPath) => {
85
77
  const loader = makeConfigLoader();
86
78
  return Effect.gen(function* () {
79
+ const suiVersion = probeHostSui();
80
+ if (suiVersion === null) return yield* Effect.fail(new CliUnavailableError({
81
+ service: "sui",
82
+ message: "the Sui CLI (`sui`) is required for `devstack codegen` but was not found on PATH",
83
+ hint: "Install it — https://docs.sui.io/guides/developer/getting-started/sui-install — then re-run `devstack codegen`."
84
+ }));
87
85
  const loadExit = yield* Effect.exit(loader.load(configPath));
88
86
  if (Exit.isFailure(loadExit)) return yield* Effect.fail(cliErrorFromConfigExit(loadExit));
89
87
  const loaded = loadExit.value;
90
88
  const stack = loaded.engine;
91
89
  const appRoot = dirname(loaded.resolvedConfigPath);
92
90
  const contributions = deriveContributions(stack.members);
93
- const runner = selectCodegenRunner();
94
- const exit = yield* Effect.exit(runEmitCycle({
95
- contributions,
96
- trackTree: true
97
- }).pipe(Effect.provide(buildCodegenLayer(appRoot, runner, identity)), Effect.provide(Logger.layer([Logger.consolePretty()]))));
91
+ const exit = yield* Effect.exit(Effect.gen(function* () {
92
+ yield* Effect.logInfo(`codegen: using host ${suiVersion}`);
93
+ return yield* runEmitCycle({
94
+ contributions,
95
+ trackTree: true
96
+ });
97
+ }).pipe(Effect.provide(buildCodegenLayer(appRoot)), Effect.provide(Logger.layer([Logger.consolePretty()]))));
98
98
  if (Exit.isFailure(exit)) return yield* Effect.fail(new CliInternalError({
99
99
  message: "codegen failed",
100
100
  cause: Cause.pretty(exit.cause)
@@ -103,6 +103,6 @@ const runCodegen = (configPath, identity) => {
103
103
  });
104
104
  };
105
105
  //#endregion
106
- export { runCodegen };
106
+ export { deriveContributions, runCodegen };
107
107
 
108
108
  //# sourceMappingURL=codegen.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/codegen.ts"],"sourcesContent":["// `devstack codegen` verb wiring.\n//\n// Deterministic, STACK-FREE codegen: `(Move source) → generated`. It\n// boots NO stack — no supervisor, no acquire, no Docker. It loads the\n// config, derives each plugin's codegen contributions from config ALONE\n// (via the plugin spec's `staticCodegen` hook), and runs ONE\n// `runEmitCycle` that writes the committed `src/generated` tree.\n//\n// On-chain ids are LOADED CONFIG DATA, not generated output. The\n// committed tree carries the all-zero sentinel for every LOCAL id (and\n// declared literals for KNOWN packages); the committed `config.ts`\n// resolver throws loudly at runtime if a sentinel id is used. The Vite\n// plugin injects the real ids in dev (live `devstack-ids.json`) and prod\n// (a committed id-config file via the `ids` option / `DEVSTACK_IDS_FILE`).\n// So `git status` shows NO churn under\n// `src/generated` after a `devstack up` — ids land only in the\n// gitignored `.devstack/`.\n//\n// Logger layer: `Logger.consolePretty()` — mirrors `apply.ts` exactly.\n// `consolePretty` needs no FileSystem, so it is provided as a separate\n// OUTER layer over the codegen service layer (which provides the Node\n// FileSystem / Path / child-process spawner UNDER the codegen services).\n// This is the same provision shape `apply` uses and compiles cleanly\n// under the strict tsconfig.\n\nimport { spawnSync } from 'node:child_process';\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, Layer, Logger } from 'effect';\nimport * as NodeChildProcessSpawner from '@effect/platform-node/NodeChildProcessSpawner';\nimport * as NodeFileSystem from '@effect/platform-node/NodeFileSystem';\nimport * as NodePath from '@effect/platform-node/NodePath';\n\nimport type { CodegenableDecl } from '../../contracts/codegenable.ts';\nimport type { AnyPlugin } from '../../substrate/plugin.ts';\nimport type { SupervisedStack } from '../../substrate/runtime/index.ts';\nimport { layerMystenMoveCodegen } from '../../orchestrators/codegen/bindings.ts';\nimport {\n\tlayerSuiMoveSummaryRunnerDocker,\n\tlayerSuiMoveSummaryRunnerHost,\n} from '../../plugins/sui/move-summary-runner.ts';\nimport { buildSubstrateLayers } from '../../orchestrators/boot.ts';\nimport { layerCodegenPaths, layerCodegenRoot } from '../../orchestrators/codegen/paths.ts';\nimport { runEmitCycle } from '../../orchestrators/codegen/service.ts';\nimport { type CliError, CliInternalError } from '../../surfaces/cli/errors.ts';\nimport { type CommandResult } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeConfigLoader } from './config-loader.ts';\nimport { identityValueFor, type ResolvedIdentity } from './identity.ts';\n\n/** Walk the stack members, calling each plugin spec's `staticCodegen`\n * hook (skipping plugins that lack one) to derive the committed-tree\n * contributions from config alone. KNOWN package ids come from the\n * declared `networks` literals; LOCAL ids stay the all-zero sentinel\n * (resolved at app build/dev time through `__DEVSTACK_IDS__`). */\nconst deriveContributions = (members: ReadonlyArray<AnyPlugin>): ReadonlyArray<CodegenableDecl> => {\n\tconst decls: Array<CodegenableDecl> = [];\n\tfor (const member of members) {\n\t\tif (member.staticCodegen === undefined) continue;\n\t\tdecls.push(...member.staticCodegen());\n\t}\n\treturn decls;\n};\n\n/** Which Move-summary runner the stack-free codegen verb uses.\n *\n * - `host` — invoke a local `sui` CLI on PATH directly (fast, no Docker).\n * - `docker` — run `sui move summary` inside the pinned Sui CLI image\n * (the same runner `boot.ts` wires for `up`/`apply`). Needs Docker but\n * NO host `sui`, which is what CI has.\n *\n * Selection follows `DEVSTACK_CODEGEN_RUNNER` (`docker`/`host`); when unset,\n * auto-detect: use `host` if a `sui` binary is on PATH, else fall back to\n * `docker`. CI never installs `sui`, so it lands on `docker` automatically;\n * a workflow can still force it with `DEVSTACK_CODEGEN_RUNNER=docker`. */\ntype CodegenRunner = 'host' | 'docker';\n\n/** True if a `sui` binary is resolvable on PATH (cheap `--version` probe). */\nconst hostSuiAvailable = (): boolean => {\n\ttry {\n\t\tconst probe = spawnSync('sui', ['--version'], {\n\t\t\ttimeout: 5_000,\n\t\t\tstdio: ['ignore', 'ignore', 'ignore'],\n\t\t});\n\t\treturn probe.status === 0;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\nconst selectCodegenRunner = (): CodegenRunner => {\n\tconst requested = process.env['DEVSTACK_CODEGEN_RUNNER']?.trim().toLowerCase();\n\tif (requested === 'docker' || requested === 'host') return requested;\n\treturn hostSuiAvailable() ? 'host' : 'docker';\n};\n\n/**\n * Build the codegen service layer: `CodegenPathsService` +\n * `MoveSummaryRunnerService` + `MoveCodegenService`, with the Node\n * FileSystem / Path / child-process spawner provided UNDER them. Mirrors\n * `apply`'s substrate-layer provision shape — one merged layer providing\n * the services, with `Logger` supplied as a separate outer layer over the\n * program (`consolePretty` needs no FileSystem).\n *\n * The Move-summary runner is selected per `selectCodegenRunner`. The\n * `host` runner only needs a child-process spawner; the `docker` runner\n * needs the full Docker `ContainerRuntimeService`, so it provides the\n * substrate layer stack (`buildSubstrateLayers`) UNDER it. The Docker\n * runner produces byte-identical bindings to the host runner — it runs the\n * SAME `sui move summary` argv, just inside the pinned CLI image — so the\n * committed `src/generated` tree is independent of which runner emitted it.\n */\nconst buildCodegenLayer = (appRoot: string, runner: CodegenRunner, identity: ResolvedIdentity) => {\n\tconst generatedDir = resolvePath(appRoot, 'src', 'generated');\n\tconst platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);\n\tconst codegenPaths = layerCodegenPaths.pipe(\n\t\tLayer.provideMerge(\n\t\t\tlayerCodegenRoot({\n\t\t\t\toutputDir: generatedDir,\n\t\t\t\tstackSubdir: null,\n\t\t\t\t// Static contributions never target `generated-extras` (those\n\t\t\t\t// decls are dev-only and carry no `staticCodegen`), so this\n\t\t\t\t// path is declared but never written.\n\t\t\t\textrasDir: resolvePath(appRoot, 'src', 'generated-extras'),\n\t\t\t}),\n\t\t),\n\t);\n\tconst moveRunner =\n\t\trunner === 'docker'\n\t\t\t? layerSuiMoveSummaryRunnerDocker.pipe(\n\t\t\t\t\t// The Docker runner only needs `ContainerRuntimeService`; the\n\t\t\t\t\t// substrate layer stack provides it (along with the rest of L0).\n\t\t\t\t\t// No stack containers are created — only one-shot `sui move\n\t\t\t\t\t// summary` runs against the pinned CLI image.\n\t\t\t\t\tLayer.provideMerge(\n\t\t\t\t\t\tbuildSubstrateLayers(identityValueFor(identity), identity.runtimeRoot),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t: layerSuiMoveSummaryRunnerHost.pipe(Layer.provideMerge(NodeChildProcessSpawner.layer));\n\treturn Layer.mergeAll(codegenPaths, moveRunner, layerMystenMoveCodegen()).pipe(\n\t\tLayer.provideMerge(platform),\n\t);\n};\n\n/**\n * Run the stack-free codegen verb. `configPath` is the devstack config\n * path. No stack boots; the committed `src/generated` tree is rewritten\n * deterministically from Move source.\n */\nexport const runCodegen = (\n\tconfigPath: string | undefined,\n\tidentity: ResolvedIdentity,\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\tconst loadExit = yield* Effect.exit(loader.load(configPath));\n\t\tif (Exit.isFailure(loadExit)) {\n\t\t\treturn yield* Effect.fail(cliErrorFromConfigExit(loadExit));\n\t\t}\n\t\tconst loaded = loadExit.value;\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\n\t\tconst contributions = deriveContributions(stack.members);\n\t\tconst runner = selectCodegenRunner();\n\n\t\tconst exit = yield* Effect.exit(\n\t\t\trunEmitCycle({ contributions, trackTree: true }).pipe(\n\t\t\t\tEffect.provide(buildCodegenLayer(appRoot, runner, identity)),\n\t\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\t),\n\t\t);\n\t\tif (Exit.isFailure(exit)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliInternalError({\n\t\t\t\t\tmessage: 'codegen failed',\n\t\t\t\t\tcause: Cause.pretty(exit.cause as Cause.Cause<unknown>),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn { exitCode: ExitCode.OK };\n\t});\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA0DA,MAAM,uBAAuB,YAAsE;CAClG,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,OAAO,kBAAkB,KAAA,GAAW;EACxC,MAAM,KAAK,GAAG,OAAO,cAAc,CAAC;CACrC;CACA,OAAO;AACR;;AAgBA,MAAM,yBAAkC;CACvC,IAAI;EAKH,OAJc,UAAU,OAAO,CAAC,WAAW,GAAG;GAC7C,SAAS;GACT,OAAO;IAAC;IAAU;IAAU;GAAQ;EACrC,CACW,CAAC,CAAC,WAAW;CACzB,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,4BAA2C;CAChD,MAAM,YAAY,QAAQ,IAAI,0BAA0B,EAAE,KAAK,CAAC,CAAC,YAAY;CAC7E,IAAI,cAAc,YAAY,cAAc,QAAQ,OAAO;CAC3D,OAAO,iBAAiB,IAAI,SAAS;AACtC;;;;;;;;;;;;;;;;;AAkBA,MAAM,qBAAqB,SAAiB,QAAuB,aAA+B;CACjG,MAAM,eAAeA,QAAY,SAAS,OAAO,WAAW;CAC5D,MAAM,WAAW,MAAM,SAAS,eAAe,OAAO,SAAS,KAAK;CACpE,MAAM,eAAe,kBAAkB,KACtC,MAAM,aACL,iBAAiB;EAChB,WAAW;EACX,aAAa;EAIb,WAAWA,QAAY,SAAS,OAAO,kBAAkB;CAC1D,CAAC,CACF,CACD;CACA,MAAM,aACL,WAAW,WACR,gCAAgC,KAKhC,MAAM,aACL,qBAAqB,iBAAiB,QAAQ,GAAG,SAAS,WAAW,CACtE,CACD,IACC,8BAA8B,KAAK,MAAM,aAAa,wBAAwB,KAAK,CAAC;CACxF,OAAO,MAAM,SAAS,cAAc,YAAY,uBAAuB,CAAC,CAAC,CAAC,KACzE,MAAM,aAAa,QAAQ,CAC5B;AACD;;;;;;AAOA,MAAa,cACZ,YACA,aAC4C;CAC5C,MAAM,SAAS,iBAAiB;CAChC,OAAO,OAAO,IAAI,aAAa;EAC9B,MAAM,WAAW,OAAO,OAAO,KAAK,OAAO,KAAK,UAAU,CAAC;EAC3D,IAAI,KAAK,UAAU,QAAQ,GAC1B,OAAO,OAAO,OAAO,KAAK,uBAAuB,QAAQ,CAAC;EAE3D,MAAM,SAAS,SAAS;EACxB,MAAM,QAAS,OAA+D;EAC9E,MAAM,UAAU,QAAQ,OAAO,kBAAkB;EAEjD,MAAM,gBAAgB,oBAAoB,MAAM,OAAO;EACvD,MAAM,SAAS,oBAAoB;EAEnC,MAAM,OAAO,OAAO,OAAO,KAC1B,aAAa;GAAE;GAAe,WAAW;EAAK,CAAC,CAAC,CAAC,KAChD,OAAO,QAAQ,kBAAkB,SAAS,QAAQ,QAAQ,CAAC,GAC3D,OAAO,QAAQ,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CACtD,CACD;EACA,IAAI,KAAK,UAAU,IAAI,GACtB,OAAO,OAAO,OAAO,KACpB,IAAI,iBAAiB;GACpB,SAAS;GACT,OAAO,MAAM,OAAO,KAAK,KAA6B;EACvD,CAAC,CACF;EAED,OAAO,EAAE,UAAU,SAAS,GAAG;CAChC,CAAC;AACF"}
1
+ {"version":3,"file":"codegen.mjs","names":["resolvePath"],"sources":["../../../src/cli/wirings/codegen.ts"],"sourcesContent":["// `devstack codegen` verb wiring.\n//\n// Deterministic, STACK-FREE codegen: `(Move source) → generated`. It\n// boots NO stack and uses NO Docker — the `sui move summary` step runs\n// against a host `sui` CLI on PATH, which is REQUIRED: codegen fails fast\n// with an actionable error when `sui` is absent (rather than silently\n// pulling a Docker image). The detected `sui --version` is logged so the\n// build that produced the committed tree is visible; CI installs a pinned\n// `sui` and the drift-guard catches any version-skew in the output. It\n// loads the config, derives each plugin's codegen contributions from config\n// ALONE (via the plugin spec's `staticCodegen` hook), and runs ONE\n// `runEmitCycle` that writes the committed `src/generated` tree.\n//\n// On-chain ids are LOADED CONFIG DATA, not generated output. The\n// committed tree carries the all-zero sentinel for every LOCAL id (and\n// declared literals for KNOWN packages); the committed `config.ts`\n// resolver throws loudly at runtime if a sentinel id is used. The Vite\n// plugin injects the real ids in dev (live `devstack-ids.json`) and prod\n// (a committed id-config file via the `ids` option / `DEVSTACK_IDS_FILE`).\n// So `git status` shows NO churn under\n// `src/generated` after a `devstack up` — ids land only in the\n// gitignored `.devstack/`.\n//\n// Logger layer: `Logger.consolePretty()` — mirrors `apply.ts` exactly.\n// `consolePretty` needs no FileSystem, so it is provided as a separate\n// OUTER layer over the codegen service layer (which provides the Node\n// FileSystem / Path / child-process spawner UNDER the codegen services).\n// This is the same provision shape `apply` uses and compiles cleanly\n// under the strict tsconfig.\n\nimport { spawnSync } from 'node:child_process';\nimport { dirname, resolve as resolvePath } from 'node:path';\n\nimport { Cause, Effect, Exit, Layer, Logger } from 'effect';\nimport * as NodeChildProcessSpawner from '@effect/platform-node/NodeChildProcessSpawner';\nimport * as NodeFileSystem from '@effect/platform-node/NodeFileSystem';\nimport * as NodePath from '@effect/platform-node/NodePath';\n\nimport type { CodegenableDecl } from '../../contracts/codegenable.ts';\nimport type { AnyPlugin } from '../../substrate/plugin.ts';\nimport type { SupervisedStack } from '../../substrate/runtime/index.ts';\nimport { layerMystenMoveCodegen } from '../../orchestrators/codegen/bindings.ts';\nimport { layerSuiMoveSummaryRunnerHost } from '../../plugins/sui/move-summary-runner.ts';\nimport { layerCodegenPaths, layerCodegenRoot } from '../../orchestrators/codegen/paths.ts';\nimport { runEmitCycle } from '../../orchestrators/codegen/service.ts';\nimport { type CliError, CliInternalError, CliUnavailableError } from '../../surfaces/cli/errors.ts';\nimport { type CommandResult } from '../../surfaces/cli/commands/index.ts';\nimport { ExitCode } from '../../surfaces/cli/sysexits.ts';\nimport type { LoadedConfig } from '../../surfaces/cli/commands/config-loader.ts';\n\nimport { cliErrorFromConfigExit } from '../bail.ts';\nimport { makeConfigLoader } from './config-loader.ts';\n\n/** Walk the stack members, calling each plugin spec's `staticCodegen`\n * hook (skipping plugins that lack one) to derive the committed-tree\n * contributions from config alone. KNOWN package ids come from the\n * declared `networks` literals; LOCAL ids stay the all-zero sentinel\n * (resolved at app build/dev time through `__DEVSTACK_IDS__`). */\nexport const deriveContributions = (\n\tmembers: ReadonlyArray<AnyPlugin>,\n): ReadonlyArray<CodegenableDecl> => {\n\tconst decls: Array<CodegenableDecl> = [];\n\tfor (const member of members) {\n\t\tif (member.staticCodegen === undefined) continue;\n\t\tdecls.push(...member.staticCodegen());\n\t}\n\treturn decls;\n};\n\n/** Probe the host `sui` CLI with a cheap `--version` call. Returns the\n * trimmed version line on success (e.g. `sui 1.39.0-abcdef0`), or `null`\n * when no `sui` is resolvable on PATH (missing binary, non-zero exit, or\n * spawn failure). The version is surfaced in codegen output so the build\n * that produced the committed tree is visible. */\nconst probeHostSui = (): string | null => {\n\ttry {\n\t\tconst probe = spawnSync('sui', ['--version'], { timeout: 5_000, encoding: 'utf8' });\n\t\tif (probe.status !== 0) return null;\n\t\tconst line = (probe.stdout ?? '').trim() || (probe.stderr ?? '').trim();\n\t\treturn line.length > 0 ? line : 'sui (version unknown)';\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/**\n * Build the codegen service layer: `CodegenPathsService` +\n * `MoveSummaryRunnerService` + `MoveCodegenService`, with the Node\n * FileSystem / Path / child-process spawner provided UNDER them. Mirrors\n * `apply`'s substrate-layer provision shape — one merged layer providing\n * the services, with `Logger` supplied as a separate outer layer over the\n * program (`consolePretty` needs no FileSystem).\n *\n * The Move-summary runner is the HOST runner — it invokes the `sui` CLI on\n * PATH and only needs a child-process spawner (no Docker, no substrate\n * layer stack). The caller guarantees `sui` is present (see `runCodegen`).\n */\nconst buildCodegenLayer = (appRoot: string) => {\n\tconst generatedDir = resolvePath(appRoot, 'src', 'generated');\n\tconst platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);\n\tconst codegenPaths = layerCodegenPaths.pipe(\n\t\tLayer.provideMerge(\n\t\t\tlayerCodegenRoot({\n\t\t\t\toutputDir: generatedDir,\n\t\t\t\tstackSubdir: null,\n\t\t\t\t// Static contributions never target `generated-extras` (those\n\t\t\t\t// decls are dev-only and carry no `staticCodegen`), so this\n\t\t\t\t// path is declared but never written.\n\t\t\t\textrasDir: resolvePath(appRoot, 'src', 'generated-extras'),\n\t\t\t}),\n\t\t),\n\t);\n\tconst moveRunner = layerSuiMoveSummaryRunnerHost.pipe(\n\t\tLayer.provideMerge(NodeChildProcessSpawner.layer),\n\t);\n\treturn Layer.mergeAll(codegenPaths, moveRunner, layerMystenMoveCodegen()).pipe(\n\t\tLayer.provideMerge(platform),\n\t);\n};\n\n/**\n * Run the stack-free codegen verb. `configPath` is the devstack config\n * path. No stack boots; the committed `src/generated` tree is rewritten\n * deterministically from Move source.\n */\nexport const runCodegen = (\n\tconfigPath: string | undefined,\n): Effect.Effect<CommandResult, CliError> => {\n\tconst loader = makeConfigLoader();\n\treturn Effect.gen(function* () {\n\t\t// Host `sui` is REQUIRED — the `sui move summary` step has no Docker\n\t\t// fallback. Fail fast with an actionable hint when it is missing,\n\t\t// rather than emitting an empty/partial tree.\n\t\tconst suiVersion = probeHostSui();\n\t\tif (suiVersion === null) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliUnavailableError({\n\t\t\t\t\tservice: 'sui',\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t'the Sui CLI (`sui`) is required for `devstack codegen` but was not found on PATH',\n\t\t\t\t\thint: 'Install it — https://docs.sui.io/guides/developer/getting-started/sui-install — then re-run `devstack codegen`.',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst loadExit = yield* Effect.exit(loader.load(configPath));\n\t\tif (Exit.isFailure(loadExit)) {\n\t\t\treturn yield* Effect.fail(cliErrorFromConfigExit(loadExit));\n\t\t}\n\t\tconst loaded = loadExit.value;\n\t\tconst stack = (loaded as LoadedConfig & { readonly engine: SupervisedStack }).engine;\n\t\tconst appRoot = dirname(loaded.resolvedConfigPath);\n\n\t\tconst contributions = deriveContributions(stack.members);\n\n\t\tconst exit = yield* Effect.exit(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tyield* Effect.logInfo(`codegen: using host ${suiVersion}`);\n\t\t\t\treturn yield* runEmitCycle({ contributions, trackTree: true });\n\t\t\t}).pipe(\n\t\t\t\tEffect.provide(buildCodegenLayer(appRoot)),\n\t\t\t\tEffect.provide(Logger.layer([Logger.consolePretty()])),\n\t\t\t),\n\t\t);\n\t\tif (Exit.isFailure(exit)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew CliInternalError({\n\t\t\t\t\tmessage: 'codegen failed',\n\t\t\t\t\tcause: Cause.pretty(exit.cause as Cause.Cause<unknown>),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn { exitCode: ExitCode.OK };\n\t});\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0DA,MAAa,uBACZ,YACoC;CACpC,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,OAAO,kBAAkB,KAAA,GAAW;EACxC,MAAM,KAAK,GAAG,OAAO,cAAc,CAAC;CACrC;CACA,OAAO;AACR;;;;;;AAOA,MAAM,qBAAoC;CACzC,IAAI;EACH,MAAM,QAAQ,UAAU,OAAO,CAAC,WAAW,GAAG;GAAE,SAAS;GAAO,UAAU;EAAO,CAAC;EAClF,IAAI,MAAM,WAAW,GAAG,OAAO;EAC/B,MAAM,QAAQ,MAAM,UAAU,GAAA,CAAI,KAAK,MAAM,MAAM,UAAU,GAAA,CAAI,KAAK;EACtE,OAAO,KAAK,SAAS,IAAI,OAAO;CACjC,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;;;;;;AAcA,MAAM,qBAAqB,YAAoB;CAC9C,MAAM,eAAeA,QAAY,SAAS,OAAO,WAAW;CAC5D,MAAM,WAAW,MAAM,SAAS,eAAe,OAAO,SAAS,KAAK;CACpE,MAAM,eAAe,kBAAkB,KACtC,MAAM,aACL,iBAAiB;EAChB,WAAW;EACX,aAAa;EAIb,WAAWA,QAAY,SAAS,OAAO,kBAAkB;CAC1D,CAAC,CACF,CACD;CACA,MAAM,aAAa,8BAA8B,KAChD,MAAM,aAAa,wBAAwB,KAAK,CACjD;CACA,OAAO,MAAM,SAAS,cAAc,YAAY,uBAAuB,CAAC,CAAC,CAAC,KACzE,MAAM,aAAa,QAAQ,CAC5B;AACD;;;;;;AAOA,MAAa,cACZ,eAC4C;CAC5C,MAAM,SAAS,iBAAiB;CAChC,OAAO,OAAO,IAAI,aAAa;EAI9B,MAAM,aAAa,aAAa;EAChC,IAAI,eAAe,MAClB,OAAO,OAAO,OAAO,KACpB,IAAI,oBAAoB;GACvB,SAAS;GACT,SACC;GACD,MAAM;EACP,CAAC,CACF;EAGD,MAAM,WAAW,OAAO,OAAO,KAAK,OAAO,KAAK,UAAU,CAAC;EAC3D,IAAI,KAAK,UAAU,QAAQ,GAC1B,OAAO,OAAO,OAAO,KAAK,uBAAuB,QAAQ,CAAC;EAE3D,MAAM,SAAS,SAAS;EACxB,MAAM,QAAS,OAA+D;EAC9E,MAAM,UAAU,QAAQ,OAAO,kBAAkB;EAEjD,MAAM,gBAAgB,oBAAoB,MAAM,OAAO;EAEvD,MAAM,OAAO,OAAO,OAAO,KAC1B,OAAO,IAAI,aAAa;GACvB,OAAO,OAAO,QAAQ,uBAAuB,YAAY;GACzD,OAAO,OAAO,aAAa;IAAE;IAAe,WAAW;GAAK,CAAC;EAC9D,CAAC,CAAC,CAAC,KACF,OAAO,QAAQ,kBAAkB,OAAO,CAAC,GACzC,OAAO,QAAQ,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,CACtD,CACD;EACA,IAAI,KAAK,UAAU,IAAI,GACtB,OAAO,OAAO,OAAO,KACpB,IAAI,iBAAiB;GACpB,SAAS;GACT,OAAO,MAAM,OAAO,KAAK,KAA6B;EACvD,CAAC,CACF;EAED,OAAO,EAAE,UAAU,SAAS,GAAG;CAChC,CAAC;AACF"}