@mysten-incubation/devstack 0.6.0 → 0.7.1

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 (65) hide show
  1. package/dist/build-integrations/vite/index.mjs +1 -0
  2. package/dist/build-integrations/vite/index.mjs.map +1 -1
  3. package/dist/cli/wirings/apply.mjs +24 -6
  4. package/dist/cli/wirings/apply.mjs.map +1 -1
  5. package/dist/cli/wirings/dump-deployment.mjs +2 -1
  6. package/dist/cli/wirings/dump-deployment.mjs.map +1 -1
  7. package/dist/cli/wirings/identity.mjs +2 -1
  8. package/dist/cli/wirings/identity.mjs.map +1 -1
  9. package/dist/cli/wirings/snapshot.mjs +2 -1
  10. package/dist/cli/wirings/snapshot.mjs.map +1 -1
  11. package/dist/cli/wirings/up.mjs +25 -29
  12. package/dist/cli/wirings/up.mjs.map +1 -1
  13. package/dist/plugins/package/codegen.mjs +4 -4
  14. package/dist/plugins/package/codegen.mjs.map +1 -1
  15. package/dist/plugins/package/dep-resolution.mjs +19 -9
  16. package/dist/plugins/package/dep-resolution.mjs.map +1 -1
  17. package/dist/plugins/package/index.d.mts +3 -3
  18. package/dist/plugins/package/index.mjs +9 -5
  19. package/dist/plugins/package/index.mjs.map +1 -1
  20. package/dist/plugins/package/mode-known.mjs +2 -2
  21. package/dist/plugins/package/mode-known.mjs.map +1 -1
  22. package/dist/plugins/package/mode-local.mjs +4 -4
  23. package/dist/plugins/package/mode-local.mjs.map +1 -1
  24. package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
  25. package/dist/plugins/sui/index.d.mts +10 -10
  26. package/dist/plugins/sui/index.mjs.map +1 -1
  27. package/dist/plugins/sui/move/index.mjs +10 -6
  28. package/dist/plugins/sui/move/index.mjs.map +1 -1
  29. package/dist/plugins/wallet/codegen.d.mts +14 -10
  30. package/dist/plugins/wallet/codegen.mjs.map +1 -1
  31. package/dist/plugins/wallet/origin-policy.mjs +43 -3
  32. package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
  33. package/dist/plugins/wallet/service.mjs +9 -7
  34. package/dist/plugins/wallet/service.mjs.map +1 -1
  35. package/dist/plugins/walrus/client-services.mjs +13 -5
  36. package/dist/plugins/walrus/client-services.mjs.map +1 -1
  37. package/dist/plugins/walrus/codegen.d.mts +1 -0
  38. package/dist/plugins/walrus/codegen.mjs +1 -0
  39. package/dist/plugins/walrus/codegen.mjs.map +1 -1
  40. package/dist/plugins/walrus/errors.d.mts +1 -1
  41. package/dist/plugins/walrus/errors.mjs.map +1 -1
  42. package/dist/plugins/walrus/index.d.mts +10 -0
  43. package/dist/plugins/walrus/index.mjs +14 -3
  44. package/dist/plugins/walrus/index.mjs.map +1 -1
  45. package/dist/plugins/walrus/mode/known-deploy.d.mts +1 -0
  46. package/dist/plugins/walrus/mode/known-deploy.mjs +6 -2
  47. package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
  48. package/dist/plugins/walrus/mode/local-cluster.d.mts +6 -2
  49. package/dist/plugins/walrus/mode/local-cluster.mjs +10 -7
  50. package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
  51. package/dist/plugins/walrus/routable.mjs +27 -31
  52. package/dist/plugins/walrus/routable.mjs.map +1 -1
  53. package/dist/plugins/walrus/snapshot.mjs.map +1 -1
  54. package/dist/substrate/cross-process.mjs +2 -1
  55. package/dist/substrate/cross-process.mjs.map +1 -1
  56. package/dist/substrate/runtime/cross-process/roster.mjs +5 -2
  57. package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
  58. package/dist/surfaces/cli/commands/supervisor-presence.mjs +4 -2
  59. package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
  60. package/dist/surfaces/cli/errors.mjs +12 -2
  61. package/dist/surfaces/cli/errors.mjs.map +1 -1
  62. package/dist/surfaces/cli/index.mjs.map +1 -1
  63. package/images/walrus/Dockerfile +11 -7
  64. package/images/walrus/run-walrus-client-service.sh +41 -9
  65. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/plugins/walrus/snapshot.ts"],"sourcesContent":["// Walrus plugin — Snapshotable contribution.\n//\n// Distilled-doc reference (06-walrus.md §\"Persistence model\" +\n// §\"Hard requirements\" item 7):\n// - `runtime/walrus/<name>/deploy/` MUST ride the snapshot tar.\n// It holds storage-node private keys + per-node configs that\n// `walrus-deploy` wrote; without them, a cached \"walrus is already\n// deployed\" artifact cannot be honored on resume.\n// - Local cluster: N storage-node containers plus enabled release\n// publisher/aggregator service containers are declared as managed\n// containers; runtime adapter pauses then `docker commit`. Per-role\n// labels keep every committed layer distinct on restore.\n// - Known-deployment: no containers, no subtrees. The shape still\n// exists so the identity guard fires on restore.\n//\n// Identity guard: contributes the deploy mode + network + (when\n// local) the `name` discriminator to the pre-restore identity\n// record. A snapshot taken in local mode restored under known\n// mode (or vice versa) refuses BEFORE any destructive mutation.\n\nimport { Effect } from 'effect';\n\nimport type { ContainerLabelTuple, SnapshotableDecl } from '../../contracts/snapshotable.ts';\n\n/** Discriminator passed to `makeSnapshotable` — narrower than the\n * full mode union because the snapshot shape only cares about\n * \"local with containers\" vs \"known with nothing\". */\nexport type WalrusSnapshotMode = 'local' | 'known';\n\n/** Build the Snapshotable contribution.\n *\n * `app` / `stack` / `walrusName` are resolved at the plugin's\n * acquire-time so the snapshot's identity guard can match the\n * plugin's actual container labels.\n *\n * `nodeCount` (local mode only) drives the per-node managed-container\n * decls. Each storage node carries `role: storage-node-${i}` so the\n * substrate's label filter pauses/commits each independently. */\nexport const makeSnapshotable = (\n\tmode: WalrusSnapshotMode,\n\tapp: string,\n\tstack: string,\n\twalrusName: string,\n\tnetwork: string,\n\tnodeCount = 1,\n\tclientServiceRoles: ReadonlyArray<'aggregator' | 'publisher'> = [],\n): SnapshotableDecl => {\n\tconst labels = (role: string): ContainerLabelTuple => ({\n\t\tapp,\n\t\tstack,\n\t\tplugin: 'walrus',\n\t\trole,\n\t});\n\n\tswitch (mode) {\n\t\tcase 'local': {\n\t\t\tconst perNodeContainers: ReadonlyArray<ContainerLabelTuple> = Array.from(\n\t\t\t\t{ length: nodeCount },\n\t\t\t\t(_, i) => labels(`storage-node-${i}`),\n\t\t\t);\n\t\t\tconst serviceContainers = clientServiceRoles.map(labels);\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\t// `runtime/walrus/<name>/deploy/` — the deploy one-shot's\n\t\t\t\t// output dir. The substrate's runtime-dir root is\n\t\t\t\t// resolved at acquire; the path here is relative to it.\n\t\t\t\tsubtrees: [`walrus/${walrusName}/deploy/`],\n\t\t\t\tmanagedContainers: [...perNodeContainers, ...serviceContainers],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\t// Storage nodes need >10s to flush + checkpoint RocksDB\n\t\t\t\t// on `docker stop`; the snapshot quiesce mirrors the\n\t\t\t\t// stop grace (distilled-doc invariant 22). Default\n\t\t\t\t// \"pause container\" gives us this for free in the\n\t\t\t\t// substrate's adapter.\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'local' as const,\n\t\t\t\t\tname: walrusName,\n\t\t\t\t\tnodeCount,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t\tpostRestore: Effect.void,\n\t\t\t\t// Storage-node keystores are secret material; the\n\t\t\t\t// substrate preserves 0o600 on round-trip.\n\t\t\t\tsecretMaterial: true,\n\t\t\t};\n\t\t}\n\t\tcase 'known': {\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\tsubtrees: [],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'known' as const,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t}\n};\n"],"mappings":";;;;;;;;;;;AAsCA,MAAa,oBACZ,MACA,KACA,OACA,YACA,SACA,YAAY,GACZ,qBAAgE,CAAC,MAC3C;CACtB,MAAM,UAAU,UAAuC;EACtD;EACA;EACA,QAAQ;EACR;CACD;CAEA,QAAQ,MAAR;EACC,KAAK,SAAS;GACb,MAAM,oBAAwD,MAAM,KACnE,EAAE,QAAQ,UAAU,IACnB,GAAG,MAAM,OAAO,gBAAgB,GAAG,CACrC;GACA,MAAM,oBAAoB,mBAAmB,IAAI,MAAM;GACvD,OAAO;IACN,MAAM;IAIN,UAAU,CAAC,UAAU,WAAW,SAAS;IACzC,mBAAmB,CAAC,GAAG,mBAAmB,GAAG,iBAAiB;IAC9D,kBAAkB;IAMlB,YAAY,OAAO,QAAQ;KAC1B,MAAM;KACN,MAAM;KACN,MAAM;KACN;KACA;IACD,CAAC;IACD,aAAa,OAAO;IAGpB,gBAAgB;GACjB;EACD;EACA,KAAK,SACJ,OAAO;GACN,MAAM;GACN,UAAU,CAAC;GACX,kBAAkB;GAClB,YAAY,OAAO,QAAQ;IAC1B,MAAM;IACN,MAAM;IACN;GACD,CAAC;EACF;CAEF;AACD"}
1
+ {"version":3,"file":"snapshot.mjs","names":[],"sources":["../../../src/plugins/walrus/snapshot.ts"],"sourcesContent":["// Walrus plugin — Snapshotable contribution.\n//\n// Distilled-doc reference (06-walrus.md §\"Persistence model\" +\n// §\"Hard requirements\" item 7):\n// - `runtime/walrus/<name>/deploy/` MUST ride the snapshot tar.\n// It holds storage-node private keys + per-node configs that\n// `walrus-deploy` wrote; without them, a cached \"walrus is already\n// deployed\" artifact cannot be honored on resume.\n// - Local cluster: N storage-node containers plus enabled release\n// publisher/aggregator/upload-relay service containers are declared as managed\n// containers; runtime adapter pauses then `docker commit`. Per-role\n// labels keep every committed layer distinct on restore.\n// - Known-deployment: no containers, no subtrees. The shape still\n// exists so the identity guard fires on restore.\n//\n// Identity guard: contributes the deploy mode + network + (when\n// local) the `name` discriminator to the pre-restore identity\n// record. A snapshot taken in local mode restored under known\n// mode (or vice versa) refuses BEFORE any destructive mutation.\n\nimport { Effect } from 'effect';\n\nimport type { ContainerLabelTuple, SnapshotableDecl } from '../../contracts/snapshotable.ts';\nimport type { WalrusClientServiceRole } from './client-services.ts';\n\n/** Discriminator passed to `makeSnapshotable` — narrower than the\n * full mode union because the snapshot shape only cares about\n * \"local with containers\" vs \"known with nothing\". */\nexport type WalrusSnapshotMode = 'local' | 'known';\n\n/** Build the Snapshotable contribution.\n *\n * `app` / `stack` / `walrusName` are resolved at the plugin's\n * acquire-time so the snapshot's identity guard can match the\n * plugin's actual container labels.\n *\n * `nodeCount` (local mode only) drives the per-node managed-container\n * decls. Each storage node carries `role: storage-node-${i}` so the\n * substrate's label filter pauses/commits each independently. */\nexport const makeSnapshotable = (\n\tmode: WalrusSnapshotMode,\n\tapp: string,\n\tstack: string,\n\twalrusName: string,\n\tnetwork: string,\n\tnodeCount = 1,\n\tclientServiceRoles: ReadonlyArray<WalrusClientServiceRole> = [],\n): SnapshotableDecl => {\n\tconst labels = (role: string): ContainerLabelTuple => ({\n\t\tapp,\n\t\tstack,\n\t\tplugin: 'walrus',\n\t\trole,\n\t});\n\n\tswitch (mode) {\n\t\tcase 'local': {\n\t\t\tconst perNodeContainers: ReadonlyArray<ContainerLabelTuple> = Array.from(\n\t\t\t\t{ length: nodeCount },\n\t\t\t\t(_, i) => labels(`storage-node-${i}`),\n\t\t\t);\n\t\t\tconst serviceContainers = clientServiceRoles.map(labels);\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\t// `runtime/walrus/<name>/deploy/` — the deploy one-shot's\n\t\t\t\t// output dir. The substrate's runtime-dir root is\n\t\t\t\t// resolved at acquire; the path here is relative to it.\n\t\t\t\tsubtrees: [`walrus/${walrusName}/deploy/`],\n\t\t\t\tmanagedContainers: [...perNodeContainers, ...serviceContainers],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\t// Storage nodes need >10s to flush + checkpoint RocksDB\n\t\t\t\t// on `docker stop`; the snapshot quiesce mirrors the\n\t\t\t\t// stop grace (distilled-doc invariant 22). Default\n\t\t\t\t// \"pause container\" gives us this for free in the\n\t\t\t\t// substrate's adapter.\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'local' as const,\n\t\t\t\t\tname: walrusName,\n\t\t\t\t\tnodeCount,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t\tpostRestore: Effect.void,\n\t\t\t\t// Storage-node keystores are secret material; the\n\t\t\t\t// substrate preserves 0o600 on round-trip.\n\t\t\t\tsecretMaterial: true,\n\t\t\t};\n\t\t}\n\t\tcase 'known': {\n\t\t\treturn {\n\t\t\t\tkind: 'snapshotable',\n\t\t\t\tsubtrees: [],\n\t\t\t\tmissingTolerance: 'fine',\n\t\t\t\tpreRestore: Effect.succeed({\n\t\t\t\t\tkind: 'walrus' as const,\n\t\t\t\t\tmode: 'known' as const,\n\t\t\t\t\tnetwork,\n\t\t\t\t}),\n\t\t\t};\n\t\t}\n\t}\n};\n"],"mappings":";;;;;;;;;;;AAuCA,MAAa,oBACZ,MACA,KACA,OACA,YACA,SACA,YAAY,GACZ,qBAA6D,CAAC,MACxC;CACtB,MAAM,UAAU,UAAuC;EACtD;EACA;EACA,QAAQ;EACR;CACD;CAEA,QAAQ,MAAR;EACC,KAAK,SAAS;GACb,MAAM,oBAAwD,MAAM,KACnE,EAAE,QAAQ,UAAU,IACnB,GAAG,MAAM,OAAO,gBAAgB,GAAG,CACrC;GACA,MAAM,oBAAoB,mBAAmB,IAAI,MAAM;GACvD,OAAO;IACN,MAAM;IAIN,UAAU,CAAC,UAAU,WAAW,SAAS;IACzC,mBAAmB,CAAC,GAAG,mBAAmB,GAAG,iBAAiB;IAC9D,kBAAkB;IAMlB,YAAY,OAAO,QAAQ;KAC1B,MAAM;KACN,MAAM;KACN,MAAM;KACN;KACA;IACD,CAAC;IACD,aAAa,OAAO;IAGpB,gBAAgB;GACjB;EACD;EACA,KAAK,SACJ,OAAO;GACN,MAAM;GACN,UAAU,CAAC;GACX,kBAAkB;GAClB,YAAY,OAAO,QAAQ;IAC1B,MAAM;IACN,MAAM;IACN;GACD,CAAC;EACF;CAEF;AACD"}
@@ -7,7 +7,8 @@ const RosterHolderSchema = Schema.Struct({
7
7
  hostname: Schema.String,
8
8
  claimedAt: Schema.Number,
9
9
  heartbeatAt: Schema.Number,
10
- intent: Schema.Literals(["normal", "snapshot"])
10
+ intent: Schema.Literals(["normal", "snapshot"]),
11
+ graphInputId: Schema.optional(Schema.String)
11
12
  });
12
13
  const RosterDocumentSchema = versionedDocSchema(1, { holders: Schema.Array(RosterHolderSchema) });
13
14
  const DEFAULT_SWEEP_POLICY = {
@@ -1 +1 @@
1
- {"version":3,"file":"cross-process.mjs","names":[],"sources":["../../src/substrate/cross-process.ts"],"sourcesContent":["// Cross-process safety protocol.\n//\n// Architecture § Cross-process safety protocol. Core artifacts per\n// stack on disk under `<runtime-root>/stacks/<stack>/`:\n//\n// - `stack.lock` — OS-advisory exclusive lock; short critical\n// sections only.\n// - `roster.json` — authoritative cross-process record of holders;\n// mutated only under the lock.\n// - `commands.ndjson` / `events.ndjson` — filesystem command\n// channel between peer CLI commands and the live supervisor.\n//\n// The snapshot bounce holds `stack.lock` for the (bounded, whole-stack-\n// stopped) snapshot window — there is no separate snapshot-reservation file\n// (the lock subsumes the concurrency guard).\n\nimport { Schema } from 'effect';\n\nimport { versionedDocSchema } from './versioned-doc-schema.ts';\n\n/** Holder intent — `normal` for ordinary peers; `snapshot` while\n * the holder is mid-capture (peers' commands defer). */\nexport type HolderIntent = 'normal' | 'snapshot';\n\n/** One holder entry in `roster.json`. PID + startTime distinguish live\n * processes from PID reuse.\n *\n * `startTime` is `number | null` — `null` means \"the platform could\n * not probe a start-time stamp for this process at write time\" (an\n * exotic platform, or `ps`/`tasklist` failed). Readers MUST treat\n * `null` as the conservative branch (see `isOwnEntry` in `roster.ts`\n * and `checkHolderLiveness` in `liveness.ts`): on a null recorded\n * stamp the start-time comparison is skipped and the (pid, hostname)\n * pair carries the identity. */\nexport interface RosterHolder {\n\treadonly pid: number;\n\t/** Process start-time, used for PID-reuse-safe liveness. `null`\n\t * means \"unprobable\" — see the interface doc above. */\n\treadonly startTime: number | null;\n\treadonly hostname: string;\n\treadonly claimedAt: number;\n\treadonly heartbeatAt: number;\n\treadonly intent: HolderIntent;\n}\n\n/** Roster document schema — versioned for schema validation. */\nexport interface RosterDocument {\n\treadonly version: 1;\n\treadonly holders: ReadonlyArray<RosterHolder>;\n}\n\nexport const RosterHolderSchema = Schema.Struct({\n\tpid: Schema.Number,\n\tstartTime: Schema.NullOr(Schema.Number),\n\thostname: Schema.String,\n\tclaimedAt: Schema.Number,\n\theartbeatAt: Schema.Number,\n\tintent: Schema.Literals(['normal', 'snapshot']),\n});\n\nexport const RosterDocumentSchema = versionedDocSchema(1, {\n\tholders: Schema.Array(RosterHolderSchema),\n});\n\n/** Sweep policy — peers older than `staleAfterMillis` AND failing\n * the PID liveness check are evicted on the next claim under the\n * exclusive lock. */\nexport interface RosterSweepPolicy {\n\treadonly heartbeatIntervalMillis: number;\n\treadonly staleAfterMillis: number;\n}\n\nexport const DEFAULT_SWEEP_POLICY: RosterSweepPolicy = {\n\theartbeatIntervalMillis: 10_000,\n\tstaleAfterMillis: 30_000,\n};\n"],"mappings":";;;AAmDA,MAAa,qBAAqB,OAAO,OAAO;CAC/C,KAAK,OAAO;CACZ,WAAW,OAAO,OAAO,OAAO,MAAM;CACtC,UAAU,OAAO;CACjB,WAAW,OAAO;CAClB,aAAa,OAAO;CACpB,QAAQ,OAAO,SAAS,CAAC,UAAU,UAAU,CAAC;AAC/C,CAAC;AAED,MAAa,uBAAuB,mBAAmB,GAAG,EACzD,SAAS,OAAO,MAAM,kBAAkB,EACzC,CAAC;AAUD,MAAa,uBAA0C;CACtD,yBAAyB;CACzB,kBAAkB;AACnB"}
1
+ {"version":3,"file":"cross-process.mjs","names":[],"sources":["../../src/substrate/cross-process.ts"],"sourcesContent":["// Cross-process safety protocol.\n//\n// Architecture § Cross-process safety protocol. Core artifacts per\n// stack on disk under `<runtime-root>/stacks/<stack>/`:\n//\n// - `stack.lock` — OS-advisory exclusive lock; short critical\n// sections only.\n// - `roster.json` — authoritative cross-process record of holders;\n// mutated only under the lock.\n// - `commands.ndjson` / `events.ndjson` — filesystem command\n// channel between peer CLI commands and the live supervisor.\n//\n// The snapshot bounce holds `stack.lock` for the (bounded, whole-stack-\n// stopped) snapshot window — there is no separate snapshot-reservation file\n// (the lock subsumes the concurrency guard).\n\nimport { Schema } from 'effect';\n\nimport { versionedDocSchema } from './versioned-doc-schema.ts';\n\n/** Holder intent — `normal` for ordinary peers; `snapshot` while\n * the holder is mid-capture (peers' commands defer). */\nexport type HolderIntent = 'normal' | 'snapshot';\n\n/** One holder entry in `roster.json`. PID + startTime distinguish live\n * processes from PID reuse.\n *\n * `startTime` is `number | null` — `null` means \"the platform could\n * not probe a start-time stamp for this process at write time\" (an\n * exotic platform, or `ps`/`tasklist` failed). Readers MUST treat\n * `null` as the conservative branch (see `isOwnEntry` in `roster.ts`\n * and `checkHolderLiveness` in `liveness.ts`): on a null recorded\n * stamp the start-time comparison is skipped and the (pid, hostname)\n * pair carries the identity. */\nexport interface RosterHolder {\n\treadonly pid: number;\n\t/** Process start-time, used for PID-reuse-safe liveness. `null`\n\t * means \"unprobable\" — see the interface doc above. */\n\treadonly startTime: number | null;\n\treadonly hostname: string;\n\treadonly claimedAt: number;\n\treadonly heartbeatAt: number;\n\treadonly intent: HolderIntent;\n\t/** Optional desired-graph identity for a live supervisor. CLI peers use\n\t * this to refuse live apply/codegen refreshes against a supervisor booted\n\t * from a different stack graph. */\n\treadonly graphInputId?: string;\n}\n\n/** Roster document schema — versioned for schema validation. */\nexport interface RosterDocument {\n\treadonly version: 1;\n\treadonly holders: ReadonlyArray<RosterHolder>;\n}\n\nexport const RosterHolderSchema = Schema.Struct({\n\tpid: Schema.Number,\n\tstartTime: Schema.NullOr(Schema.Number),\n\thostname: Schema.String,\n\tclaimedAt: Schema.Number,\n\theartbeatAt: Schema.Number,\n\tintent: Schema.Literals(['normal', 'snapshot']),\n\tgraphInputId: Schema.optional(Schema.String),\n});\n\nexport const RosterDocumentSchema = versionedDocSchema(1, {\n\tholders: Schema.Array(RosterHolderSchema),\n});\n\n/** Sweep policy — peers older than `staleAfterMillis` AND failing\n * the PID liveness check are evicted on the next claim under the\n * exclusive lock. */\nexport interface RosterSweepPolicy {\n\treadonly heartbeatIntervalMillis: number;\n\treadonly staleAfterMillis: number;\n}\n\nexport const DEFAULT_SWEEP_POLICY: RosterSweepPolicy = {\n\theartbeatIntervalMillis: 10_000,\n\tstaleAfterMillis: 30_000,\n};\n"],"mappings":";;;AAuDA,MAAa,qBAAqB,OAAO,OAAO;CAC/C,KAAK,OAAO;CACZ,WAAW,OAAO,OAAO,OAAO,MAAM;CACtC,UAAU,OAAO;CACjB,WAAW,OAAO;CAClB,aAAa,OAAO;CACpB,QAAQ,OAAO,SAAS,CAAC,UAAU,UAAU,CAAC;CAC9C,cAAc,OAAO,SAAS,OAAO,MAAM;AAC5C,CAAC;AAED,MAAa,uBAAuB,mBAAmB,GAAG,EACzD,SAAS,OAAO,MAAM,kBAAkB,EACzC,CAAC;AAUD,MAAa,uBAA0C;CACtD,yBAAyB;CACzB,kBAAkB;AACnB"}
@@ -96,9 +96,12 @@ const withStackLock = (paths, body) => Effect.scoped(Effect.gen(function* () {
96
96
  *
97
97
  * Returns the swept document, our holder entry, and any evicted peers.
98
98
  */
99
- const claim = (paths, intent = "normal", policy = DEFAULT_SWEEP_POLICY) => withStackLock(paths, Effect.gen(function* () {
99
+ const claim = (paths, intent = "normal", policy = DEFAULT_SWEEP_POLICY, metadata = {}) => withStackLock(paths, Effect.gen(function* () {
100
100
  const { swept, evicted } = yield* sweepStaleHolders(yield* readRoster(paths.rosterFile).pipe(Effect.catchTag("RosterCorruptError", () => Effect.succeed(EMPTY_ROSTER))), policy);
101
- const self = { ...ownHolder(intent) };
101
+ const self = {
102
+ ...ownHolder(intent),
103
+ ...metadata
104
+ };
102
105
  const next = {
103
106
  version: 1,
104
107
  holders: [...swept.holders, self]
@@ -1 +1 @@
1
- {"version":3,"file":"roster.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/roster.ts"],"sourcesContent":["// `roster.json` — authoritative cross-process holder record.\n//\n// Architecture § Cross-process safety protocol § Roster:\n// \"the authoritative cross-process record of which OS processes are\n// currently 'in' this stack.\"\n//\n// Mutated only under `stack.lock`. Schema-validated on every read so a\n// truncated/forward-version write never corrupts a peer's view. Stale\n// entries reaped during step-3 sweep on the next claim.\n\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Data, Effect } from 'effect';\n\nimport {\n\tDEFAULT_SWEEP_POLICY,\n\ttype RosterDocument,\n\tRosterDocumentSchema,\n\ttype RosterHolder,\n\ttype RosterSweepPolicy,\n} from '../../cross-process.ts';\nimport { LogAttr } from '../observability/log-attrs.ts';\nimport { readVersionedDocumentSync, writeVersionedDocumentSync } from '../../versioned-doc-sync.ts';\nimport { selfPid } from './self-pid.ts';\nimport { acquireStackLock } from './stack-lock.ts';\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\townHolder,\n\tprocessStartTime,\n} from './liveness.ts';\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class RosterCorruptError extends Data.TaggedError('RosterCorruptError')<{\n\treadonly path: string;\n\treadonly raw: string;\n\treadonly cause: unknown;\n}> {}\n\nexport class RosterIoError extends Data.TaggedError('RosterIoError')<{\n\treadonly path: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type RosterError = RosterCorruptError | RosterIoError;\n\n// -----------------------------------------------------------------------------\n// Read / write\n// -----------------------------------------------------------------------------\n\nconst EMPTY_ROSTER: RosterDocument = { version: 1, holders: [] };\n\nconst ROSTER_DOC_ERRORS = {\n\tmkIo: ({ path, cause }: { path: string; cause: unknown }) => new RosterIoError({ path, cause }),\n\tmkCorrupt: ({ path, raw, cause }: { path: string; raw: string; cause: unknown }) =>\n\t\tnew RosterCorruptError({ path, raw, cause }),\n} as const;\n\n/** Read the roster from disk. Returns the empty document if absent.\n * Tolerates a missing file but NOT a malformed file (a malformed\n * roster surfaces a typed error so callers can decide whether to\n * abandon or rewrite). */\nexport const readRoster = (path: string): Effect.Effect<RosterDocument, RosterError> =>\n\treadVersionedDocumentSync(path, RosterDocumentSchema, ROSTER_DOC_ERRORS, EMPTY_ROSTER);\n\n/** Atomic write: route through the canonical sync primitive. The\n * roster's mutations are all under `stack.lock`, so the non-yielding\n * sync surface is correct here — and it shares ONE owner of the\n * tempfile dance with cache and manifest. */\nconst atomicWriteRoster = (path: string, doc: RosterDocument): Effect.Effect<void, RosterIoError> =>\n\twriteVersionedDocumentSync(path, doc, ROSTER_DOC_ERRORS);\n\n// -----------------------------------------------------------------------------\n// Sweep\n// -----------------------------------------------------------------------------\n\n/** Walk the roster's holders and drop those that fail the liveness\n * test AND whose heartbeats are older than `staleAfterMillis`.\n *\n * Architecture § Claim protocol step 3 — \"Holders whose\n * `heartbeatAt` is older than 3× the heartbeat interval AND who fail\n * the PID liveness check are evicted.\"\n *\n * A live holder with a stale heartbeat is NOT evicted — heartbeat\n * staleness alone is allowed (slow peer). The conjunction matters. */\nexport const sweepStaleHolders = Effect.fn('cross-process.roster.sweep')(function* (\n\tdoc: RosterDocument,\n\tpolicy: RosterSweepPolicy = DEFAULT_SWEEP_POLICY,\n\tnow: number = Date.now(),\n) {\n\t// Yield a fresh per-sweep liveness scope so the same pid is probed\n\t// AT MOST once across all holders in this pass — two holders sharing\n\t// a pid (corrupted-roster edge case) collapse to one fork. The\n\t// `Layer.provide` below scopes the cache to THIS sweep only.\n\tconst probe = yield* LivenessProbeScope;\n\tconst survivors: RosterHolder[] = [];\n\tconst evicted: RosterHolder[] = [];\n\tfor (const holder of doc.holders) {\n\t\tconst heartbeatStale = now - holder.heartbeatAt > policy.staleAfterMillis;\n\t\tif (!heartbeatStale) {\n\t\t\tsurvivors.push(holder);\n\t\t\tcontinue;\n\t\t}\n\t\tconst liveness = yield* probe\n\t\t\t.probeHolderLiveness(holder)\n\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\tif (liveness === 'dead') {\n\t\t\tevicted.push(holder);\n\t\t} else {\n\t\t\tsurvivors.push(holder);\n\t\t}\n\t}\n\treturn {\n\t\tswept: { version: doc.version, holders: survivors } satisfies RosterDocument,\n\t\tevicted: evicted as ReadonlyArray<RosterHolder>,\n\t};\n}, Effect.provide(layerLivenessProbeScope));\n\n// -----------------------------------------------------------------------------\n// Claim / release / heartbeat\n// -----------------------------------------------------------------------------\n\n/** Outcome of a claim: the holder this process registered, the swept\n * document, and whether this process is now the sole holder. */\nexport interface ClaimResult {\n\treadonly self: RosterHolder;\n\treadonly roster: RosterDocument;\n\treadonly evicted: ReadonlyArray<RosterHolder>;\n\treadonly soleHolder: boolean;\n}\n\n/** Outcome of a release: the swept document and whether this process\n * was the last leaver (no peers remain after removal). Architecture §\n * Release protocol step 4. */\nexport interface ReleaseResult {\n\treadonly roster: RosterDocument;\n\treadonly lastLeaver: boolean;\n}\n\ninterface RosterPaths {\n\treadonly stackLockFile: string;\n\treadonly rosterFile: string;\n}\n\n/** Match a roster holder against THIS process's identity.\n *\n * Liveness elsewhere (`checkHolderLiveness`) uses\n * `(pid, hostname, startTime)` — PID alone is insufficient on\n * long-uptime hosts where the kernel can recycle PIDs. The roster\n * mutators (`heartbeat`, `release`, `setIntent`) must apply the same\n * triple match so a recycled-PID peer's entry is never silently\n * overwritten/removed by this process.\n *\n * `startTime` is the FNV-1a hash of `ps -o lstart` (see\n * `liveness.processStartTime`). A `null` probe (process gone, or\n * exotic platform) skips the start-time check — the same conservative\n * policy `checkHolderLiveness` applies. */\nconst isOwnEntry = (\n\th: RosterHolder,\n\townPid: number,\n\townHost: string,\n\townStartTime: number | null,\n): boolean => {\n\tif (h.pid !== ownPid || h.hostname !== ownHost) return false;\n\t// Either side null → fall back to (pid, hostname). The roster's\n\t// own-entry test must symmetrically accept a null recorded stamp:\n\t// the writer's probe failed (exotic platform / transient `ps`\n\t// error) but the entry IS ours. Mismatching a probed `ownStartTime`\n\t// against a recorded `null` would orphan our own entry — peers\n\t// would then harvest it as \"dead\" on the next sweep.\n\tif (ownStartTime === null || h.startTime === null) return true;\n\treturn h.startTime === ownStartTime;\n};\n\nconst withStackLock = <A, E, R>(\n\tpaths: RosterPaths,\n\tbody: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E | import('./stack-lock.ts').StackLockError, R> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(paths.stackLockFile);\n\t\t\treturn yield* body;\n\t\t}),\n\t);\n\n/**\n * Claim protocol — architecture § Claim protocol.\n *\n * Under the exclusive lock:\n * 1. Read the roster (or initialize empty if missing).\n * 2. Sweep stale holders (PID liveness + heartbeat age).\n * 3. Append this process's entry.\n * 4. Atomic write the result.\n *\n * Returns the swept document, our holder entry, and any evicted peers.\n */\nexport const claim = (\n\tpaths: RosterPaths,\n\tintent: 'normal' | 'snapshot' = 'normal',\n\tpolicy: RosterSweepPolicy = DEFAULT_SWEEP_POLICY,\n): Effect.Effect<ClaimResult, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst initial = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst { swept, evicted } = yield* sweepStaleHolders(initial, policy);\n\t\t\tconst self: RosterHolder = { ...ownHolder(intent) };\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: [...swept.holders, self],\n\t\t\t};\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t\treturn {\n\t\t\t\tself,\n\t\t\t\troster: next,\n\t\t\t\tevicted,\n\t\t\t\tsoleHolder: swept.holders.length === 0,\n\t\t\t};\n\t\t}),\n\t);\n\n/**\n * Heartbeat protocol — refresh this process's `heartbeatAt`. Under the\n * stack lock to keep mutation atomic. Architecture § Heartbeat\n * protocol.\n */\nexport const heartbeat = (\n\tpaths: RosterPaths,\n\townPid: number = selfPid(),\n): Effect.Effect<void, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst now = Date.now();\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tlet touched = false;\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: current.holders.map((h) => {\n\t\t\t\t\tif (isOwnEntry(h, ownPid, ownHost, ownStartTime)) {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t\treturn { ...h, heartbeatAt: now };\n\t\t\t\t\t}\n\t\t\t\t\treturn h;\n\t\t\t\t}),\n\t\t\t};\n\t\t\tif (touched) yield* atomicWriteRoster(paths.rosterFile, next);\n\t\t}),\n\t);\n\n/**\n * Release protocol — architecture § Release protocol.\n *\n * Removes this process's holder. Returns whether this was the\n * last-leaver (caller runs the stop finalizer if so).\n */\nexport const release = (\n\tpaths: RosterPaths,\n\townPid: number = selfPid(),\n): Effect.Effect<ReleaseResult, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tconst remaining = current.holders.filter(\n\t\t\t\t(h) => !isOwnEntry(h, ownPid, ownHost, ownStartTime),\n\t\t\t);\n\t\t\tconst next: RosterDocument = { version: 1, holders: remaining };\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t\treturn {\n\t\t\t\troster: next,\n\t\t\t\tlastLeaver: remaining.length === 0,\n\t\t\t};\n\t\t}),\n\t);\n\n/**\n * Set this process's `intent` (`normal` ↔ `snapshot`) under the\n * exclusive lock. Architecture § Concurrent snapshot step 2 / 5.\n */\nexport const setIntent = (\n\tpaths: RosterPaths,\n\tintent: 'normal' | 'snapshot',\n\townPid: number = selfPid(),\n): Effect.Effect<void, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: current.holders.map((h) =>\n\t\t\t\t\tisOwnEntry(h, ownPid, ownHost, ownStartTime) ? { ...h, intent } : h,\n\t\t\t\t),\n\t\t\t};\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t}),\n\t);\n\n/** Background heartbeat fiber. Wakes every `intervalMillis` (default\n * matches `DEFAULT_SWEEP_POLICY.heartbeatIntervalMillis`) and refreshes\n * this process's `heartbeatAt`.\n *\n * Returns an Effect that runs forever in its Scope — the supervisor\n * forks it via `Effect.forkScoped` so it tears down with the stack. */\nexport const heartbeatFiber = (\n\tpaths: RosterPaths,\n\tintervalMillis: number = DEFAULT_SWEEP_POLICY.heartbeatIntervalMillis,\n): Effect.Effect<never> =>\n\tEffect.gen(function* () {\n\t\twhile (true) {\n\t\t\tyield* Effect.sleep(`${intervalMillis} millis`);\n\t\t\tyield* heartbeat(paths).pipe(\n\t\t\t\t// A heartbeat failure is non-fatal — the next peer's sweep\n\t\t\t\t// would otherwise evict us, but the architecture treats this\n\t\t\t\t// as \"the dev didn't get to flush a heartbeat in time;\n\t\t\t\t// recover by retrying next interval.\" Log via Effect's\n\t\t\t\t// logger; do not propagate.\n\t\t\t\tEffect.catch((err) =>\n\t\t\t\t\tEffect.logWarning('roster heartbeat failed').pipe(\n\t\t\t\t\t\tEffect.annotateLogs({\n\t\t\t\t\t\t\t[LogAttr.rosterHeartbeatIntervalMs]: intervalMillis,\n\t\t\t\t\t\t\t[LogAttr.errorCause]: String(err),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t});\n"],"mappings":"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"}
1
+ {"version":3,"file":"roster.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/roster.ts"],"sourcesContent":["// `roster.json` — authoritative cross-process holder record.\n//\n// Architecture § Cross-process safety protocol § Roster:\n// \"the authoritative cross-process record of which OS processes are\n// currently 'in' this stack.\"\n//\n// Mutated only under `stack.lock`. Schema-validated on every read so a\n// truncated/forward-version write never corrupts a peer's view. Stale\n// entries reaped during step-3 sweep on the next claim.\n\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Data, Effect } from 'effect';\n\nimport {\n\tDEFAULT_SWEEP_POLICY,\n\ttype RosterDocument,\n\tRosterDocumentSchema,\n\ttype RosterHolder,\n\ttype RosterSweepPolicy,\n} from '../../cross-process.ts';\nimport { LogAttr } from '../observability/log-attrs.ts';\nimport { readVersionedDocumentSync, writeVersionedDocumentSync } from '../../versioned-doc-sync.ts';\nimport { selfPid } from './self-pid.ts';\nimport { acquireStackLock } from './stack-lock.ts';\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\townHolder,\n\tprocessStartTime,\n} from './liveness.ts';\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class RosterCorruptError extends Data.TaggedError('RosterCorruptError')<{\n\treadonly path: string;\n\treadonly raw: string;\n\treadonly cause: unknown;\n}> {}\n\nexport class RosterIoError extends Data.TaggedError('RosterIoError')<{\n\treadonly path: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type RosterError = RosterCorruptError | RosterIoError;\n\n// -----------------------------------------------------------------------------\n// Read / write\n// -----------------------------------------------------------------------------\n\nconst EMPTY_ROSTER: RosterDocument = { version: 1, holders: [] };\n\nconst ROSTER_DOC_ERRORS = {\n\tmkIo: ({ path, cause }: { path: string; cause: unknown }) => new RosterIoError({ path, cause }),\n\tmkCorrupt: ({ path, raw, cause }: { path: string; raw: string; cause: unknown }) =>\n\t\tnew RosterCorruptError({ path, raw, cause }),\n} as const;\n\n/** Read the roster from disk. Returns the empty document if absent.\n * Tolerates a missing file but NOT a malformed file (a malformed\n * roster surfaces a typed error so callers can decide whether to\n * abandon or rewrite). */\nexport const readRoster = (path: string): Effect.Effect<RosterDocument, RosterError> =>\n\treadVersionedDocumentSync(path, RosterDocumentSchema, ROSTER_DOC_ERRORS, EMPTY_ROSTER);\n\n/** Atomic write: route through the canonical sync primitive. The\n * roster's mutations are all under `stack.lock`, so the non-yielding\n * sync surface is correct here — and it shares ONE owner of the\n * tempfile dance with cache and manifest. */\nconst atomicWriteRoster = (path: string, doc: RosterDocument): Effect.Effect<void, RosterIoError> =>\n\twriteVersionedDocumentSync(path, doc, ROSTER_DOC_ERRORS);\n\n// -----------------------------------------------------------------------------\n// Sweep\n// -----------------------------------------------------------------------------\n\n/** Walk the roster's holders and drop those that fail the liveness\n * test AND whose heartbeats are older than `staleAfterMillis`.\n *\n * Architecture § Claim protocol step 3 — \"Holders whose\n * `heartbeatAt` is older than 3× the heartbeat interval AND who fail\n * the PID liveness check are evicted.\"\n *\n * A live holder with a stale heartbeat is NOT evicted — heartbeat\n * staleness alone is allowed (slow peer). The conjunction matters. */\nexport const sweepStaleHolders = Effect.fn('cross-process.roster.sweep')(function* (\n\tdoc: RosterDocument,\n\tpolicy: RosterSweepPolicy = DEFAULT_SWEEP_POLICY,\n\tnow: number = Date.now(),\n) {\n\t// Yield a fresh per-sweep liveness scope so the same pid is probed\n\t// AT MOST once across all holders in this pass — two holders sharing\n\t// a pid (corrupted-roster edge case) collapse to one fork. The\n\t// `Layer.provide` below scopes the cache to THIS sweep only.\n\tconst probe = yield* LivenessProbeScope;\n\tconst survivors: RosterHolder[] = [];\n\tconst evicted: RosterHolder[] = [];\n\tfor (const holder of doc.holders) {\n\t\tconst heartbeatStale = now - holder.heartbeatAt > policy.staleAfterMillis;\n\t\tif (!heartbeatStale) {\n\t\t\tsurvivors.push(holder);\n\t\t\tcontinue;\n\t\t}\n\t\tconst liveness = yield* probe\n\t\t\t.probeHolderLiveness(holder)\n\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\tif (liveness === 'dead') {\n\t\t\tevicted.push(holder);\n\t\t} else {\n\t\t\tsurvivors.push(holder);\n\t\t}\n\t}\n\treturn {\n\t\tswept: { version: doc.version, holders: survivors } satisfies RosterDocument,\n\t\tevicted: evicted as ReadonlyArray<RosterHolder>,\n\t};\n}, Effect.provide(layerLivenessProbeScope));\n\n// -----------------------------------------------------------------------------\n// Claim / release / heartbeat\n// -----------------------------------------------------------------------------\n\n/** Outcome of a claim: the holder this process registered, the swept\n * document, and whether this process is now the sole holder. */\nexport interface ClaimResult {\n\treadonly self: RosterHolder;\n\treadonly roster: RosterDocument;\n\treadonly evicted: ReadonlyArray<RosterHolder>;\n\treadonly soleHolder: boolean;\n}\n\n/** Outcome of a release: the swept document and whether this process\n * was the last leaver (no peers remain after removal). Architecture §\n * Release protocol step 4. */\nexport interface ReleaseResult {\n\treadonly roster: RosterDocument;\n\treadonly lastLeaver: boolean;\n}\n\ninterface RosterPaths {\n\treadonly stackLockFile: string;\n\treadonly rosterFile: string;\n}\n\n/** Match a roster holder against THIS process's identity.\n *\n * Liveness elsewhere (`checkHolderLiveness`) uses\n * `(pid, hostname, startTime)` — PID alone is insufficient on\n * long-uptime hosts where the kernel can recycle PIDs. The roster\n * mutators (`heartbeat`, `release`, `setIntent`) must apply the same\n * triple match so a recycled-PID peer's entry is never silently\n * overwritten/removed by this process.\n *\n * `startTime` is the FNV-1a hash of `ps -o lstart` (see\n * `liveness.processStartTime`). A `null` probe (process gone, or\n * exotic platform) skips the start-time check — the same conservative\n * policy `checkHolderLiveness` applies. */\nconst isOwnEntry = (\n\th: RosterHolder,\n\townPid: number,\n\townHost: string,\n\townStartTime: number | null,\n): boolean => {\n\tif (h.pid !== ownPid || h.hostname !== ownHost) return false;\n\t// Either side null → fall back to (pid, hostname). The roster's\n\t// own-entry test must symmetrically accept a null recorded stamp:\n\t// the writer's probe failed (exotic platform / transient `ps`\n\t// error) but the entry IS ours. Mismatching a probed `ownStartTime`\n\t// against a recorded `null` would orphan our own entry — peers\n\t// would then harvest it as \"dead\" on the next sweep.\n\tif (ownStartTime === null || h.startTime === null) return true;\n\treturn h.startTime === ownStartTime;\n};\n\nconst withStackLock = <A, E, R>(\n\tpaths: RosterPaths,\n\tbody: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E | import('./stack-lock.ts').StackLockError, R> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(paths.stackLockFile);\n\t\t\treturn yield* body;\n\t\t}),\n\t);\n\n/**\n * Claim protocol — architecture § Claim protocol.\n *\n * Under the exclusive lock:\n * 1. Read the roster (or initialize empty if missing).\n * 2. Sweep stale holders (PID liveness + heartbeat age).\n * 3. Append this process's entry.\n * 4. Atomic write the result.\n *\n * Returns the swept document, our holder entry, and any evicted peers.\n */\nexport const claim = (\n\tpaths: RosterPaths,\n\tintent: 'normal' | 'snapshot' = 'normal',\n\tpolicy: RosterSweepPolicy = DEFAULT_SWEEP_POLICY,\n\tmetadata: { readonly graphInputId?: string } = {},\n): Effect.Effect<ClaimResult, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst initial = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst { swept, evicted } = yield* sweepStaleHolders(initial, policy);\n\t\t\tconst self: RosterHolder = { ...ownHolder(intent), ...metadata };\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: [...swept.holders, self],\n\t\t\t};\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t\treturn {\n\t\t\t\tself,\n\t\t\t\troster: next,\n\t\t\t\tevicted,\n\t\t\t\tsoleHolder: swept.holders.length === 0,\n\t\t\t};\n\t\t}),\n\t);\n\n/**\n * Heartbeat protocol — refresh this process's `heartbeatAt`. Under the\n * stack lock to keep mutation atomic. Architecture § Heartbeat\n * protocol.\n */\nexport const heartbeat = (\n\tpaths: RosterPaths,\n\townPid: number = selfPid(),\n): Effect.Effect<void, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst now = Date.now();\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tlet touched = false;\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: current.holders.map((h) => {\n\t\t\t\t\tif (isOwnEntry(h, ownPid, ownHost, ownStartTime)) {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t\treturn { ...h, heartbeatAt: now };\n\t\t\t\t\t}\n\t\t\t\t\treturn h;\n\t\t\t\t}),\n\t\t\t};\n\t\t\tif (touched) yield* atomicWriteRoster(paths.rosterFile, next);\n\t\t}),\n\t);\n\n/**\n * Release protocol — architecture § Release protocol.\n *\n * Removes this process's holder. Returns whether this was the\n * last-leaver (caller runs the stop finalizer if so).\n */\nexport const release = (\n\tpaths: RosterPaths,\n\townPid: number = selfPid(),\n): Effect.Effect<ReleaseResult, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tconst remaining = current.holders.filter(\n\t\t\t\t(h) => !isOwnEntry(h, ownPid, ownHost, ownStartTime),\n\t\t\t);\n\t\t\tconst next: RosterDocument = { version: 1, holders: remaining };\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t\treturn {\n\t\t\t\troster: next,\n\t\t\t\tlastLeaver: remaining.length === 0,\n\t\t\t};\n\t\t}),\n\t);\n\n/**\n * Set this process's `intent` (`normal` ↔ `snapshot`) under the\n * exclusive lock. Architecture § Concurrent snapshot step 2 / 5.\n */\nexport const setIntent = (\n\tpaths: RosterPaths,\n\tintent: 'normal' | 'snapshot',\n\townPid: number = selfPid(),\n): Effect.Effect<void, RosterError | import('./stack-lock.ts').StackLockError> =>\n\twithStackLock(\n\t\tpaths,\n\t\tEffect.gen(function* () {\n\t\t\tconst current = yield* readRoster(paths.rosterFile).pipe(\n\t\t\t\tEffect.catchTag('RosterCorruptError', () => Effect.succeed(EMPTY_ROSTER)),\n\t\t\t);\n\t\t\tconst ownHost = nodeHostname();\n\t\t\tconst ownStartTime = processStartTime(ownPid);\n\t\t\tconst next: RosterDocument = {\n\t\t\t\tversion: 1,\n\t\t\t\tholders: current.holders.map((h) =>\n\t\t\t\t\tisOwnEntry(h, ownPid, ownHost, ownStartTime) ? { ...h, intent } : h,\n\t\t\t\t),\n\t\t\t};\n\t\t\tyield* atomicWriteRoster(paths.rosterFile, next);\n\t\t}),\n\t);\n\n/** Background heartbeat fiber. Wakes every `intervalMillis` (default\n * matches `DEFAULT_SWEEP_POLICY.heartbeatIntervalMillis`) and refreshes\n * this process's `heartbeatAt`.\n *\n * Returns an Effect that runs forever in its Scope — the supervisor\n * forks it via `Effect.forkScoped` so it tears down with the stack. */\nexport const heartbeatFiber = (\n\tpaths: RosterPaths,\n\tintervalMillis: number = DEFAULT_SWEEP_POLICY.heartbeatIntervalMillis,\n): Effect.Effect<never> =>\n\tEffect.gen(function* () {\n\t\twhile (true) {\n\t\t\tyield* Effect.sleep(`${intervalMillis} millis`);\n\t\t\tyield* heartbeat(paths).pipe(\n\t\t\t\t// A heartbeat failure is non-fatal — the next peer's sweep\n\t\t\t\t// would otherwise evict us, but the architecture treats this\n\t\t\t\t// as \"the dev didn't get to flush a heartbeat in time;\n\t\t\t\t// recover by retrying next interval.\" Log via Effect's\n\t\t\t\t// logger; do not propagate.\n\t\t\t\tEffect.catch((err) =>\n\t\t\t\t\tEffect.logWarning('roster heartbeat failed').pipe(\n\t\t\t\t\t\tEffect.annotateLogs({\n\t\t\t\t\t\t\t[LogAttr.rosterHeartbeatIntervalMs]: intervalMillis,\n\t\t\t\t\t\t\t[LogAttr.errorCause]: String(err),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t});\n"],"mappings":"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"}
@@ -21,12 +21,14 @@ const probeSupervisorPresence = (rosterFile) => Effect.gen(function* () {
21
21
  for (const holder of doc.holders) if ((yield* probe.probeHolderLiveness(holder).pipe(Effect.catch(() => Effect.succeed("alive")))) === "alive") return {
22
22
  live: true,
23
23
  pid: holder.pid,
24
- hostname: holder.hostname
24
+ hostname: holder.hostname,
25
+ graphInputId: holder.graphInputId ?? null
25
26
  };
26
27
  return {
27
28
  live: false,
28
29
  pid: null,
29
- hostname: null
30
+ hostname: null,
31
+ graphInputId: null
30
32
  };
31
33
  }).pipe(Effect.provide(layerLivenessProbeScope));
32
34
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor-presence.mjs","names":[],"sources":["../../../../src/surfaces/cli/commands/supervisor-presence.ts"],"sourcesContent":["// Detect whether a supervisor is currently live for a given stack.\n//\n// The CLI uses this to decide whether a verb should publish via the\n// cross-process command channel (live supervisor: publish + await ack)\n// or use its direct/offline fallback.\n//\n// Detection reads the existing `roster.json` (architecture § Cross-\n// process safety protocol § Roster) and walks holders. A holder with a\n// live PID + matching start-time → supervisor live (matches\n// `liveness.ts` discipline; devstack is single-host).\n\nimport { Effect } from 'effect';\n\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\treadRoster,\n\ttype RosterError,\n} from '../../../substrate/runtime/cross-process/index.ts';\n\nexport interface SupervisorPresence {\n\treadonly live: boolean;\n\treadonly pid: number | null;\n\treadonly hostname: string | null;\n}\n\n/**\n * Read `rosterFile` and report whether at least one live holder\n * remains. Returns `{ live: false, pid: null }` when the roster is\n * missing OR all holders are dead-by-liveness-check.\n *\n * Tolerates corrupt rosters: a malformed file is treated as \"no\n * live supervisor\" — the worst case is the CLI refusing a publish\n * the user could have buffered.\n */\nexport const probeSupervisorPresence = (\n\trosterFile: string,\n): Effect.Effect<SupervisorPresence, RosterError> =>\n\t// Yield a fresh `LivenessProbeScope` so a corrupted-roster edge case\n\t// with multiple holders sharing one pid forks `ps`/`tasklist` once.\n\tEffect.gen(function* () {\n\t\tconst doc = yield* readRoster(rosterFile).pipe(\n\t\t\tEffect.catchTag('RosterCorruptError', () =>\n\t\t\t\tEffect.succeed({ version: 1 as const, holders: [] }),\n\t\t\t),\n\t\t);\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tfor (const holder of doc.holders) {\n\t\t\tconst liveness = yield* probe\n\t\t\t\t.probeHolderLiveness(holder)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\tif (liveness === 'alive') {\n\t\t\t\treturn { live: true, pid: holder.pid, hostname: holder.hostname };\n\t\t\t}\n\t\t}\n\t\treturn { live: false, pid: null, hostname: null };\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n"],"mappings":";;;;;;;;;;;;;;AAmCA,MAAa,2BACZ,eAIA,OAAO,IAAI,aAAa;CACvB,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC,CAAC,KACzC,OAAO,SAAS,4BACf,OAAO,QAAQ;EAAE,SAAS;EAAY,SAAS,CAAC;CAAE,CAAC,CACpD,CACD;CACA,MAAM,QAAQ,OAAO;CACrB,KAAK,MAAM,UAAU,IAAI,SAIxB,KAAI,OAHoB,MACtB,oBAAoB,MAAM,CAAC,CAC3B,KAAK,OAAO,YAAY,OAAO,QAAQ,OAAgB,CAAC,CAAC,OAC1C,SAChB,OAAO;EAAE,MAAM;EAAM,KAAK,OAAO;EAAK,UAAU,OAAO;CAAS;CAGlE,OAAO;EAAE,MAAM;EAAO,KAAK;EAAM,UAAU;CAAK;AACjD,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,uBAAuB,CAAC"}
1
+ {"version":3,"file":"supervisor-presence.mjs","names":[],"sources":["../../../../src/surfaces/cli/commands/supervisor-presence.ts"],"sourcesContent":["// Detect whether a supervisor is currently live for a given stack.\n//\n// The CLI uses this to decide whether a verb should publish via the\n// cross-process command channel (live supervisor: publish + await ack)\n// or use its direct/offline fallback.\n//\n// Detection reads the existing `roster.json` (architecture § Cross-\n// process safety protocol § Roster) and walks holders. A holder with a\n// live PID + matching start-time → supervisor live (matches\n// `liveness.ts` discipline; devstack is single-host).\n\nimport { Effect } from 'effect';\n\nimport {\n\tlayerLivenessProbeScope,\n\tLivenessProbeScope,\n\treadRoster,\n\ttype RosterError,\n} from '../../../substrate/runtime/cross-process/index.ts';\n\nexport interface SupervisorPresence {\n\treadonly live: boolean;\n\treadonly pid: number | null;\n\treadonly hostname: string | null;\n\treadonly graphInputId: string | null;\n}\n\n/**\n * Read `rosterFile` and report whether at least one live holder\n * remains. Returns `{ live: false, pid: null }` when the roster is\n * missing OR all holders are dead-by-liveness-check.\n *\n * Tolerates corrupt rosters: a malformed file is treated as \"no\n * live supervisor\" — the worst case is the CLI refusing a publish\n * the user could have buffered.\n */\nexport const probeSupervisorPresence = (\n\trosterFile: string,\n): Effect.Effect<SupervisorPresence, RosterError> =>\n\t// Yield a fresh `LivenessProbeScope` so a corrupted-roster edge case\n\t// with multiple holders sharing one pid forks `ps`/`tasklist` once.\n\tEffect.gen(function* () {\n\t\tconst doc = yield* readRoster(rosterFile).pipe(\n\t\t\tEffect.catchTag('RosterCorruptError', () =>\n\t\t\t\tEffect.succeed({ version: 1 as const, holders: [] }),\n\t\t\t),\n\t\t);\n\t\tconst probe = yield* LivenessProbeScope;\n\t\tfor (const holder of doc.holders) {\n\t\t\tconst liveness = yield* probe\n\t\t\t\t.probeHolderLiveness(holder)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed('alive' as const)));\n\t\t\tif (liveness === 'alive') {\n\t\t\t\treturn {\n\t\t\t\t\tlive: true,\n\t\t\t\t\tpid: holder.pid,\n\t\t\t\t\thostname: holder.hostname,\n\t\t\t\t\tgraphInputId: holder.graphInputId ?? null,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { live: false, pid: null, hostname: null, graphInputId: null };\n\t}).pipe(Effect.provide(layerLivenessProbeScope));\n"],"mappings":";;;;;;;;;;;;;;AAoCA,MAAa,2BACZ,eAIA,OAAO,IAAI,aAAa;CACvB,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC,CAAC,KACzC,OAAO,SAAS,4BACf,OAAO,QAAQ;EAAE,SAAS;EAAY,SAAS,CAAC;CAAE,CAAC,CACpD,CACD;CACA,MAAM,QAAQ,OAAO;CACrB,KAAK,MAAM,UAAU,IAAI,SAIxB,KAAI,OAHoB,MACtB,oBAAoB,MAAM,CAAC,CAC3B,KAAK,OAAO,YAAY,OAAO,QAAQ,OAAgB,CAAC,CAAC,OAC1C,SAChB,OAAO;EACN,MAAM;EACN,KAAK,OAAO;EACZ,UAAU,OAAO;EACjB,cAAc,OAAO,gBAAgB;CACtC;CAGF,OAAO;EAAE,MAAM;EAAO,KAAK;EAAM,UAAU;EAAM,cAAc;CAAK;AACrE,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,uBAAuB,CAAC"}
@@ -25,6 +25,12 @@ var CliConfirmDeclinedError = class extends Data.TaggedError("CliConfirmDeclined
25
25
  * refused to mutate shared state. */
26
26
  var CliSupervisorLiveError = class extends Data.TaggedError("CliSupervisorLiveError") {};
27
27
  Data.TaggedError("CliNoSupervisorError");
28
+ /** A live supervisor is running, but it was booted from a different
29
+ * stack graph than the config the current verb just loaded. Commands
30
+ * that only re-run post-acquire/codegen against the live supervisor
31
+ * must refuse this, otherwise generated static bindings can diverge
32
+ * from the live deployment values. */
33
+ var CliLiveGraphMismatchError = class extends Data.TaggedError("CliLiveGraphMismatchError") {};
28
34
  /** Internal/unexpected failure. Wraps an arbitrary cause; the cascade
29
35
  * formatter renders the inner. */
30
36
  var CliInternalError = class extends Data.TaggedError("CliInternalError") {};
@@ -42,6 +48,7 @@ const isCliError = (value) => {
42
48
  case "CliConfirmDeclinedError":
43
49
  case "CliSupervisorLiveError":
44
50
  case "CliNoSupervisorError":
51
+ case "CliLiveGraphMismatchError":
45
52
  case "CliInternalError":
46
53
  case "CliAlreadyReportedError": return true;
47
54
  default: return false;
@@ -62,6 +69,7 @@ const exitCodeFor = (error) => {
62
69
  case "CliConfirmDeclinedError": return ExitCode.CONFIRM_REQUIRED;
63
70
  case "CliSupervisorLiveError": return ExitCode.SUPERVISOR_LIVE;
64
71
  case "CliNoSupervisorError": return ExitCode.UNAVAILABLE;
72
+ case "CliLiveGraphMismatchError": return ExitCode.SUPERVISOR_LIVE;
65
73
  case "CliInternalError": return ExitCode.SOFTWARE;
66
74
  case "CliAlreadyReportedError": return error.exitCode;
67
75
  default: return ExitCode.SOFTWARE;
@@ -81,6 +89,7 @@ const summaryFor = (error) => {
81
89
  case "CliConfirmDeclinedError": return `${error.verb} confirmation declined`;
82
90
  case "CliSupervisorLiveError": return `supervisor live for ${error.app}/${error.stack}`;
83
91
  case "CliNoSupervisorError": return `no supervisor running for ${error.app}/${error.stack}`;
92
+ case "CliLiveGraphMismatchError": return `live supervisor graph is stale for ${error.app}/${error.stack}`;
84
93
  case "CliInternalError": return error.message;
85
94
  case "CliAlreadyReportedError": return "(already reported)";
86
95
  default: return "(unknown error)";
@@ -95,7 +104,8 @@ const hintFor = (error) => {
95
104
  case "CliConfirmRequiredError":
96
105
  case "CliConfirmDeclinedError":
97
106
  case "CliSupervisorLiveError":
98
- case "CliNoSupervisorError": return error.hint;
107
+ case "CliNoSupervisorError":
108
+ case "CliLiveGraphMismatchError": return error.hint;
99
109
  case "CliConfigNotFoundError": return "config file not found; run `devstack init` or pass `--config <path>`";
100
110
  case "CliConfigInvalidError":
101
111
  case "CliSnapshotNotFoundError":
@@ -105,6 +115,6 @@ const hintFor = (error) => {
105
115
  }
106
116
  };
107
117
  //#endregion
108
- export { CliConfigInvalidError, CliConfigNotFoundError, CliConfirmDeclinedError, CliConfirmRequiredError, CliInternalError, CliSnapshotAmbiguousError, CliSnapshotNotFoundError, CliSupervisorLiveError, CliUnavailableError, CliUsageError, exitCodeFor, hintFor, isCliError, summaryFor };
118
+ export { CliConfigInvalidError, CliConfigNotFoundError, CliConfirmDeclinedError, CliConfirmRequiredError, CliInternalError, CliLiveGraphMismatchError, CliSnapshotAmbiguousError, CliSnapshotNotFoundError, CliSupervisorLiveError, CliUnavailableError, CliUsageError, exitCodeFor, hintFor, isCliError, summaryFor };
109
119
 
110
120
  //# sourceMappingURL=errors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":["XC"],"sources":["../../../src/surfaces/cli/errors.ts"],"sourcesContent":["// CLI surface — typed errors carried up to the top-level renderer.\n//\n// Architecture (distilled/20-cli.md § Edge cases) enumerates the\n// surface's failure modes. Each one MUST resolve to a stable sysexit\n// code; the error tag here carries that mapping so the dispatcher\n// doesn't sprinkle exit codes through every command.\n//\n// Why tagged errors and not plain `Effect.fail(string)`?\n// - `catchTag` in commands can pick out the precise tag without\n// stringly matching.\n// - The cascade formatter (substrate/runtime/observability/\n// cascade-formatter.ts) renders these natively via their `_tag`.\n// - The `schema --json` command enumerates the tag table.\n\nimport { Data } from 'effect';\n\nimport { type ExitCode, ExitCode as XC } from './sysexits.ts';\n\n// -----------------------------------------------------------------------------\n// Tagged errors\n// -----------------------------------------------------------------------------\n\n/** Argv parsing produced an error — unknown subcommand, malformed\n * flag value, missing required positional. */\nexport class CliUsageError extends Data.TaggedError('CliUsageError')<{\n\treadonly message: string;\n\treadonly hint?: string;\n}> {}\n\n/** The user's `devstack.config.ts` could not be located (path probe\n * exhausted) or could not be imported. */\nexport class CliConfigNotFoundError extends Data.TaggedError('CliConfigNotFoundError')<{\n\treadonly message: string;\n\treadonly searchedPaths?: ReadonlyArray<string>;\n}> {}\n\n/** The config imported successfully but its default export is not a\n * `Stack` value (failed brand check, missing required field). */\nexport class CliConfigInvalidError extends Data.TaggedError('CliConfigInvalidError')<{\n\treadonly message: string;\n\treadonly cause?: unknown;\n}> {}\n\n/** A required service (Docker daemon, network) is unavailable. */\nexport class CliUnavailableError extends Data.TaggedError('CliUnavailableError')<{\n\treadonly message: string;\n\treadonly service: string;\n\treadonly hint?: string;\n}> {}\n\n/** Snapshot name/id not found. Carries the reference the caller\n * used so the failure renderer can echo it back. */\nexport class CliSnapshotNotFoundError extends Data.TaggedError('CliSnapshotNotFoundError')<{\n\treadonly snapshotRef: string;\n}> {}\n\nexport class CliSnapshotAmbiguousError extends Data.TaggedError('CliSnapshotAmbiguousError')<{\n\treadonly snapshotRef: string;\n\treadonly matches: ReadonlyArray<string>;\n}> {}\n\n/** Destructive verb refused because `--yes` was absent and prompting\n * was forbidden (non-TTY stdin or `--no-input`). */\nexport class CliConfirmRequiredError extends Data.TaggedError('CliConfirmRequiredError')<{\n\treadonly verb: string;\n\treadonly hint?: string;\n}> {}\n\n/** Destructive verb was prompted interactively and the user declined. */\nexport class CliConfirmDeclinedError extends Data.TaggedError('CliConfirmDeclinedError')<{\n\treadonly verb: string;\n\treadonly hint?: string;\n}> {}\n\n/** Engine reports a live supervisor for the target stack; the verb\n * refused to mutate shared state. */\nexport class CliSupervisorLiveError extends Data.TaggedError('CliSupervisorLiveError')<{\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly hint?: string;\n}> {}\n\n/** The verb requires a live supervisor (a stack must be `up`) but\n * none is running for `(app, stack)`. Distinguished from\n * `CliSupervisorLiveError` (the inverse case where the verb refused\n * because one IS live). */\nexport class CliNoSupervisorError extends Data.TaggedError('CliNoSupervisorError')<{\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly hint?: string;\n}> {}\n\n/** Internal/unexpected failure. Wraps an arbitrary cause; the cascade\n * formatter renders the inner. */\nexport class CliInternalError extends Data.TaggedError('CliInternalError')<{\n\treadonly message: string;\n\treadonly cause?: unknown;\n}> {}\n\n/** Sentinel: subcommand already pretty-rendered its failure. The\n * top-level renderer recognizes this tag and skips re-rendering, but\n * still propagates `exitCode` to the OS exit code.\n *\n * Architecture § Learnings: \"Already-reported sentinel pattern\" — the\n * marker traverses the cause structure used by the Effect runtime, so\n * the top-level `Effect.catch` sees it. */\nexport class CliAlreadyReportedError extends Data.TaggedError('CliAlreadyReportedError')<{\n\treadonly exitCode: ExitCode;\n}> {}\n\n// -----------------------------------------------------------------------------\n// Union + sysexit projection\n// -----------------------------------------------------------------------------\n\nexport type CliError =\n\t| CliUsageError\n\t| CliConfigNotFoundError\n\t| CliConfigInvalidError\n\t| CliUnavailableError\n\t| CliSnapshotNotFoundError\n\t| CliSnapshotAmbiguousError\n\t| CliConfirmRequiredError\n\t| CliConfirmDeclinedError\n\t| CliSupervisorLiveError\n\t| CliNoSupervisorError\n\t| CliInternalError\n\t| CliAlreadyReportedError;\n\nexport const isCliError = (value: unknown): value is CliError => {\n\tif (typeof value !== 'object' || value === null) return false;\n\tconst tag = (value as { readonly _tag?: unknown })._tag;\n\tswitch (tag) {\n\t\tcase 'CliUsageError':\n\t\tcase 'CliConfigNotFoundError':\n\t\tcase 'CliConfigInvalidError':\n\t\tcase 'CliUnavailableError':\n\t\tcase 'CliSnapshotNotFoundError':\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\tcase 'CliSupervisorLiveError':\n\t\tcase 'CliNoSupervisorError':\n\t\tcase 'CliInternalError':\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n};\n\n/** Project a tagged error to its sysexit code. The dispatcher reads\n * this to set `process.exitCode` — there is exactly one place in the\n * surface where tag → numeric mapping happens. */\nexport const exitCodeFor = (error: CliError): ExitCode => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn XC.USAGE;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\treturn XC.NO_INPUT;\n\t\tcase 'CliConfigInvalidError':\n\t\t\treturn XC.CONFIG;\n\t\tcase 'CliUnavailableError':\n\t\t\treturn XC.UNAVAILABLE;\n\t\tcase 'CliSnapshotNotFoundError':\n\t\t\treturn XC.SNAPSHOT_NOT_FOUND;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn XC.USAGE;\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\t\treturn XC.CONFIRM_REQUIRED;\n\t\tcase 'CliSupervisorLiveError':\n\t\t\treturn XC.SUPERVISOR_LIVE;\n\t\tcase 'CliNoSupervisorError':\n\t\t\treturn XC.UNAVAILABLE;\n\t\tcase 'CliInternalError':\n\t\t\treturn XC.SOFTWARE;\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn error.exitCode;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn XC.SOFTWARE;\n\t\t}\n\t}\n};\n\n/** Short single-line summary for the envelope's `error.summary`\n * field. Cascade detail lives in `error.chain[]`. */\nexport const summaryFor = (error: CliError): string => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn error.message;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\treturn error.message;\n\t\tcase 'CliConfigInvalidError':\n\t\t\treturn error.message;\n\t\tcase 'CliUnavailableError':\n\t\t\treturn `${error.service} unavailable: ${error.message}`;\n\t\tcase 'CliSnapshotNotFoundError':\n\t\t\treturn `snapshot not found: ${error.snapshotRef}`;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn `snapshot reference is ambiguous: ${error.snapshotRef}`;\n\t\tcase 'CliConfirmRequiredError':\n\t\t\treturn `${error.verb} requires confirmation`;\n\t\tcase 'CliConfirmDeclinedError':\n\t\t\treturn `${error.verb} confirmation declined`;\n\t\tcase 'CliSupervisorLiveError':\n\t\t\treturn `supervisor live for ${error.app}/${error.stack}`;\n\t\tcase 'CliNoSupervisorError':\n\t\t\treturn `no supervisor running for ${error.app}/${error.stack}`;\n\t\tcase 'CliInternalError':\n\t\t\treturn error.message;\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn '(already reported)';\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn '(unknown error)';\n\t\t}\n\t}\n};\n\n/** Optional hint for the envelope's `error.hint` field. */\nexport const hintFor = (error: CliError): string | undefined => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn error.hint;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn `use one of these ids instead: ${error.matches.join(', ')}`;\n\t\tcase 'CliUnavailableError':\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\tcase 'CliSupervisorLiveError':\n\t\tcase 'CliNoSupervisorError':\n\t\t\treturn error.hint;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\t// Operator-facing hint: the most common cause is running a\n\t\t\t// devstack verb in a directory whose ancestors don't contain a\n\t\t\t// `devstack.config.ts`. Two actions resolve it — either\n\t\t\t// initialize one in the current tree, or point the loader at\n\t\t\t// an explicit path. We surface both so the envelope renderer\n\t\t\t// (JSON `error.hint`, human stderr line) can render one short\n\t\t\t// line instead of forcing the caller to remember the recipe.\n\t\t\treturn 'config file not found; run `devstack init` or pass `--config <path>`';\n\t\tcase 'CliConfigInvalidError':\n\t\tcase 'CliSnapshotNotFoundError':\n\t\tcase 'CliInternalError':\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn undefined;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn undefined;\n\t\t}\n\t}\n};\n"],"mappings":";;;;;AAwBA,IAAa,gBAAb,cAAmC,KAAK,YAAY,eAAe,CAAC,CAGjE,CAAC;;;AAIJ,IAAa,yBAAb,cAA4C,KAAK,YAAY,wBAAwB,CAAC,CAGnF,CAAC;;;AAIJ,IAAa,wBAAb,cAA2C,KAAK,YAAY,uBAAuB,CAAC,CAGjF,CAAC;;AAGJ,IAAa,sBAAb,cAAyC,KAAK,YAAY,qBAAqB,CAAC,CAI7E,CAAC;;;AAIJ,IAAa,2BAAb,cAA8C,KAAK,YAAY,0BAA0B,CAAC,CAEvF,CAAC;AAEJ,IAAa,4BAAb,cAA+C,KAAK,YAAY,2BAA2B,CAAC,CAGzF,CAAC;;;AAIJ,IAAa,0BAAb,cAA6C,KAAK,YAAY,yBAAyB,CAAC,CAGrF,CAAC;;AAGJ,IAAa,0BAAb,cAA6C,KAAK,YAAY,yBAAyB,CAAC,CAGrF,CAAC;;;AAIJ,IAAa,yBAAb,cAA4C,KAAK,YAAY,wBAAwB,CAAC,CAInF,CAAC;AAMsC,KAAK,YAAY,sBAAsB;;;AAQjF,IAAa,mBAAb,cAAsC,KAAK,YAAY,kBAAkB,CAAC,CAGvE,CAAC;AASyC,KAAK,YAAY,yBAAyB;AAsBvF,MAAa,cAAc,UAAsC;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CAExD,QADa,MAAsC,MACnD;EACC,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,2BACJ,OAAO;EACR,SACC,OAAO;CACT;AACD;;;;AAKA,MAAa,eAAe,UAA8B;CACzD,QAAQ,MAAM,MAAd;EACC,KAAK,iBACJ,OAAOA,SAAG;EACX,KAAK,0BACJ,OAAOA,SAAG;EACX,KAAK,yBACJ,OAAOA,SAAG;EACX,KAAK,uBACJ,OAAOA,SAAG;EACX,KAAK,4BACJ,OAAOA,SAAG;EACX,KAAK,6BACJ,OAAOA,SAAG;EACX,KAAK;EACL,KAAK,2BACJ,OAAOA,SAAG;EACX,KAAK,0BACJ,OAAOA,SAAG;EACX,KAAK,wBACJ,OAAOA,SAAG;EACX,KAAK,oBACJ,OAAOA,SAAG;EACX,KAAK,2BACJ,OAAO,MAAM;EACd,SAGC,OAAOA,SAAG;CAEZ;AACD;;;AAIA,MAAa,cAAc,UAA4B;CACtD,QAAQ,MAAM,MAAd;EACC,KAAK,iBACJ,OAAO,MAAM;EACd,KAAK,0BACJ,OAAO,MAAM;EACd,KAAK,yBACJ,OAAO,MAAM;EACd,KAAK,uBACJ,OAAO,GAAG,MAAM,QAAQ,gBAAgB,MAAM;EAC/C,KAAK,4BACJ,OAAO,uBAAuB,MAAM;EACrC,KAAK,6BACJ,OAAO,oCAAoC,MAAM;EAClD,KAAK,2BACJ,OAAO,GAAG,MAAM,KAAK;EACtB,KAAK,2BACJ,OAAO,GAAG,MAAM,KAAK;EACtB,KAAK,0BACJ,OAAO,uBAAuB,MAAM,IAAI,GAAG,MAAM;EAClD,KAAK,wBACJ,OAAO,6BAA6B,MAAM,IAAI,GAAG,MAAM;EACxD,KAAK,oBACJ,OAAO,MAAM;EACd,KAAK,2BACJ,OAAO;EACR,SAGC,OAAO;CAET;AACD;;AAGA,MAAa,WAAW,UAAwC;CAC/D,QAAQ,MAAM,MAAd;EACC,KAAK,iBACJ,OAAO,MAAM;EACd,KAAK,6BACJ,OAAO,iCAAiC,MAAM,QAAQ,KAAK,IAAI;EAChE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,wBACJ,OAAO,MAAM;EACd,KAAK,0BAQJ,OAAO;EACR,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,2BACJ;EACD,SAGC;CAEF;AACD"}
1
+ {"version":3,"file":"errors.mjs","names":["XC"],"sources":["../../../src/surfaces/cli/errors.ts"],"sourcesContent":["// CLI surface — typed errors carried up to the top-level renderer.\n//\n// Architecture (distilled/20-cli.md § Edge cases) enumerates the\n// surface's failure modes. Each one MUST resolve to a stable sysexit\n// code; the error tag here carries that mapping so the dispatcher\n// doesn't sprinkle exit codes through every command.\n//\n// Why tagged errors and not plain `Effect.fail(string)`?\n// - `catchTag` in commands can pick out the precise tag without\n// stringly matching.\n// - The cascade formatter (substrate/runtime/observability/\n// cascade-formatter.ts) renders these natively via their `_tag`.\n// - The `schema --json` command enumerates the tag table.\n\nimport { Data } from 'effect';\n\nimport { type ExitCode, ExitCode as XC } from './sysexits.ts';\n\n// -----------------------------------------------------------------------------\n// Tagged errors\n// -----------------------------------------------------------------------------\n\n/** Argv parsing produced an error — unknown subcommand, malformed\n * flag value, missing required positional. */\nexport class CliUsageError extends Data.TaggedError('CliUsageError')<{\n\treadonly message: string;\n\treadonly hint?: string;\n}> {}\n\n/** The user's `devstack.config.ts` could not be located (path probe\n * exhausted) or could not be imported. */\nexport class CliConfigNotFoundError extends Data.TaggedError('CliConfigNotFoundError')<{\n\treadonly message: string;\n\treadonly searchedPaths?: ReadonlyArray<string>;\n}> {}\n\n/** The config imported successfully but its default export is not a\n * `Stack` value (failed brand check, missing required field). */\nexport class CliConfigInvalidError extends Data.TaggedError('CliConfigInvalidError')<{\n\treadonly message: string;\n\treadonly cause?: unknown;\n}> {}\n\n/** A required service (Docker daemon, network) is unavailable. */\nexport class CliUnavailableError extends Data.TaggedError('CliUnavailableError')<{\n\treadonly message: string;\n\treadonly service: string;\n\treadonly hint?: string;\n}> {}\n\n/** Snapshot name/id not found. Carries the reference the caller\n * used so the failure renderer can echo it back. */\nexport class CliSnapshotNotFoundError extends Data.TaggedError('CliSnapshotNotFoundError')<{\n\treadonly snapshotRef: string;\n}> {}\n\nexport class CliSnapshotAmbiguousError extends Data.TaggedError('CliSnapshotAmbiguousError')<{\n\treadonly snapshotRef: string;\n\treadonly matches: ReadonlyArray<string>;\n}> {}\n\n/** Destructive verb refused because `--yes` was absent and prompting\n * was forbidden (non-TTY stdin or `--no-input`). */\nexport class CliConfirmRequiredError extends Data.TaggedError('CliConfirmRequiredError')<{\n\treadonly verb: string;\n\treadonly hint?: string;\n}> {}\n\n/** Destructive verb was prompted interactively and the user declined. */\nexport class CliConfirmDeclinedError extends Data.TaggedError('CliConfirmDeclinedError')<{\n\treadonly verb: string;\n\treadonly hint?: string;\n}> {}\n\n/** Engine reports a live supervisor for the target stack; the verb\n * refused to mutate shared state. */\nexport class CliSupervisorLiveError extends Data.TaggedError('CliSupervisorLiveError')<{\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly hint?: string;\n}> {}\n\n/** The verb requires a live supervisor (a stack must be `up`) but\n * none is running for `(app, stack)`. Distinguished from\n * `CliSupervisorLiveError` (the inverse case where the verb refused\n * because one IS live). */\nexport class CliNoSupervisorError extends Data.TaggedError('CliNoSupervisorError')<{\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly hint?: string;\n}> {}\n\n/** A live supervisor is running, but it was booted from a different\n * stack graph than the config the current verb just loaded. Commands\n * that only re-run post-acquire/codegen against the live supervisor\n * must refuse this, otherwise generated static bindings can diverge\n * from the live deployment values. */\nexport class CliLiveGraphMismatchError extends Data.TaggedError('CliLiveGraphMismatchError')<{\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly liveGraphInputId: string | null;\n\treadonly currentGraphInputId: string;\n\treadonly hint?: string;\n}> {}\n\n/** Internal/unexpected failure. Wraps an arbitrary cause; the cascade\n * formatter renders the inner. */\nexport class CliInternalError extends Data.TaggedError('CliInternalError')<{\n\treadonly message: string;\n\treadonly cause?: unknown;\n}> {}\n\n/** Sentinel: subcommand already pretty-rendered its failure. The\n * top-level renderer recognizes this tag and skips re-rendering, but\n * still propagates `exitCode` to the OS exit code.\n *\n * Architecture § Learnings: \"Already-reported sentinel pattern\" — the\n * marker traverses the cause structure used by the Effect runtime, so\n * the top-level `Effect.catch` sees it. */\nexport class CliAlreadyReportedError extends Data.TaggedError('CliAlreadyReportedError')<{\n\treadonly exitCode: ExitCode;\n}> {}\n\n// -----------------------------------------------------------------------------\n// Union + sysexit projection\n// -----------------------------------------------------------------------------\n\nexport type CliError =\n\t| CliUsageError\n\t| CliConfigNotFoundError\n\t| CliConfigInvalidError\n\t| CliUnavailableError\n\t| CliSnapshotNotFoundError\n\t| CliSnapshotAmbiguousError\n\t| CliConfirmRequiredError\n\t| CliConfirmDeclinedError\n\t| CliSupervisorLiveError\n\t| CliNoSupervisorError\n\t| CliLiveGraphMismatchError\n\t| CliInternalError\n\t| CliAlreadyReportedError;\n\nexport const isCliError = (value: unknown): value is CliError => {\n\tif (typeof value !== 'object' || value === null) return false;\n\tconst tag = (value as { readonly _tag?: unknown })._tag;\n\tswitch (tag) {\n\t\tcase 'CliUsageError':\n\t\tcase 'CliConfigNotFoundError':\n\t\tcase 'CliConfigInvalidError':\n\t\tcase 'CliUnavailableError':\n\t\tcase 'CliSnapshotNotFoundError':\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\tcase 'CliSupervisorLiveError':\n\t\tcase 'CliNoSupervisorError':\n\t\tcase 'CliLiveGraphMismatchError':\n\t\tcase 'CliInternalError':\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn false;\n\t}\n};\n\n/** Project a tagged error to its sysexit code. The dispatcher reads\n * this to set `process.exitCode` — there is exactly one place in the\n * surface where tag → numeric mapping happens. */\nexport const exitCodeFor = (error: CliError): ExitCode => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn XC.USAGE;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\treturn XC.NO_INPUT;\n\t\tcase 'CliConfigInvalidError':\n\t\t\treturn XC.CONFIG;\n\t\tcase 'CliUnavailableError':\n\t\t\treturn XC.UNAVAILABLE;\n\t\tcase 'CliSnapshotNotFoundError':\n\t\t\treturn XC.SNAPSHOT_NOT_FOUND;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn XC.USAGE;\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\t\treturn XC.CONFIRM_REQUIRED;\n\t\tcase 'CliSupervisorLiveError':\n\t\t\treturn XC.SUPERVISOR_LIVE;\n\t\tcase 'CliNoSupervisorError':\n\t\t\treturn XC.UNAVAILABLE;\n\t\tcase 'CliLiveGraphMismatchError':\n\t\t\treturn XC.SUPERVISOR_LIVE;\n\t\tcase 'CliInternalError':\n\t\t\treturn XC.SOFTWARE;\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn error.exitCode;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn XC.SOFTWARE;\n\t\t}\n\t}\n};\n\n/** Short single-line summary for the envelope's `error.summary`\n * field. Cascade detail lives in `error.chain[]`. */\nexport const summaryFor = (error: CliError): string => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn error.message;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\treturn error.message;\n\t\tcase 'CliConfigInvalidError':\n\t\t\treturn error.message;\n\t\tcase 'CliUnavailableError':\n\t\t\treturn `${error.service} unavailable: ${error.message}`;\n\t\tcase 'CliSnapshotNotFoundError':\n\t\t\treturn `snapshot not found: ${error.snapshotRef}`;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn `snapshot reference is ambiguous: ${error.snapshotRef}`;\n\t\tcase 'CliConfirmRequiredError':\n\t\t\treturn `${error.verb} requires confirmation`;\n\t\tcase 'CliConfirmDeclinedError':\n\t\t\treturn `${error.verb} confirmation declined`;\n\t\tcase 'CliSupervisorLiveError':\n\t\t\treturn `supervisor live for ${error.app}/${error.stack}`;\n\t\tcase 'CliNoSupervisorError':\n\t\t\treturn `no supervisor running for ${error.app}/${error.stack}`;\n\t\tcase 'CliLiveGraphMismatchError':\n\t\t\treturn `live supervisor graph is stale for ${error.app}/${error.stack}`;\n\t\tcase 'CliInternalError':\n\t\t\treturn error.message;\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn '(already reported)';\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn '(unknown error)';\n\t\t}\n\t}\n};\n\n/** Optional hint for the envelope's `error.hint` field. */\nexport const hintFor = (error: CliError): string | undefined => {\n\tswitch (error._tag) {\n\t\tcase 'CliUsageError':\n\t\t\treturn error.hint;\n\t\tcase 'CliSnapshotAmbiguousError':\n\t\t\treturn `use one of these ids instead: ${error.matches.join(', ')}`;\n\t\tcase 'CliUnavailableError':\n\t\tcase 'CliConfirmRequiredError':\n\t\tcase 'CliConfirmDeclinedError':\n\t\tcase 'CliSupervisorLiveError':\n\t\tcase 'CliNoSupervisorError':\n\t\tcase 'CliLiveGraphMismatchError':\n\t\t\treturn error.hint;\n\t\tcase 'CliConfigNotFoundError':\n\t\t\t// Operator-facing hint: the most common cause is running a\n\t\t\t// devstack verb in a directory whose ancestors don't contain a\n\t\t\t// `devstack.config.ts`. Two actions resolve it — either\n\t\t\t// initialize one in the current tree, or point the loader at\n\t\t\t// an explicit path. We surface both so the envelope renderer\n\t\t\t// (JSON `error.hint`, human stderr line) can render one short\n\t\t\t// line instead of forcing the caller to remember the recipe.\n\t\t\treturn 'config file not found; run `devstack init` or pass `--config <path>`';\n\t\tcase 'CliConfigInvalidError':\n\t\tcase 'CliSnapshotNotFoundError':\n\t\tcase 'CliInternalError':\n\t\tcase 'CliAlreadyReportedError':\n\t\t\treturn undefined;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = error;\n\t\t\tvoid _exhaustive;\n\t\t\treturn undefined;\n\t\t}\n\t}\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["runStricli"],"sources":["../../../src/surfaces/cli/index.ts"],"sourcesContent":["// CLI surface — top-level entry point.\n//\n// The public CLI is intentionally small: `up` is the attached operator\n// surface, `apply` is live-aware reconcile (publish to `up` when it is\n// running, one-shot otherwise), and every other command is offline/direct.\n\nimport {\n\tbuildApplication,\n\tbuildChoiceParser,\n\tbuildCommand,\n\tbuildRouteMap,\n\trun as runStricli,\n\ttext_en,\n\ttype CommandContext as StricliCommandContext,\n\ttype StricliProcess,\n} from '@stricli/core';\nimport { Effect, type Scope } from 'effect';\n\nimport { commandSchema } from './command-tree.ts';\nimport { type CliError, CliInternalError, CliUsageError, exitCodeFor } from './errors.ts';\nimport {\n\ttype CliRendererMode,\n\tENV_VARS,\n\ttype GlobalFlags,\n\ttype OutputMode,\n\ttype SnapshotStalePolicy,\n} from './flags.ts';\nimport { ExitCode } from './sysexits.ts';\nimport { parseDevstackNetworkName } from '../../api/inference-network.ts';\nimport { type CliIO, emitFailure, emitSuccess, nodeProcessIO } from './output.ts';\nimport {\n\ttype CommandResult,\n\ttype ConfigDeps,\n\ttype DoctorDeps,\n\ttype PruneDeps,\n\ttype PruneResourceScope,\n\ttype SnapshotDeps,\n\ttype StatusDeps,\n\ttype WipeDeps,\n\trunConfig,\n\trunDoctor,\n\trunPrune,\n\trunSnapshot,\n\trunStatus,\n\trunWipe,\n} from './commands/index.ts';\nimport { readDevstackVersion } from '../../cli/wirings/read-devstack-version.ts';\n\n// -----------------------------------------------------------------------------\n// Deps bundle\n// -----------------------------------------------------------------------------\n\nexport interface LifecycleCommandDeps {\n\treadonly run: (flags: GlobalFlags) => Effect.Effect<CommandResult, CliError>;\n}\n\n/** The stack-free `codegen` verb takes only a config path — it boots no\n * stack, so it needs no renderer/snapshot/identity flags. */\nexport interface CodegenCommandDeps {\n\treadonly run: (flags: {\n\t\treadonly configPath: string | undefined;\n\t}) => Effect.Effect<CommandResult, CliError>;\n}\n\n/** `dump-deployment` emits the stack's `deployment.json` deployment. It owns\n * its own output (file via `--out`, else stdout), so it receives `io`\n * and the resolved `outputMode` alongside the config path + destination. */\nexport interface DumpDeploymentCommandDeps {\n\treadonly run: (flags: {\n\t\treadonly configPath: string | undefined;\n\t\treadonly out: string | undefined;\n\t\treadonly network: string | undefined;\n\t\treadonly io: CliIO;\n\t\treadonly outputMode: OutputMode;\n\t}) => Effect.Effect<CommandResult, CliError>;\n}\n\nexport interface CliDeps {\n\treadonly up: LifecycleCommandDeps;\n\treadonly apply: LifecycleCommandDeps;\n\treadonly codegen: CodegenCommandDeps;\n\treadonly dumpDeployment: DumpDeploymentCommandDeps;\n\treadonly status: StatusDeps;\n\treadonly snapshot: SnapshotDeps;\n\treadonly prune: PruneDeps;\n\treadonly doctor: DoctorDeps;\n\treadonly config: ConfigDeps;\n\treadonly wipe: WipeDeps;\n}\n\nexport interface DispatchEnv {\n\treadonly argv: ReadonlyArray<string>;\n\treadonly env: Readonly<Record<string, string | undefined>>;\n\treadonly stdinIsTty: boolean;\n\treadonly io?: CliIO;\n}\n\n// -----------------------------------------------------------------------------\n// Stricli context + buffered process\n// -----------------------------------------------------------------------------\n\ninterface BufferedProcess extends StricliProcess {\n\treadonly stdoutBuffer: Array<string>;\n\treadonly stderrBuffer: Array<string>;\n\texitCode?: number | string | null;\n}\n\ninterface TrackedIO extends CliIO {\n\treadonly touched: () => boolean;\n\treadonly lastExitCode: () => number | null;\n}\n\ninterface DevstackCliContext extends StricliCommandContext {\n\treadonly deps: CliDeps;\n\treadonly env: Readonly<Record<string, string | undefined>>;\n\treadonly stdinIsTty: boolean;\n\treadonly io: TrackedIO;\n\treadonly process: BufferedProcess;\n}\n\nconst makeBufferedProcess = (\n\tenv: Readonly<Record<string, string | undefined>>,\n): BufferedProcess => {\n\tconst stdoutBuffer: Array<string> = [];\n\tconst stderrBuffer: Array<string> = [];\n\treturn {\n\t\tstdoutBuffer,\n\t\tstderrBuffer,\n\t\tenv,\n\t\tstdout: {\n\t\t\twrite: (str) => {\n\t\t\t\tstdoutBuffer.push(str);\n\t\t\t},\n\t\t},\n\t\tstderr: {\n\t\t\twrite: (str) => {\n\t\t\t\tstderrBuffer.push(str);\n\t\t\t},\n\t\t},\n\t};\n};\n\nconst trackIO = (io: CliIO): TrackedIO => {\n\tlet touched = false;\n\tlet lastExitCode: number | null = null;\n\treturn {\n\t\ttouched: () => touched,\n\t\tlastExitCode: () => lastExitCode,\n\t\twriteStdout: (line) =>\n\t\t\tio.writeStdout(line).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\twriteStderr: (line) =>\n\t\t\tio.writeStderr(line).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\tsetExitCode: (code) =>\n\t\t\tio.setExitCode(code).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t\tlastExitCode = code;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Flag models\n// -----------------------------------------------------------------------------\n\ninterface IdentityFlags {\n\treadonly json?: boolean;\n\treadonly app?: string;\n\treadonly stack?: string;\n\treadonly stateDir?: string;\n\treadonly verbose?: boolean;\n}\n\ninterface ConfigFlags extends IdentityFlags {\n\treadonly config?: string;\n\treadonly network?: string;\n}\n\ninterface UpFlags extends ConfigFlags {\n\treadonly renderer?: CliRendererMode;\n\treadonly fromSnapshot?: string;\n\treadonly snapshotCache?: string;\n\treadonly snapshotStale?: SnapshotStalePolicy;\n}\n\ninterface DumpDeploymentFlags extends ConfigFlags {\n\treadonly out?: string;\n}\n\ninterface DestructiveFlags extends IdentityFlags {\n\treadonly dryRun?: boolean;\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n}\n\ninterface ConfirmFlags extends IdentityFlags {\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n}\n\ninterface PruneFlags {\n\treadonly json?: boolean;\n\treadonly stateDir?: string;\n\treadonly verbose?: boolean;\n\treadonly dryRun?: boolean;\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n\treadonly list?: boolean;\n\treadonly all?: boolean;\n\treadonly noContainers?: boolean;\n\treadonly noNetworks?: boolean;\n\treadonly noVolumes?: boolean;\n\treadonly includeImages?: boolean;\n}\n\ninterface SnapshotSaveFlags extends ConfigFlags {\n\treadonly name?: string;\n}\n\nconst textParser = (input: string): string => input;\n\nconst stringFlag = (brief: string, placeholder: string) =>\n\t({\n\t\tkind: 'parsed',\n\t\tparse: textParser,\n\t\toptional: true,\n\t\tplaceholder,\n\t\tbrief,\n\t}) as const;\n\nconst boolFlag = (brief: string) =>\n\t({\n\t\tkind: 'boolean',\n\t\toptional: true,\n\t\tbrief,\n\t}) as const;\n\nconst identityFlagParams = {\n\tjson: boolFlag('Emit JSON envelope output'),\n\tapp: stringFlag('Override app name', 'name'),\n\tstack: stringFlag('Override stack name', 'name'),\n\tstateDir: stringFlag('Override state directory', 'path'),\n\tverbose: boolFlag('Enable more verbose logging'),\n} as const;\n\nconst configFlagParams = {\n\t...identityFlagParams,\n\tconfig: stringFlag('Override devstack.config.ts path', 'path'),\n\tnetwork: stringFlag('Override network before config import', 'name'),\n} as const;\n\nconst destructiveFlagParams = {\n\t...identityFlagParams,\n\tdryRun: boolFlag('Skip mutating effects'),\n\tyes: boolFlag('Assume yes on prompts'),\n\tnoInput: boolFlag('Forbid prompts'),\n} as const;\n\nconst globalMaintenanceFlagParams = {\n\tjson: identityFlagParams.json,\n\tstateDir: identityFlagParams.stateDir,\n\tverbose: identityFlagParams.verbose,\n} as const;\n\nconst confirmFlagParams = {\n\t...identityFlagParams,\n\tyes: boolFlag('Assume yes on prompts'),\n\tnoInput: boolFlag('Forbid prompts'),\n} as const;\n\nconst pruneFlagParams = {\n\t...globalMaintenanceFlagParams,\n\tdryRun: destructiveFlagParams.dryRun,\n\tyes: destructiveFlagParams.yes,\n\tnoInput: destructiveFlagParams.noInput,\n\tlist: boolFlag('List devstack-labelled Docker resources without pruning'),\n\tall: boolFlag('Prune every idle non-shared resource group'),\n\tnoContainers: boolFlag('Do not remove containers'),\n\tnoNetworks: boolFlag('Do not remove networks'),\n\tnoVolumes: boolFlag('Do not remove volumes'),\n\tincludeImages: boolFlag('Also remove devstack-labelled images for selected groups'),\n} as const;\n\nconst rendererParser = buildChoiceParser(['tui', 'plain', 'silent'] as const);\nconst snapshotStaleParser = buildChoiceParser(['warn', 'block', 'clean-start'] as const);\n\nconst outputModeFrom = (\n\tflags: Pick<IdentityFlags, 'json'>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): OutputMode => (flags.json === true || env[ENV_VARS.JSON] === '1' ? 'json' : 'human');\n\nconst pruneResourcesFromFlags = (flags: PruneFlags): PruneResourceScope => ({\n\tcontainers: flags.noContainers !== true,\n\tnetworks: flags.noNetworks !== true,\n\tvolumes: flags.noVolumes !== true,\n\timages: flags.includeImages === true,\n});\n\nconst optionalEnv = (\n\tvalue: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>>,\n\tkey: string,\n): string | undefined => value ?? env[key];\n\nconst makeGlobalFlags = (\n\tctx: DevstackCliContext,\n\tflags: IdentityFlags & Partial<ConfigFlags & UpFlags & DestructiveFlags>,\n\trest: ReadonlyArray<string>,\n): GlobalFlags => {\n\tconst networkRaw = optionalEnv(flags.network, ctx.env, ENV_VARS.NETWORK);\n\tlet network: string | undefined;\n\tif (networkRaw !== undefined) {\n\t\ttry {\n\t\t\tnetwork = parseDevstackNetworkName(\n\t\t\t\tnetworkRaw,\n\t\t\t\tflags.network === undefined ? ENV_VARS.NETWORK : '--network',\n\t\t\t);\n\t\t} catch (cause) {\n\t\t\tthrow cause instanceof Error\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: new CliInternalError({ message: 'failed to parse network flag', cause });\n\t\t}\n\t}\n\treturn {\n\t\toutputMode: outputModeFrom(flags, ctx.env),\n\t\tapp: optionalEnv(flags.app, ctx.env, ENV_VARS.APP),\n\t\tstack: optionalEnv(flags.stack, ctx.env, ENV_VARS.STACK),\n\t\tstateDir: optionalEnv(flags.stateDir, ctx.env, ENV_VARS.STATE_DIR),\n\t\tconfigPath: optionalEnv(flags.config, ctx.env, ENV_VARS.CONFIG_PATH),\n\t\tnetwork,\n\t\trenderer: flags.renderer,\n\t\tfromSnapshot: flags.fromSnapshot,\n\t\tsnapshotCache: flags.snapshotCache,\n\t\tsnapshotStalePolicy: flags.snapshotStale,\n\t\tdryRun: flags.dryRun === true,\n\t\tconfirm: {\n\t\t\tassumeYes: flags.yes === true,\n\t\t\tforbidPrompt: flags.noInput === true || ctx.env[ENV_VARS.NO_INPUT] === '1',\n\t\t\tstdinIsTty: ctx.stdinIsTty,\n\t\t},\n\t\tverbose: flags.verbose === true,\n\t\trest,\n\t};\n};\n\n/**\n * Bridge the CLI `--network` flag through `process.env` so config-load-time\n * factory reads pick it up. This indirection is deliberate, not a leak.\n *\n * Why the mutation exists:\n * The `deepbook()` factory (`plugins/deepbook/index.ts`) defaults its mode\n * by reading `process.env.DEVSTACK_NETWORK` at config import time — before\n * any flag value has reached the orchestrator. To make `--network` affect\n * the same default, we must mutate the env BEFORE the user's\n * `devstack.config.ts` is loaded. The chain is:\n * `--network=<net>` flag → setNetworkEnv → `process.env.DEVSTACK_NETWORK`\n * → `deepbook()` factory's env read at import.\n *\n * Why save/restore (and a scoped finalizer) matters:\n * The CLI is also invoked from tests and embedded harnesses inside a single\n * process. An unscoped mutation leaks across invocations: a test that runs\n * `dispatch(['up','--network=testnet'])` followed by `dispatch(['up'])`\n * would see the second call inherit `testnet` from the first. The\n * finalizer restores the prior value (or deletes the key if it was unset)\n * on success, failure, AND interrupt — `Effect.addFinalizer` guarantees\n * the cleanup runs regardless of how the scope closes.\n */\nconst setNetworkEnv = (flags: GlobalFlags): Effect.Effect<void, never, Scope.Scope> => {\n\tif (flags.network === undefined) return Effect.void;\n\tconst next = flags.network;\n\treturn Effect.gen(function* () {\n\t\tconst prior = process.env[ENV_VARS.NETWORK];\n\t\tprocess.env[ENV_VARS.NETWORK] = next;\n\t\tyield* Effect.addFinalizer(() =>\n\t\t\tEffect.sync(() => {\n\t\t\t\tif (prior === undefined) {\n\t\t\t\t\tdelete process.env[ENV_VARS.NETWORK];\n\t\t\t\t} else {\n\t\t\t\t\tprocess.env[ENV_VARS.NETWORK] = prior;\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t});\n};\n\n// -----------------------------------------------------------------------------\n// Command execution helpers\n// -----------------------------------------------------------------------------\n\nconst runCommandEffect = async (\n\tctx: DevstackCliContext,\n\tcommand: string,\n\tflags: GlobalFlags,\n\teffect: Effect.Effect<CommandResult, CliError>,\n): Promise<void> => {\n\t// `setNetworkEnv` registers a `process.env[ENV_VARS.NETWORK]` restore as a\n\t// scope finalizer; the outer `Effect.scoped` closes that scope after the\n\t// command completes (success, failure, or interrupt), preventing env leaks\n\t// between concurrent CLI invocations in the same process.\n\tconst program = Effect.scoped(\n\t\tsetNetworkEnv(flags).pipe(\n\t\t\tEffect.andThen(effect),\n\t\t\tEffect.catch((error: CliError) =>\n\t\t\t\temitFailure(ctx.io, flags.outputMode, {\n\t\t\t\t\tcommand,\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror,\n\t\t\t\t}).pipe(Effect.as({ exitCode: exitCodeFor(error) })),\n\t\t\t),\n\t\t\tEffect.catchCause((cause) =>\n\t\t\t\temitFailure(ctx.io, flags.outputMode, {\n\t\t\t\t\tcommand,\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror: new CliInternalError({ message: 'unexpected internal failure' }),\n\t\t\t\t\tcause,\n\t\t\t\t}).pipe(Effect.as({ exitCode: ExitCode.SOFTWARE })),\n\t\t\t),\n\t\t),\n\t);\n\tconst result = await Effect.runPromise(program);\n\tif (ctx.io.lastExitCode() === null) {\n\t\tawait Effect.runPromise(ctx.io.setExitCode(result.exitCode));\n\t}\n};\n\nconst runWithFlags = async (\n\tctx: DevstackCliContext,\n\tcommand: string,\n\trawFlags: IdentityFlags & Partial<ConfigFlags & UpFlags & DestructiveFlags>,\n\trest: ReadonlyArray<string>,\n\teffect: (flags: GlobalFlags) => Effect.Effect<CommandResult, CliError>,\n): Promise<void> => {\n\tlet flags: GlobalFlags;\n\ttry {\n\t\tflags = makeGlobalFlags(ctx, rawFlags, rest);\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 CliInternalError({ message: 'failed to resolve CLI flags', cause });\n\t\tconst mode = outputModeFrom(rawFlags, ctx.env);\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(ctx.io, mode, {\n\t\t\t\tcommand,\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tawait runCommandEffect(ctx, command, flags, effect(flags));\n};\n\nconst requiredPositional = (placeholder: string, brief: string) =>\n\t({\n\t\tparse: textParser,\n\t\tplaceholder,\n\t\tbrief,\n\t}) as const;\n\nconst optionalPositional = (placeholder: string, brief: string) =>\n\t({\n\t\tparse: textParser,\n\t\tplaceholder,\n\t\tbrief,\n\t\toptional: true as const,\n\t}) as const;\n\n// -----------------------------------------------------------------------------\n// Commands\n// -----------------------------------------------------------------------------\n\nconst upCommand = buildCommand<UpFlags, [], DevstackCliContext>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\trenderer: {\n\t\t\t\tkind: 'parsed',\n\t\t\t\tparse: rendererParser,\n\t\t\t\toptional: true,\n\t\t\t\tplaceholder: 'tui|plain|silent',\n\t\t\t\tbrief: 'Select the attached renderer',\n\t\t\t},\n\t\t\tfromSnapshot: stringFlag('Start by restoring a named snapshot before acquire', 'name-or-id'),\n\t\t\tsnapshotCache: stringFlag(\n\t\t\t\t'Use a named snapshot as a startup cache and refresh it when stale',\n\t\t\t\t'name',\n\t\t\t),\n\t\t\tsnapshotStale: {\n\t\t\t\tkind: 'parsed',\n\t\t\t\tparse: snapshotStaleParser,\n\t\t\t\toptional: true,\n\t\t\t\tplaceholder: 'warn|block|clean-start',\n\t\t\t\tbrief: 'Policy when --from-snapshot inputs differ from the current stack',\n\t\t\t},\n\t\t},\n\t},\n\tdocs: {\n\t\tbrief: 'Boot a stack and stay attached until interrupted',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'up', flags, [], (global) => this.deps.up.run(global));\n\t},\n});\n\nconst applyCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: {\n\t\tbrief: 'Reconcile a live stack or run one-shot setup',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'apply', flags, [], (global) => this.deps.apply.run(global));\n\t},\n});\n\nconst codegenCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: {\n\t\tbrief: 'Regenerate committed bindings from Move source (no stack boot)',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'codegen', flags, [], (global) =>\n\t\t\tthis.deps.codegen.run({ configPath: global.configPath }),\n\t\t);\n\t},\n});\n\nconst dumpDeploymentCommand = buildCommand<DumpDeploymentFlags, [], DevstackCliContext>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\tout: stringFlag('Write the deployment JSON to this file instead of stdout', 'path'),\n\t\t\tnetwork: stringFlag(\n\t\t\t\t'Emit a typed deployments/<network>.ts (satisfies AppNetworkDeployment) instead of the raw envelope',\n\t\t\t\t'name',\n\t\t\t),\n\t\t},\n\t},\n\tdocs: {\n\t\tbrief: 'Emit the stack deployment (deployment.json) for a real-network deploy',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'dump-deployment', flags, [], (global) =>\n\t\t\tthis.deps.dumpDeployment.run({\n\t\t\t\tconfigPath: global.configPath,\n\t\t\t\tout: flags.out,\n\t\t\t\tnetwork: flags.network,\n\t\t\t\tio: this.io,\n\t\t\t\toutputMode: global.outputMode,\n\t\t\t}),\n\t\t);\n\t},\n});\n\nconst statusCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'Show the current stack projection (offline: from the manifest)' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'status', flags, [], (global) =>\n\t\t\trunStatus(this.deps.status, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst doctorCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'Run host and stack preflight checks' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'doctor', flags, [], (global) =>\n\t\t\trunDoctor(this.deps.doctor, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst configCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: { brief: 'Print resolved config inputs' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'config', flags, [], (global) =>\n\t\t\trunConfig(this.deps.config, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst schemaCommand = buildCommand<Pick<IdentityFlags, 'json'>, [], DevstackCliContext>({\n\tparameters: { flags: { json: identityFlagParams.json } },\n\tdocs: { brief: 'Emit the CLI schema' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'schema', flags, [], (global) => {\n\t\t\tconst data = { ...commandSchema(), outputMode: global.outputMode };\n\t\t\treturn emitSuccess(this.io, global.outputMode, {\n\t\t\t\tcommand: 'schema',\n\t\t\t\telapsedMs: 0,\n\t\t\t\tdata,\n\t\t\t\thumanLines: [JSON.stringify(data, null, 2)],\n\t\t\t}).pipe(Effect.as({ exitCode: 0 }));\n\t\t});\n\t},\n});\n\nconst snapshotSaveCommand = buildCommand<\n\tSnapshotSaveFlags,\n\t[string | undefined],\n\tDevstackCliContext\n>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\tname: stringFlag('Human-readable snapshot name', 'name'),\n\t\t},\n\t\tpositional: { kind: 'tuple', parameters: [optionalPositional('name', 'snapshot name')] },\n\t},\n\tdocs: { brief: 'Capture a snapshot' },\n\tfunc: function (flags, snapshotName) {\n\t\tconst rest = [\n\t\t\t'save',\n\t\t\t...(snapshotName === undefined ? [] : [snapshotName]),\n\t\t\t...(flags.name === undefined ? [] : ['--name', flags.name]),\n\t\t];\n\t\treturn runWithFlags(this, 'snapshot save', flags, rest, (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotRestoreCommand = buildCommand<ConfirmFlags, [string], DevstackCliContext>({\n\tparameters: {\n\t\tflags: confirmFlagParams,\n\t\tpositional: {\n\t\t\tkind: 'tuple',\n\t\t\tparameters: [requiredPositional('name-or-id', 'snapshot name or id')],\n\t\t},\n\t},\n\tdocs: { brief: 'Restore a snapshot' },\n\tfunc: function (flags, snapshotRef) {\n\t\treturn runWithFlags(this, 'snapshot restore', flags, ['restore', snapshotRef], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotListCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'List snapshots' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'snapshot list', flags, ['list'], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotDeleteCommand = buildCommand<ConfirmFlags, [string], DevstackCliContext>({\n\tparameters: {\n\t\tflags: confirmFlagParams,\n\t\tpositional: {\n\t\t\tkind: 'tuple',\n\t\t\tparameters: [requiredPositional('name-or-id', 'snapshot name or id')],\n\t\t},\n\t},\n\tdocs: { brief: 'Delete a snapshot' },\n\tfunc: function (flags, snapshotRef) {\n\t\treturn runWithFlags(this, 'snapshot delete', flags, ['delete', snapshotRef], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotCommands = buildRouteMap({\n\troutes: {\n\t\tsave: snapshotSaveCommand,\n\t\trestore: snapshotRestoreCommand,\n\t\tlist: snapshotListCommand,\n\t\tdelete: snapshotDeleteCommand,\n\t},\n\tdocs: { brief: 'Capture, restore, list, or delete stack snapshots' },\n});\n\nconst pruneCommand = buildCommand<PruneFlags, [], DevstackCliContext>({\n\tparameters: { flags: pruneFlagParams },\n\tdocs: { brief: 'Inventory and prune devstack-labelled Docker resources' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'prune', flags, [], (global) =>\n\t\t\trunPrune(\n\t\t\t\tthis.deps.prune,\n\t\t\t\t{ flags: global, io: this.io },\n\t\t\t\t{\n\t\t\t\t\tmode: flags.list === true ? 'list' : flags.all === true ? 'all' : 'auto',\n\t\t\t\t\tresources: pruneResourcesFromFlags(flags),\n\t\t\t\t},\n\t\t\t),\n\t\t);\n\t},\n});\n\nconst wipeCommand = buildCommand<DestructiveFlags, [], DevstackCliContext>({\n\tparameters: { flags: destructiveFlagParams },\n\tdocs: { brief: 'Destroy all state for the selected stack' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'wipe', flags, [], (global) =>\n\t\t\trunWipe(this.deps.wipe, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst root = buildRouteMap({\n\troutes: {\n\t\tup: upCommand,\n\t\tapply: applyCommand,\n\t\tcodegen: codegenCommand,\n\t\tdumpDeployment: dumpDeploymentCommand,\n\t\tstatus: statusCommand,\n\t\tdoctor: doctorCommand,\n\t\tconfig: configCommand,\n\t\tschema: schemaCommand,\n\t\tsnapshot: snapshotCommands,\n\t\tprune: pruneCommand,\n\t\twipe: wipeCommand,\n\t},\n\tdocs: {\n\t\tbrief: 'Sui development stack CLI',\n\t},\n});\n\nconst app = buildApplication(root, {\n\tname: 'devstack',\n\tversionInfo: { currentVersion: readDevstackVersion() },\n\tscanner: { caseStyle: 'allow-kebab-for-camel' },\n\tdocumentation: {\n\t\tcaseStyle: 'convert-camel-to-kebab',\n\t\tdisableAnsiColor: true,\n\t\tonlyRequiredInUsageLine: true,\n\t},\n\tlocalization: {\n\t\tloadText: () => ({\n\t\t\t...text_en,\n\t\t\texceptionWhileParsingArguments: (error) =>\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t}),\n\t},\n});\n\nconst jsonRequested = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): boolean => env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\n/**\n * Project a `BufferedProcess.exitCode` to a sysexit code AT THE\n * STRICLI-PARSE BOUNDARY ONLY.\n *\n * Contract: `BufferedProcess.exitCode` is mutated ONLY by Stricli's\n * argv-parser when an argv parse step fails (unknown subcommand,\n * malformed flag value, missing required positional). Verbs route\n * their own outcomes through `ctx.io.setExitCode`, which marks\n * `io.touched()` and short-circuits this projection in `dispatch`\n * before `flushBufferedProcess` is reached.\n *\n * A non-zero value here means Stricli rejected argv before any verb\n * ran, so mapping to `USAGE` holds by construction. Kept as a named\n * function so the invariant is documented at the call site.\n */\nconst normalizeStricliExitCode = (code: number | string | null | undefined): number => {\n\tif (typeof code === 'number' && code !== 0) return ExitCode.USAGE;\n\treturn ExitCode.OK;\n};\n\n/**\n * Bridge Stricli's synchronous `StricliProcess.{stdout,stderr}.write`\n * shape to our async (Effect-based) `CliIO` surface.\n *\n * The indirection is load-bearing for the JSON-envelope contract:\n * Stricli writes argv-parse errors to stderr the moment the parser\n * trips, but `--json` mode demands the failure be EMITTED as a\n * structured envelope on stdout (not raw text on stderr) with exit\n * code `EX_USAGE`. We can't wire Stricli's `stderr.write` directly to\n * `nodeProcessIO.writeStderr` because:\n *\n * 1. The stderr bytes are the raw parser error text; in `--json`\n * mode we need to transform them into a failure envelope.\n * 2. We don't know whether the verb handler \"touched\" the IO\n * (rendered its own envelope) until after Stricli returns.\n *\n * Buffering lets us delay the decision until both signals are\n * available. Tests substitute a `BufferedProcess` whose buffers are\n * later inspected, mirroring the prod flush behavior.\n */\nconst flushBufferedProcess = (\n\tprocess: BufferedProcess,\n\tio: CliIO,\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst exitCode = normalizeStricliExitCode(process.exitCode);\n\t\tconst stdout = process.stdoutBuffer.join('');\n\t\tconst stderr = process.stderrBuffer.join('').trim();\n\t\tif (exitCode !== 0 && jsonRequested(argv, env)) {\n\t\t\tyield* emitFailure(io, 'json', {\n\t\t\t\tcommand: '(parse)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror: new CliUsageError({\n\t\t\t\t\tmessage: stderr.length > 0 ? stderr : 'invalid command line',\n\t\t\t\t}),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (stdout.length > 0)\n\t\t\tyield* io.writeStdout(stdout.endsWith('\\n') ? stdout.slice(0, -1) : stdout);\n\t\tif (stderr.length > 0) yield* io.writeStderr(stderr);\n\t\tyield* io.setExitCode(exitCode);\n\t});\n\nexport const dispatch = (deps: CliDeps, dispatchEnv: DispatchEnv): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst io = trackIO(dispatchEnv.io ?? nodeProcessIO);\n\t\tconst process = makeBufferedProcess(dispatchEnv.env);\n\t\tconst ctx: DevstackCliContext = {\n\t\t\tprocess,\n\t\t\tdeps,\n\t\t\tenv: dispatchEnv.env,\n\t\t\tstdinIsTty: dispatchEnv.stdinIsTty,\n\t\t\tio,\n\t\t};\n\t\tyield* Effect.tryPromise({\n\t\t\ttry: () => runStricli(app, dispatchEnv.argv, ctx),\n\t\t\tcatch: (cause) => new CliInternalError({ message: 'CLI dispatcher failed', cause }),\n\t\t}).pipe(\n\t\t\tEffect.catch((error: CliError) =>\n\t\t\t\temitFailure(io, jsonRequested(dispatchEnv.argv, dispatchEnv.env) ? 'json' : 'human', {\n\t\t\t\t\tcommand: '(dispatch)',\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror,\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tif (!io.touched()) {\n\t\t\tyield* flushBufferedProcess(process, io, dispatchEnv.argv, dispatchEnv.env);\n\t\t}\n\t});\n\n// -----------------------------------------------------------------------------\n// Re-exports\n// -----------------------------------------------------------------------------\n\nexport type { CliIO } from './output.ts';\nexport type { GlobalFlags } from './flags.ts';\nexport { COMMAND_TREE, commandSchema, VERBS, type Verb } from './command-tree.ts';\nexport type { Envelope, EnvelopeError } from './envelope.ts';\nexport {\n\tENVELOPE_SCHEMA_VERSION,\n\tfailureEnvelope,\n\tstreamingEvent,\n\tsuccessEnvelope,\n\ttype StreamingEvent,\n} from './envelope.ts';\nexport { ExitCode, exitCodeName } from './sysexits.ts';\nexport {\n\ttype CliError,\n\tCliAlreadyReportedError,\n\tCliConfigInvalidError,\n\tCliConfigNotFoundError,\n\tCliConfirmDeclinedError,\n\tCliConfirmRequiredError,\n\tCliInternalError,\n\tCliSnapshotNotFoundError,\n\tCliSupervisorLiveError,\n\tCliUnavailableError,\n\tCliUsageError,\n} from './errors.ts';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["runStricli"],"sources":["../../../src/surfaces/cli/index.ts"],"sourcesContent":["// CLI surface — top-level entry point.\n//\n// The public CLI is intentionally small: `up` is the attached operator\n// surface, `apply` is live-aware reconcile (publish to `up` when it is\n// running, one-shot otherwise), and every other command is offline/direct.\n\nimport {\n\tbuildApplication,\n\tbuildChoiceParser,\n\tbuildCommand,\n\tbuildRouteMap,\n\trun as runStricli,\n\ttext_en,\n\ttype CommandContext as StricliCommandContext,\n\ttype StricliProcess,\n} from '@stricli/core';\nimport { Effect, type Scope } from 'effect';\n\nimport { commandSchema } from './command-tree.ts';\nimport { type CliError, CliInternalError, CliUsageError, exitCodeFor } from './errors.ts';\nimport {\n\ttype CliRendererMode,\n\tENV_VARS,\n\ttype GlobalFlags,\n\ttype OutputMode,\n\ttype SnapshotStalePolicy,\n} from './flags.ts';\nimport { ExitCode } from './sysexits.ts';\nimport { parseDevstackNetworkName } from '../../api/inference-network.ts';\nimport { type CliIO, emitFailure, emitSuccess, nodeProcessIO } from './output.ts';\nimport {\n\ttype CommandResult,\n\ttype ConfigDeps,\n\ttype DoctorDeps,\n\ttype PruneDeps,\n\ttype PruneResourceScope,\n\ttype SnapshotDeps,\n\ttype StatusDeps,\n\ttype WipeDeps,\n\trunConfig,\n\trunDoctor,\n\trunPrune,\n\trunSnapshot,\n\trunStatus,\n\trunWipe,\n} from './commands/index.ts';\nimport { readDevstackVersion } from '../../cli/wirings/read-devstack-version.ts';\n\n// -----------------------------------------------------------------------------\n// Deps bundle\n// -----------------------------------------------------------------------------\n\nexport interface LifecycleCommandDeps {\n\treadonly run: (flags: GlobalFlags) => Effect.Effect<CommandResult, CliError>;\n}\n\n/** The stack-free `codegen` verb takes only a config path — it boots no\n * stack, so it needs no renderer/snapshot/identity flags. */\nexport interface CodegenCommandDeps {\n\treadonly run: (flags: {\n\t\treadonly configPath: string | undefined;\n\t}) => Effect.Effect<CommandResult, CliError>;\n}\n\n/** `dump-deployment` emits the stack's `deployment.json` deployment. It owns\n * its own output (file via `--out`, else stdout), so it receives `io`\n * and the resolved `outputMode` alongside the config path + destination. */\nexport interface DumpDeploymentCommandDeps {\n\treadonly run: (flags: {\n\t\treadonly configPath: string | undefined;\n\t\treadonly out: string | undefined;\n\t\treadonly network: string | undefined;\n\t\treadonly io: CliIO;\n\t\treadonly outputMode: OutputMode;\n\t}) => Effect.Effect<CommandResult, CliError>;\n}\n\nexport interface CliDeps {\n\treadonly up: LifecycleCommandDeps;\n\treadonly apply: LifecycleCommandDeps;\n\treadonly codegen: CodegenCommandDeps;\n\treadonly dumpDeployment: DumpDeploymentCommandDeps;\n\treadonly status: StatusDeps;\n\treadonly snapshot: SnapshotDeps;\n\treadonly prune: PruneDeps;\n\treadonly doctor: DoctorDeps;\n\treadonly config: ConfigDeps;\n\treadonly wipe: WipeDeps;\n}\n\nexport interface DispatchEnv {\n\treadonly argv: ReadonlyArray<string>;\n\treadonly env: Readonly<Record<string, string | undefined>>;\n\treadonly stdinIsTty: boolean;\n\treadonly io?: CliIO;\n}\n\n// -----------------------------------------------------------------------------\n// Stricli context + buffered process\n// -----------------------------------------------------------------------------\n\ninterface BufferedProcess extends StricliProcess {\n\treadonly stdoutBuffer: Array<string>;\n\treadonly stderrBuffer: Array<string>;\n\texitCode?: number | string | null;\n}\n\ninterface TrackedIO extends CliIO {\n\treadonly touched: () => boolean;\n\treadonly lastExitCode: () => number | null;\n}\n\ninterface DevstackCliContext extends StricliCommandContext {\n\treadonly deps: CliDeps;\n\treadonly env: Readonly<Record<string, string | undefined>>;\n\treadonly stdinIsTty: boolean;\n\treadonly io: TrackedIO;\n\treadonly process: BufferedProcess;\n}\n\nconst makeBufferedProcess = (\n\tenv: Readonly<Record<string, string | undefined>>,\n): BufferedProcess => {\n\tconst stdoutBuffer: Array<string> = [];\n\tconst stderrBuffer: Array<string> = [];\n\treturn {\n\t\tstdoutBuffer,\n\t\tstderrBuffer,\n\t\tenv,\n\t\tstdout: {\n\t\t\twrite: (str) => {\n\t\t\t\tstdoutBuffer.push(str);\n\t\t\t},\n\t\t},\n\t\tstderr: {\n\t\t\twrite: (str) => {\n\t\t\t\tstderrBuffer.push(str);\n\t\t\t},\n\t\t},\n\t};\n};\n\nconst trackIO = (io: CliIO): TrackedIO => {\n\tlet touched = false;\n\tlet lastExitCode: number | null = null;\n\treturn {\n\t\ttouched: () => touched,\n\t\tlastExitCode: () => lastExitCode,\n\t\twriteStdout: (line) =>\n\t\t\tio.writeStdout(line).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\twriteStderr: (line) =>\n\t\t\tio.writeStderr(line).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\tsetExitCode: (code) =>\n\t\t\tio.setExitCode(code).pipe(\n\t\t\t\tEffect.tap(() =>\n\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\ttouched = true;\n\t\t\t\t\t\tlastExitCode = code;\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// Flag models\n// -----------------------------------------------------------------------------\n\ninterface IdentityFlags {\n\treadonly json?: boolean;\n\treadonly app?: string;\n\treadonly stack?: string;\n\treadonly stateDir?: string;\n\treadonly verbose?: boolean;\n}\n\ninterface ConfigFlags extends IdentityFlags {\n\treadonly config?: string;\n\treadonly network?: string;\n}\n\ninterface UpFlags extends ConfigFlags {\n\treadonly renderer?: CliRendererMode;\n\treadonly fromSnapshot?: string;\n\treadonly snapshotCache?: string;\n\treadonly snapshotStale?: SnapshotStalePolicy;\n}\n\ninterface DumpDeploymentFlags extends ConfigFlags {\n\treadonly out?: string;\n}\n\ninterface DestructiveFlags extends IdentityFlags {\n\treadonly dryRun?: boolean;\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n}\n\ninterface ConfirmFlags extends IdentityFlags {\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n}\n\ninterface PruneFlags {\n\treadonly json?: boolean;\n\treadonly stateDir?: string;\n\treadonly verbose?: boolean;\n\treadonly dryRun?: boolean;\n\treadonly yes?: boolean;\n\treadonly noInput?: boolean;\n\treadonly list?: boolean;\n\treadonly all?: boolean;\n\treadonly noContainers?: boolean;\n\treadonly noNetworks?: boolean;\n\treadonly noVolumes?: boolean;\n\treadonly includeImages?: boolean;\n}\n\ninterface SnapshotSaveFlags extends ConfigFlags {\n\treadonly name?: string;\n}\n\nconst textParser = (input: string): string => input;\n\nconst stringFlag = (brief: string, placeholder: string) =>\n\t({\n\t\tkind: 'parsed',\n\t\tparse: textParser,\n\t\toptional: true,\n\t\tplaceholder,\n\t\tbrief,\n\t}) as const;\n\nconst boolFlag = (brief: string) =>\n\t({\n\t\tkind: 'boolean',\n\t\toptional: true,\n\t\tbrief,\n\t}) as const;\n\nconst identityFlagParams = {\n\tjson: boolFlag('Emit JSON envelope output'),\n\tapp: stringFlag('Override app name', 'name'),\n\tstack: stringFlag('Override stack name', 'name'),\n\tstateDir: stringFlag('Override state directory', 'path'),\n\tverbose: boolFlag('Enable more verbose logging'),\n} as const;\n\nconst configFlagParams = {\n\t...identityFlagParams,\n\tconfig: stringFlag('Override devstack.config.ts path', 'path'),\n\tnetwork: stringFlag('Override network before config import', 'name'),\n} as const;\n\nconst destructiveFlagParams = {\n\t...identityFlagParams,\n\tdryRun: boolFlag('Skip mutating effects'),\n\tyes: boolFlag('Assume yes on prompts'),\n\tnoInput: boolFlag('Forbid prompts'),\n} as const;\n\nconst globalMaintenanceFlagParams = {\n\tjson: identityFlagParams.json,\n\tstateDir: identityFlagParams.stateDir,\n\tverbose: identityFlagParams.verbose,\n} as const;\n\nconst confirmFlagParams = {\n\t...identityFlagParams,\n\tyes: boolFlag('Assume yes on prompts'),\n\tnoInput: boolFlag('Forbid prompts'),\n} as const;\n\nconst pruneFlagParams = {\n\t...globalMaintenanceFlagParams,\n\tdryRun: destructiveFlagParams.dryRun,\n\tyes: destructiveFlagParams.yes,\n\tnoInput: destructiveFlagParams.noInput,\n\tlist: boolFlag('List devstack-labelled Docker resources without pruning'),\n\tall: boolFlag('Prune every idle non-shared resource group'),\n\tnoContainers: boolFlag('Do not remove containers'),\n\tnoNetworks: boolFlag('Do not remove networks'),\n\tnoVolumes: boolFlag('Do not remove volumes'),\n\tincludeImages: boolFlag('Also remove devstack-labelled images for selected groups'),\n} as const;\n\nconst rendererParser = buildChoiceParser(['tui', 'plain', 'silent'] as const);\nconst snapshotStaleParser = buildChoiceParser(['warn', 'block', 'clean-start'] as const);\n\nconst outputModeFrom = (\n\tflags: Pick<IdentityFlags, 'json'>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): OutputMode => (flags.json === true || env[ENV_VARS.JSON] === '1' ? 'json' : 'human');\n\nconst pruneResourcesFromFlags = (flags: PruneFlags): PruneResourceScope => ({\n\tcontainers: flags.noContainers !== true,\n\tnetworks: flags.noNetworks !== true,\n\tvolumes: flags.noVolumes !== true,\n\timages: flags.includeImages === true,\n});\n\nconst optionalEnv = (\n\tvalue: string | undefined,\n\tenv: Readonly<Record<string, string | undefined>>,\n\tkey: string,\n): string | undefined => value ?? env[key];\n\nconst makeGlobalFlags = (\n\tctx: DevstackCliContext,\n\tflags: IdentityFlags & Partial<ConfigFlags & UpFlags & DestructiveFlags>,\n\trest: ReadonlyArray<string>,\n): GlobalFlags => {\n\tconst networkRaw = optionalEnv(flags.network, ctx.env, ENV_VARS.NETWORK);\n\tlet network: string | undefined;\n\tif (networkRaw !== undefined) {\n\t\ttry {\n\t\t\tnetwork = parseDevstackNetworkName(\n\t\t\t\tnetworkRaw,\n\t\t\t\tflags.network === undefined ? ENV_VARS.NETWORK : '--network',\n\t\t\t);\n\t\t} catch (cause) {\n\t\t\tthrow cause instanceof Error\n\t\t\t\t? new CliUsageError({ message: cause.message })\n\t\t\t\t: new CliInternalError({ message: 'failed to parse network flag', cause });\n\t\t}\n\t}\n\treturn {\n\t\toutputMode: outputModeFrom(flags, ctx.env),\n\t\tapp: optionalEnv(flags.app, ctx.env, ENV_VARS.APP),\n\t\tstack: optionalEnv(flags.stack, ctx.env, ENV_VARS.STACK),\n\t\tstateDir: optionalEnv(flags.stateDir, ctx.env, ENV_VARS.STATE_DIR),\n\t\tconfigPath: optionalEnv(flags.config, ctx.env, ENV_VARS.CONFIG_PATH),\n\t\tnetwork,\n\t\trenderer: flags.renderer,\n\t\tfromSnapshot: flags.fromSnapshot,\n\t\tsnapshotCache: flags.snapshotCache,\n\t\tsnapshotStalePolicy: flags.snapshotStale,\n\t\tdryRun: flags.dryRun === true,\n\t\tconfirm: {\n\t\t\tassumeYes: flags.yes === true,\n\t\t\tforbidPrompt: flags.noInput === true || ctx.env[ENV_VARS.NO_INPUT] === '1',\n\t\t\tstdinIsTty: ctx.stdinIsTty,\n\t\t},\n\t\tverbose: flags.verbose === true,\n\t\trest,\n\t};\n};\n\n/**\n * Bridge the CLI `--network` flag through `process.env` so config-load-time\n * factory reads pick it up. This indirection is deliberate, not a leak.\n *\n * Why the mutation exists:\n * The `deepbook()` factory (`plugins/deepbook/index.ts`) defaults its mode\n * by reading `process.env.DEVSTACK_NETWORK` at config import time — before\n * any flag value has reached the orchestrator. To make `--network` affect\n * the same default, we must mutate the env BEFORE the user's\n * `devstack.config.ts` is loaded. The chain is:\n * `--network=<net>` flag → setNetworkEnv → `process.env.DEVSTACK_NETWORK`\n * → `deepbook()` factory's env read at import.\n *\n * Why save/restore (and a scoped finalizer) matters:\n * The CLI is also invoked from tests and embedded harnesses inside a single\n * process. An unscoped mutation leaks across invocations: a test that runs\n * `dispatch(['up','--network=testnet'])` followed by `dispatch(['up'])`\n * would see the second call inherit `testnet` from the first. The\n * finalizer restores the prior value (or deletes the key if it was unset)\n * on success, failure, AND interrupt — `Effect.addFinalizer` guarantees\n * the cleanup runs regardless of how the scope closes.\n */\nconst setNetworkEnv = (flags: GlobalFlags): Effect.Effect<void, never, Scope.Scope> => {\n\tif (flags.network === undefined) return Effect.void;\n\tconst next = flags.network;\n\treturn Effect.gen(function* () {\n\t\tconst prior = process.env[ENV_VARS.NETWORK];\n\t\tprocess.env[ENV_VARS.NETWORK] = next;\n\t\tyield* Effect.addFinalizer(() =>\n\t\t\tEffect.sync(() => {\n\t\t\t\tif (prior === undefined) {\n\t\t\t\t\tdelete process.env[ENV_VARS.NETWORK];\n\t\t\t\t} else {\n\t\t\t\t\tprocess.env[ENV_VARS.NETWORK] = prior;\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t});\n};\n\n// -----------------------------------------------------------------------------\n// Command execution helpers\n// -----------------------------------------------------------------------------\n\nconst runCommandEffect = async (\n\tctx: DevstackCliContext,\n\tcommand: string,\n\tflags: GlobalFlags,\n\teffect: Effect.Effect<CommandResult, CliError>,\n): Promise<void> => {\n\t// `setNetworkEnv` registers a `process.env[ENV_VARS.NETWORK]` restore as a\n\t// scope finalizer; the outer `Effect.scoped` closes that scope after the\n\t// command completes (success, failure, or interrupt), preventing env leaks\n\t// between concurrent CLI invocations in the same process.\n\tconst program = Effect.scoped(\n\t\tsetNetworkEnv(flags).pipe(\n\t\t\tEffect.andThen(effect),\n\t\t\tEffect.catch((error: CliError) =>\n\t\t\t\temitFailure(ctx.io, flags.outputMode, {\n\t\t\t\t\tcommand,\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror,\n\t\t\t\t}).pipe(Effect.as({ exitCode: exitCodeFor(error) })),\n\t\t\t),\n\t\t\tEffect.catchCause((cause) =>\n\t\t\t\temitFailure(ctx.io, flags.outputMode, {\n\t\t\t\t\tcommand,\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror: new CliInternalError({ message: 'unexpected internal failure' }),\n\t\t\t\t\tcause,\n\t\t\t\t}).pipe(Effect.as({ exitCode: ExitCode.SOFTWARE })),\n\t\t\t),\n\t\t),\n\t);\n\tconst result = await Effect.runPromise(program);\n\tif (ctx.io.lastExitCode() === null) {\n\t\tawait Effect.runPromise(ctx.io.setExitCode(result.exitCode));\n\t}\n};\n\nconst runWithFlags = async (\n\tctx: DevstackCliContext,\n\tcommand: string,\n\trawFlags: IdentityFlags & Partial<ConfigFlags & UpFlags & DestructiveFlags>,\n\trest: ReadonlyArray<string>,\n\teffect: (flags: GlobalFlags) => Effect.Effect<CommandResult, CliError>,\n): Promise<void> => {\n\tlet flags: GlobalFlags;\n\ttry {\n\t\tflags = makeGlobalFlags(ctx, rawFlags, rest);\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 CliInternalError({ message: 'failed to resolve CLI flags', cause });\n\t\tconst mode = outputModeFrom(rawFlags, ctx.env);\n\t\tawait Effect.runPromise(\n\t\t\temitFailure(ctx.io, mode, {\n\t\t\t\tcommand,\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror,\n\t\t\t}),\n\t\t);\n\t\treturn;\n\t}\n\tawait runCommandEffect(ctx, command, flags, effect(flags));\n};\n\nconst requiredPositional = (placeholder: string, brief: string) =>\n\t({\n\t\tparse: textParser,\n\t\tplaceholder,\n\t\tbrief,\n\t}) as const;\n\nconst optionalPositional = (placeholder: string, brief: string) =>\n\t({\n\t\tparse: textParser,\n\t\tplaceholder,\n\t\tbrief,\n\t\toptional: true as const,\n\t}) as const;\n\n// -----------------------------------------------------------------------------\n// Commands\n// -----------------------------------------------------------------------------\n\nconst upCommand = buildCommand<UpFlags, [], DevstackCliContext>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\trenderer: {\n\t\t\t\tkind: 'parsed',\n\t\t\t\tparse: rendererParser,\n\t\t\t\toptional: true,\n\t\t\t\tplaceholder: 'tui|plain|silent',\n\t\t\t\tbrief: 'Select the attached renderer',\n\t\t\t},\n\t\t\tfromSnapshot: stringFlag('Start by restoring a named snapshot before acquire', 'name-or-id'),\n\t\t\tsnapshotCache: stringFlag(\n\t\t\t\t'Use a named snapshot as a startup cache and refresh it when stale',\n\t\t\t\t'name',\n\t\t\t),\n\t\t\tsnapshotStale: {\n\t\t\t\tkind: 'parsed',\n\t\t\t\tparse: snapshotStaleParser,\n\t\t\t\toptional: true,\n\t\t\t\tplaceholder: 'warn|block|clean-start',\n\t\t\t\tbrief: 'Policy when --from-snapshot inputs differ from the current stack',\n\t\t\t},\n\t\t},\n\t},\n\tdocs: {\n\t\tbrief: 'Boot a stack and stay attached until interrupted',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'up', flags, [], (global) => this.deps.up.run(global));\n\t},\n});\n\nconst applyCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: {\n\t\tbrief: 'Reconcile a live stack or run one-shot setup',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'apply', flags, [], (global) => this.deps.apply.run(global));\n\t},\n});\n\nconst codegenCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: {\n\t\tbrief: 'Regenerate committed bindings from Move source (no stack boot)',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'codegen', flags, [], (global) =>\n\t\t\tthis.deps.codegen.run({ configPath: global.configPath }),\n\t\t);\n\t},\n});\n\nconst dumpDeploymentCommand = buildCommand<DumpDeploymentFlags, [], DevstackCliContext>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\tout: stringFlag('Write the deployment JSON to this file instead of stdout', 'path'),\n\t\t\tnetwork: stringFlag(\n\t\t\t\t'Emit a typed deployments/<network>.ts (satisfies AppNetworkDeployment) instead of the raw envelope',\n\t\t\t\t'name',\n\t\t\t),\n\t\t},\n\t},\n\tdocs: {\n\t\tbrief: 'Emit the stack deployment (deployment.json) for a real-network deploy',\n\t},\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'dump-deployment', flags, [], (global) =>\n\t\t\tthis.deps.dumpDeployment.run({\n\t\t\t\tconfigPath: global.configPath,\n\t\t\t\tout: flags.out,\n\t\t\t\tnetwork: flags.network,\n\t\t\t\tio: this.io,\n\t\t\t\toutputMode: global.outputMode,\n\t\t\t}),\n\t\t);\n\t},\n});\n\nconst statusCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'Show the current stack projection (offline: from the manifest)' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'status', flags, [], (global) =>\n\t\t\trunStatus(this.deps.status, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst doctorCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'Run host and stack preflight checks' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'doctor', flags, [], (global) =>\n\t\t\trunDoctor(this.deps.doctor, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst configCommand = buildCommand<ConfigFlags, [], DevstackCliContext>({\n\tparameters: { flags: configFlagParams },\n\tdocs: { brief: 'Print resolved config inputs' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'config', flags, [], (global) =>\n\t\t\trunConfig(this.deps.config, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst schemaCommand = buildCommand<Pick<IdentityFlags, 'json'>, [], DevstackCliContext>({\n\tparameters: { flags: { json: identityFlagParams.json } },\n\tdocs: { brief: 'Emit the CLI schema' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'schema', flags, [], (global) => {\n\t\t\tconst data = { ...commandSchema(), outputMode: global.outputMode };\n\t\t\treturn emitSuccess(this.io, global.outputMode, {\n\t\t\t\tcommand: 'schema',\n\t\t\t\telapsedMs: 0,\n\t\t\t\tdata,\n\t\t\t\thumanLines: [JSON.stringify(data, null, 2)],\n\t\t\t}).pipe(Effect.as({ exitCode: 0 }));\n\t\t});\n\t},\n});\n\nconst snapshotSaveCommand = buildCommand<\n\tSnapshotSaveFlags,\n\t[string | undefined],\n\tDevstackCliContext\n>({\n\tparameters: {\n\t\tflags: {\n\t\t\t...configFlagParams,\n\t\t\tname: stringFlag('Human-readable snapshot name', 'name'),\n\t\t},\n\t\tpositional: { kind: 'tuple', parameters: [optionalPositional('name', 'snapshot name')] },\n\t},\n\tdocs: { brief: 'Capture a snapshot' },\n\tfunc: function (flags, snapshotName) {\n\t\tconst rest = [\n\t\t\t'save',\n\t\t\t...(snapshotName === undefined ? [] : [snapshotName]),\n\t\t\t...(flags.name === undefined ? [] : ['--name', flags.name]),\n\t\t];\n\t\treturn runWithFlags(this, 'snapshot save', flags, rest, (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotRestoreCommand = buildCommand<ConfirmFlags, [string], DevstackCliContext>({\n\tparameters: {\n\t\tflags: confirmFlagParams,\n\t\tpositional: {\n\t\t\tkind: 'tuple',\n\t\t\tparameters: [requiredPositional('name-or-id', 'snapshot name or id')],\n\t\t},\n\t},\n\tdocs: { brief: 'Restore a snapshot' },\n\tfunc: function (flags, snapshotRef) {\n\t\treturn runWithFlags(this, 'snapshot restore', flags, ['restore', snapshotRef], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotListCommand = buildCommand<IdentityFlags, [], DevstackCliContext>({\n\tparameters: { flags: identityFlagParams },\n\tdocs: { brief: 'List snapshots' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'snapshot list', flags, ['list'], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotDeleteCommand = buildCommand<ConfirmFlags, [string], DevstackCliContext>({\n\tparameters: {\n\t\tflags: confirmFlagParams,\n\t\tpositional: {\n\t\t\tkind: 'tuple',\n\t\t\tparameters: [requiredPositional('name-or-id', 'snapshot name or id')],\n\t\t},\n\t},\n\tdocs: { brief: 'Delete a snapshot' },\n\tfunc: function (flags, snapshotRef) {\n\t\treturn runWithFlags(this, 'snapshot delete', flags, ['delete', snapshotRef], (global) =>\n\t\t\trunSnapshot(this.deps.snapshot, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst snapshotCommands = buildRouteMap({\n\troutes: {\n\t\tsave: snapshotSaveCommand,\n\t\trestore: snapshotRestoreCommand,\n\t\tlist: snapshotListCommand,\n\t\tdelete: snapshotDeleteCommand,\n\t},\n\tdocs: { brief: 'Capture, restore, list, or delete stack snapshots' },\n});\n\nconst pruneCommand = buildCommand<PruneFlags, [], DevstackCliContext>({\n\tparameters: { flags: pruneFlagParams },\n\tdocs: { brief: 'Inventory and prune devstack-labelled Docker resources' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'prune', flags, [], (global) =>\n\t\t\trunPrune(\n\t\t\t\tthis.deps.prune,\n\t\t\t\t{ flags: global, io: this.io },\n\t\t\t\t{\n\t\t\t\t\tmode: flags.list === true ? 'list' : flags.all === true ? 'all' : 'auto',\n\t\t\t\t\tresources: pruneResourcesFromFlags(flags),\n\t\t\t\t},\n\t\t\t),\n\t\t);\n\t},\n});\n\nconst wipeCommand = buildCommand<DestructiveFlags, [], DevstackCliContext>({\n\tparameters: { flags: destructiveFlagParams },\n\tdocs: { brief: 'Destroy all state for the selected stack' },\n\tfunc: function (flags) {\n\t\treturn runWithFlags(this, 'wipe', flags, [], (global) =>\n\t\t\trunWipe(this.deps.wipe, { flags: global, io: this.io }),\n\t\t);\n\t},\n});\n\nconst root = buildRouteMap({\n\troutes: {\n\t\tup: upCommand,\n\t\tapply: applyCommand,\n\t\tcodegen: codegenCommand,\n\t\tdumpDeployment: dumpDeploymentCommand,\n\t\tstatus: statusCommand,\n\t\tdoctor: doctorCommand,\n\t\tconfig: configCommand,\n\t\tschema: schemaCommand,\n\t\tsnapshot: snapshotCommands,\n\t\tprune: pruneCommand,\n\t\twipe: wipeCommand,\n\t},\n\tdocs: {\n\t\tbrief: 'Sui development stack CLI',\n\t},\n});\n\nconst app = buildApplication(root, {\n\tname: 'devstack',\n\tversionInfo: { currentVersion: readDevstackVersion() },\n\tscanner: { caseStyle: 'allow-kebab-for-camel' },\n\tdocumentation: {\n\t\tcaseStyle: 'convert-camel-to-kebab',\n\t\tdisableAnsiColor: true,\n\t\tonlyRequiredInUsageLine: true,\n\t},\n\tlocalization: {\n\t\tloadText: () => ({\n\t\t\t...text_en,\n\t\t\texceptionWhileParsingArguments: (error) =>\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t}),\n\t},\n});\n\nconst jsonRequested = (\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): boolean => env[ENV_VARS.JSON] === '1' || argv.includes('--json');\n\n/**\n * Project a `BufferedProcess.exitCode` to a sysexit code AT THE\n * STRICLI-PARSE BOUNDARY ONLY.\n *\n * Contract: `BufferedProcess.exitCode` is mutated ONLY by Stricli's\n * argv-parser when an argv parse step fails (unknown subcommand,\n * malformed flag value, missing required positional). Verbs route\n * their own outcomes through `ctx.io.setExitCode`, which marks\n * `io.touched()` and short-circuits this projection in `dispatch`\n * before `flushBufferedProcess` is reached.\n *\n * A non-zero value here means Stricli rejected argv before any verb\n * ran, so mapping to `USAGE` holds by construction. Kept as a named\n * function so the invariant is documented at the call site.\n */\nconst normalizeStricliExitCode = (code: number | string | null | undefined): number => {\n\tif (typeof code === 'number' && code !== 0) return ExitCode.USAGE;\n\treturn ExitCode.OK;\n};\n\n/**\n * Bridge Stricli's synchronous `StricliProcess.{stdout,stderr}.write`\n * shape to our async (Effect-based) `CliIO` surface.\n *\n * The indirection is load-bearing for the JSON-envelope contract:\n * Stricli writes argv-parse errors to stderr the moment the parser\n * trips, but `--json` mode demands the failure be EMITTED as a\n * structured envelope on stdout (not raw text on stderr) with exit\n * code `EX_USAGE`. We can't wire Stricli's `stderr.write` directly to\n * `nodeProcessIO.writeStderr` because:\n *\n * 1. The stderr bytes are the raw parser error text; in `--json`\n * mode we need to transform them into a failure envelope.\n * 2. We don't know whether the verb handler \"touched\" the IO\n * (rendered its own envelope) until after Stricli returns.\n *\n * Buffering lets us delay the decision until both signals are\n * available. Tests substitute a `BufferedProcess` whose buffers are\n * later inspected, mirroring the prod flush behavior.\n */\nconst flushBufferedProcess = (\n\tprocess: BufferedProcess,\n\tio: CliIO,\n\targv: ReadonlyArray<string>,\n\tenv: Readonly<Record<string, string | undefined>>,\n): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst exitCode = normalizeStricliExitCode(process.exitCode);\n\t\tconst stdout = process.stdoutBuffer.join('');\n\t\tconst stderr = process.stderrBuffer.join('').trim();\n\t\tif (exitCode !== 0 && jsonRequested(argv, env)) {\n\t\t\tyield* emitFailure(io, 'json', {\n\t\t\t\tcommand: '(parse)',\n\t\t\t\telapsedMs: 0,\n\t\t\t\terror: new CliUsageError({\n\t\t\t\t\tmessage: stderr.length > 0 ? stderr : 'invalid command line',\n\t\t\t\t}),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (stdout.length > 0)\n\t\t\tyield* io.writeStdout(stdout.endsWith('\\n') ? stdout.slice(0, -1) : stdout);\n\t\tif (stderr.length > 0) yield* io.writeStderr(stderr);\n\t\tyield* io.setExitCode(exitCode);\n\t});\n\nexport const dispatch = (deps: CliDeps, dispatchEnv: DispatchEnv): Effect.Effect<void> =>\n\tEffect.gen(function* () {\n\t\tconst io = trackIO(dispatchEnv.io ?? nodeProcessIO);\n\t\tconst process = makeBufferedProcess(dispatchEnv.env);\n\t\tconst ctx: DevstackCliContext = {\n\t\t\tprocess,\n\t\t\tdeps,\n\t\t\tenv: dispatchEnv.env,\n\t\t\tstdinIsTty: dispatchEnv.stdinIsTty,\n\t\t\tio,\n\t\t};\n\t\tyield* Effect.tryPromise({\n\t\t\ttry: () => runStricli(app, dispatchEnv.argv, ctx),\n\t\t\tcatch: (cause) => new CliInternalError({ message: 'CLI dispatcher failed', cause }),\n\t\t}).pipe(\n\t\t\tEffect.catch((error: CliError) =>\n\t\t\t\temitFailure(io, jsonRequested(dispatchEnv.argv, dispatchEnv.env) ? 'json' : 'human', {\n\t\t\t\t\tcommand: '(dispatch)',\n\t\t\t\t\telapsedMs: 0,\n\t\t\t\t\terror,\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tif (!io.touched()) {\n\t\t\tyield* flushBufferedProcess(process, io, dispatchEnv.argv, dispatchEnv.env);\n\t\t}\n\t});\n\n// -----------------------------------------------------------------------------\n// Re-exports\n// -----------------------------------------------------------------------------\n\nexport type { CliIO } from './output.ts';\nexport type { GlobalFlags } from './flags.ts';\nexport { COMMAND_TREE, commandSchema, VERBS, type Verb } from './command-tree.ts';\nexport type { Envelope, EnvelopeError } from './envelope.ts';\nexport {\n\tENVELOPE_SCHEMA_VERSION,\n\tfailureEnvelope,\n\tstreamingEvent,\n\tsuccessEnvelope,\n\ttype StreamingEvent,\n} from './envelope.ts';\nexport { ExitCode, exitCodeName } from './sysexits.ts';\nexport {\n\ttype CliError,\n\tCliAlreadyReportedError,\n\tCliConfigInvalidError,\n\tCliConfigNotFoundError,\n\tCliConfirmDeclinedError,\n\tCliConfirmRequiredError,\n\tCliInternalError,\n\tCliLiveGraphMismatchError,\n\tCliSnapshotNotFoundError,\n\tCliSupervisorLiveError,\n\tCliUnavailableError,\n\tCliUsageError,\n} from './errors.ts';\n"],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  #
9
9
  # Two binary sets baked in:
10
10
  #
11
- # 1. walrus / walrus-deploy / walrus-node — from the walrus release
11
+ # 1. walrus / walrus-deploy / walrus-node / walrus-upload-relay — from the walrus release
12
12
  # tarball at WALRUS_VERSION.
13
13
  #
14
14
  # 2. sui binary — from the sui release tarball at SUI_VERSION. The
@@ -26,9 +26,10 @@
26
26
  # swaps SUI for WAL, and execs `walrus-node run`.
27
27
  #
28
28
  # - /opt/walrus/scripts/run-walrus-client-service.sh — long-running
29
- # release CLI entrypoint for `walrus aggregator` and `walrus publisher`.
30
- # Relocates a deploy-generated wallet and execs the selected `walrus`
31
- # subcommand.
29
+ # release CLI entrypoint for `walrus aggregator`, `walrus publisher`,
30
+ # and `walrus-upload-relay`.
31
+ # Relocates deploy-generated client config and execs the selected
32
+ # service command.
32
33
  #
33
34
  # `ubuntu:24.04` because the walrus + sui binaries link against
34
35
  # glibc 2.38+; debian:bookworm (glibc 2.36) fails at runtime.
@@ -59,7 +60,7 @@ RUN set -eux; \
59
60
  tar -xzf /tmp/walrus.tgz -C /tmp/walrus-unpack; \
60
61
  find /tmp/walrus-unpack -maxdepth 2 -type f -executable -exec mv {} /opt/walrus/bin/ \; ; \
61
62
  chmod +x /opt/walrus/bin/*; \
62
- for bin in walrus walrus-node walrus-deploy; do \
63
+ for bin in walrus walrus-node walrus-deploy walrus-upload-relay; do \
63
64
  test -x "/opt/walrus/bin/${bin}" || { \
64
65
  echo "walrus release ${WALRUS_VERSION} missing required binary ${bin}" >&2; \
65
66
  ls -la /opt/walrus/bin >&2; \
@@ -75,6 +76,7 @@ RUN set -eux; \
75
76
  /opt/walrus/bin/walrus --version; \
76
77
  /opt/walrus/bin/walrus aggregator --help >/dev/null; \
77
78
  /opt/walrus/bin/walrus publisher --help >/dev/null; \
79
+ /opt/walrus/bin/walrus-upload-relay --help >/dev/null; \
78
80
  /opt/walrus/bin/walrus-node --help >/dev/null; \
79
81
  /opt/walrus/bin/walrus-deploy --help >/dev/null
80
82
 
@@ -109,6 +111,7 @@ RUN set -eux; \
109
111
  RUN ln -sf /opt/walrus/bin/walrus /usr/local/bin/walrus \
110
112
  && ln -sf /opt/walrus/bin/walrus-node /usr/local/bin/walrus-node \
111
113
  && ln -sf /opt/walrus/bin/walrus-deploy /usr/local/bin/walrus-deploy \
114
+ && ln -sf /opt/walrus/bin/walrus-upload-relay /usr/local/bin/walrus-upload-relay \
112
115
  && ln -sf /root/sui_bin/sui /usr/local/bin/sui
113
116
 
114
117
  RUN mkdir -p /opt/walrus/scripts /opt/walrus/outputs /var/walrus/storage
@@ -126,10 +129,11 @@ STOPSIGNAL SIGINT
126
129
  # their release CLI default unless overridden by env.
127
130
  EXPOSE 9185
128
131
  EXPOSE 31415
132
+ EXPOSE 3000
129
133
 
130
134
  # Default to storage-node entrypoint. The deploy one-shot overrides
131
135
  # via `runOneShot` argv: argv[0] == 'deploy' runs `deploy-walrus.sh`.
132
- # `aggregator` and `publisher` run the release-provided client services;
136
+ # `aggregator`, `publisher`, and `upload-relay` run release-provided client services;
133
137
  # any other command falls back to the storage-node entrypoint.
134
- ENTRYPOINT ["/bin/bash", "-c", "case \"${1:-}\" in deploy) shift; exec /opt/walrus/scripts/deploy-walrus.sh \"$@\" ;; aggregator|publisher) mode=\"$1\"; shift; exec /opt/walrus/scripts/run-walrus-client-service.sh \"$mode\" \"$@\" ;; *) exec /opt/walrus/scripts/run-walrus.sh ;; esac", "--"]
138
+ ENTRYPOINT ["/bin/bash", "-c", "case \"${1:-}\" in deploy) shift; exec /opt/walrus/scripts/deploy-walrus.sh \"$@\" ;; aggregator|publisher|upload-relay) mode=\"$1\"; shift; exec /opt/walrus/scripts/run-walrus-client-service.sh \"$mode\" \"$@\" ;; *) exec /opt/walrus/scripts/run-walrus.sh ;; esac", "--"]
135
139
  CMD []