spexcode 0.6.7 → 0.7.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +12 -7
  2. package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
  3. package/node_modules/@spexcode/session-application/dist/index.js +35 -0
  4. package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
  5. package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
  6. package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
  7. package/node_modules/@spexcode/session-application/dist/production.js +438 -0
  8. package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
  9. package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
  10. package/node_modules/@spexcode/session-application/package.json +29 -0
  11. package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
  12. package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
  13. package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
  14. package/node_modules/@spexcode/session-events/dist/index.js +245 -0
  15. package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
  16. package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
  17. package/node_modules/@spexcode/session-events/package.json +24 -0
  18. package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
  19. package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
  20. package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
  21. package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
  22. package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
  23. package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
  24. package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
  25. package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
  26. package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
  27. package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
  28. package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
  29. package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
  30. package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
  31. package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
  32. package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
  33. package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
  34. package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
  35. package/node_modules/@spexcode/session-runtime/package.json +24 -0
  36. package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
  37. package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
  38. package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
  39. package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
  40. package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
  41. package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +31 -0
  42. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +135 -0
  43. package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
  44. package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
  45. package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
  46. package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
  47. package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
  48. package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
  49. package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
  50. package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
  51. package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
  52. package/node_modules/@spexcode/session-topology/package.json +24 -0
  53. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +175 -21
  54. package/node_modules/@spexcode/spec-cli/dist/cli.js +121 -59
  55. package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
  56. package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
  57. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
  58. package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
  59. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
  60. package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
  61. package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
  62. package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
  63. package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
  64. package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
  65. package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
  66. package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
  67. package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
  68. package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
  69. package/node_modules/@spexcode/spec-cli/dist/guide.js +29 -18
  70. package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
  71. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +24 -6
  72. package/node_modules/@spexcode/spec-cli/dist/harness.js +510 -72
  73. package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
  74. package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
  75. package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
  76. package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
  77. package/node_modules/@spexcode/spec-cli/dist/host.js +98 -1
  78. package/node_modules/@spexcode/spec-cli/dist/index.js +294 -36
  79. package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
  80. package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
  81. package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
  82. package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
  83. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
  84. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +46 -15
  85. package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
  86. package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
  87. package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
  88. package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
  89. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
  90. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
  91. package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
  92. package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
  93. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
  94. package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
  95. package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
  96. package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
  97. package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
  98. package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
  99. package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
  100. package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
  101. package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
  102. package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
  103. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
  104. package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
  105. package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
  106. package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
  107. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
  108. package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
  109. package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
  110. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +112 -15
  111. package/node_modules/@spexcode/spec-cli/dist/sessions.js +1553 -764
  112. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
  113. package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
  114. package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
  115. package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
  116. package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
  117. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
  118. package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
  119. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
  120. package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
  121. package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
  122. package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
  123. package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
  124. package/node_modules/@spexcode/spec-cli/package.json +8 -6
  125. package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
  126. package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
  127. package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
  128. package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
  129. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
  130. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
  131. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  132. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
  133. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
  134. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
  135. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
  136. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
  137. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
  138. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
  139. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
  140. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
  141. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
  142. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
  143. package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
  144. package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
  145. package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
  146. package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
  147. package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
  148. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -1
  149. package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
  150. package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
  151. package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
  152. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
  153. package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
  154. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
  155. package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
  156. package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
  157. package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
  158. package/node_modules/@spexcode/spec-core/package.json +4 -1
  159. package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
  160. package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
  161. package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
  162. package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
  163. package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
  164. package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
  165. package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
  166. package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
  167. package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
  168. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  169. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  170. package/node_modules/@spexcode/transcript/dist/frames.d.ts +44 -0
  171. package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
  172. package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
  173. package/node_modules/@spexcode/transcript/dist/index.js +5 -0
  174. package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
  175. package/node_modules/@spexcode/transcript/dist/live.js +65 -0
  176. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
  177. package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
  178. package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
  179. package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
  180. package/node_modules/@spexcode/transcript/dist/turns.d.ts +42 -0
  181. package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
  182. package/node_modules/@spexcode/transcript/package.json +30 -0
  183. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  184. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  185. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  186. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  187. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  188. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  189. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  190. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  191. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  192. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  193. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  194. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  195. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  196. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  197. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  198. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  199. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  200. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  201. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  202. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  203. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  204. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  205. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  206. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  207. package/node_modules/smol-toml/LICENSE +24 -0
  208. package/node_modules/smol-toml/README.md +418 -0
  209. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  210. package/node_modules/smol-toml/dist/date.js +127 -0
  211. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  212. package/node_modules/smol-toml/dist/error.js +63 -0
  213. package/node_modules/smol-toml/dist/extract.js +69 -0
  214. package/node_modules/smol-toml/dist/index.cjs +734 -0
  215. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  216. package/node_modules/smol-toml/dist/index.js +33 -0
  217. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  218. package/node_modules/smol-toml/dist/parse.js +149 -0
  219. package/node_modules/smol-toml/dist/primitive.js +238 -0
  220. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  221. package/node_modules/smol-toml/dist/stringify.js +181 -0
  222. package/node_modules/smol-toml/dist/struct.js +179 -0
  223. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  224. package/node_modules/smol-toml/dist/util.js +89 -0
  225. package/node_modules/smol-toml/package.json +68 -0
  226. package/package.json +9 -3
  227. package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
  228. package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
  229. package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
  230. package/node_modules/@spexcode/session-core/dist/index.js +0 -5
  231. package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
  232. package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
  233. package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
  234. package/node_modules/@spexcode/session-core/dist/message.js +0 -53
  235. package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
  236. package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
  237. package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
  238. package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
  239. package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
  240. package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
  241. package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
  242. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -26
  243. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
  244. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
  245. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
  246. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
  247. package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
@@ -0,0 +1,173 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { applyComponentMigrations } from '@spexcode/session-protocol';
3
+ import { failTopology, TopologyError } from './errors.js';
4
+ import { TOPOLOGY_MIGRATIONS } from './schema.js';
5
+ const EDGE_ID = /^[0-9a-f]{32}$/;
6
+ const RELATION_TYPE = /^[0-9A-Za-z._:-]{1,64}$/;
7
+ const SESSION_ID = /^(?!-)[0-9A-Za-z_-]{1,256}$/;
8
+ const EDGE_COLUMNS = `edge_id, from_session_id, to_session_id, relation_type, created_at_ms, removed_at_ms`;
9
+ export function openTopology(protocol) {
10
+ applyComponentMigrations(protocol, 'session-topology', TOPOLOGY_MIGRATIONS);
11
+ const requireTransaction = (tx) => {
12
+ if (!tx
13
+ || typeof tx.exec !== 'function'
14
+ || typeof tx.query !== 'function'
15
+ || typeof tx.enqueue !== 'function') {
16
+ failTopology('TOPOLOGY_TRANSACTION_INVALID', 'a live protocol transaction context is required');
17
+ }
18
+ return tx;
19
+ };
20
+ const requireSessionId = (sessionId) => {
21
+ if (typeof sessionId !== 'string' || !SESSION_ID.test(sessionId)) {
22
+ failTopology('TOPOLOGY_SESSION_ID_INVALID', 'session id must match the protocol address grammar');
23
+ }
24
+ };
25
+ const requireRelationType = (relationType) => {
26
+ if (typeof relationType !== 'string' || !RELATION_TYPE.test(relationType)) {
27
+ failTopology('TOPOLOGY_RELATION_TYPE_INVALID', 'relation type must match [0-9A-Za-z._:-]{1,64}');
28
+ }
29
+ };
30
+ const requireEdgeId = (edgeId) => {
31
+ if (typeof edgeId !== 'string' || !EDGE_ID.test(edgeId)) {
32
+ failTopology('TOPOLOGY_EDGE_ID_INVALID', 'edge id must be 32 lowercase hexadecimal characters');
33
+ }
34
+ };
35
+ const toEdge = (row) => ({
36
+ edgeId: String(row.edge_id),
37
+ fromSessionId: String(row.from_session_id),
38
+ toSessionId: String(row.to_session_id),
39
+ relationType: String(row.relation_type),
40
+ createdAtMs: Number(row.created_at_ms),
41
+ removedAtMs: row.removed_at_ms === null ? null : Number(row.removed_at_ms),
42
+ });
43
+ const asEdgeRows = (rows) => rows;
44
+ const requireAddresses = (tx, fromSessionId, toSessionId) => {
45
+ const rows = tx.query('SELECT session_id FROM protocol_sessions WHERE session_id IN (?,?) ORDER BY session_id', fromSessionId, toSessionId);
46
+ const found = new Set(rows.map(row => String(row.session_id)));
47
+ if (!found.has(fromSessionId) || !found.has(toSessionId)) {
48
+ failTopology('TOPOLOGY_SESSION_UNKNOWN', 'one or more topology session addresses are unknown');
49
+ }
50
+ };
51
+ const cycleExists = (tx, fromSessionId, toSessionId, relationType) => tx.query(`WITH RECURSIVE reachable(session_id) AS (
52
+ VALUES (?)
53
+ UNION
54
+ SELECT edge.to_session_id
55
+ FROM topology_edges AS edge INDEXED BY topology_active_edge
56
+ JOIN reachable ON edge.from_session_id=reachable.session_id
57
+ WHERE edge.relation_type=? AND edge.removed_at_ms IS NULL
58
+ )
59
+ SELECT 1 AS present FROM reachable WHERE session_id=? LIMIT 1`, toSessionId, relationType, fromSessionId).length > 0;
60
+ const validateNewEdge = (tx, fromSessionId, toSessionId, relationType) => {
61
+ requireTransaction(tx);
62
+ requireSessionId(fromSessionId);
63
+ requireSessionId(toSessionId);
64
+ requireRelationType(relationType);
65
+ if (fromSessionId === toSessionId) {
66
+ failTopology('TOPOLOGY_SELF_EDGE', 'an edge cannot point from a session to itself');
67
+ }
68
+ requireAddresses(tx, fromSessionId, toSessionId);
69
+ if (cycleExists(tx, fromSessionId, toSessionId, relationType)) {
70
+ failTopology('TOPOLOGY_CYCLE_REFUSED', 'the edge would create a relation cycle');
71
+ }
72
+ };
73
+ const activeEdge = (tx, fromSessionId, toSessionId, relationType) => asEdgeRows(tx.query(`SELECT ${EDGE_COLUMNS} FROM topology_edges INDEXED BY topology_active_edge
74
+ WHERE from_session_id=? AND to_session_id=? AND relation_type=? AND removed_at_ms IS NULL`, fromSessionId, toSessionId, relationType))[0];
75
+ const insertEdge = (tx, fromSessionId, toSessionId, relationType) => {
76
+ const edgeId = randomBytes(16).toString('hex');
77
+ const createdAtMs = Date.now();
78
+ try {
79
+ tx.exec(`INSERT INTO topology_edges
80
+ (edge_id, from_session_id, to_session_id, relation_type, created_at_ms, removed_at_ms)
81
+ VALUES (?,?,?,?,?,NULL)`, edgeId, fromSessionId, toSessionId, relationType, createdAtMs);
82
+ }
83
+ catch (error) {
84
+ const code = String(error?.code ?? '');
85
+ if (code.includes('UNIQUE') || code.includes('PRIMARYKEY')) {
86
+ failTopology('TOPOLOGY_EDGE_EXISTS', 'the active topology edge already exists', error);
87
+ }
88
+ if (code.includes('FOREIGNKEY')) {
89
+ failTopology('TOPOLOGY_SESSION_UNKNOWN', 'one or more topology session addresses are unknown', error);
90
+ }
91
+ failTopology('TOPOLOGY_STORAGE_ERROR', 'the topology edge could not be stored', error);
92
+ }
93
+ return { edgeId, fromSessionId, toSessionId, relationType, createdAtMs, removedAtMs: null };
94
+ };
95
+ const attach = (tx, fromSessionId, toSessionId, relationType) => {
96
+ validateNewEdge(tx, fromSessionId, toSessionId, relationType);
97
+ if (activeEdge(tx, fromSessionId, toSessionId, relationType)) {
98
+ failTopology('TOPOLOGY_EDGE_EXISTS', 'the active topology edge already exists');
99
+ }
100
+ return insertEdge(tx, fromSessionId, toSessionId, relationType);
101
+ };
102
+ const detach = (tx, edgeId) => {
103
+ requireTransaction(tx);
104
+ requireEdgeId(edgeId);
105
+ const row = asEdgeRows(tx.query(`SELECT ${EDGE_COLUMNS} FROM topology_edges WHERE edge_id=? AND removed_at_ms IS NULL`, edgeId))[0];
106
+ if (!row)
107
+ failTopology('TOPOLOGY_EDGE_UNKNOWN', 'the active topology edge does not exist');
108
+ const removedAtMs = Date.now();
109
+ const result = tx.exec('UPDATE topology_edges SET removed_at_ms=? WHERE edge_id=? AND removed_at_ms IS NULL', removedAtMs, edgeId);
110
+ if (result.changes !== 1) {
111
+ failTopology('TOPOLOGY_EDGE_UNKNOWN', 'the active topology edge does not exist');
112
+ }
113
+ return { ...toEdge(row), removedAtMs };
114
+ };
115
+ const withRead = (tx, query) => {
116
+ if (tx !== undefined)
117
+ return query(requireTransaction(tx));
118
+ return protocol.withTransaction(query);
119
+ };
120
+ const queryEdges = (sessionId, relationType, tx, direction) => {
121
+ requireSessionId(sessionId);
122
+ if (relationType !== undefined)
123
+ requireRelationType(relationType);
124
+ return withRead(tx, active => {
125
+ const index = direction === 'from' ? 'topology_active_edge' : 'topology_active_to';
126
+ const column = direction === 'from' ? 'from_session_id' : 'to_session_id';
127
+ const relation = relationType === undefined ? '' : ' AND relation_type=?';
128
+ const params = relationType === undefined ? [sessionId] : [sessionId, relationType];
129
+ return asEdgeRows(active.query(`SELECT ${EDGE_COLUMNS} FROM topology_edges INDEXED BY ${index}
130
+ WHERE ${column}=? AND removed_at_ms IS NULL${relation}
131
+ ORDER BY relation_type, from_session_id, to_session_id, created_at_ms, edge_id`, ...params)).map(toEdge);
132
+ });
133
+ };
134
+ return {
135
+ attach,
136
+ detach,
137
+ reparent(tx, subjectSessionId, nextFromSessionId, relationType) {
138
+ validateNewEdge(tx, nextFromSessionId, subjectSessionId, relationType);
139
+ const removedAtMs = Date.now();
140
+ tx.exec(`UPDATE topology_edges SET removed_at_ms=?
141
+ WHERE to_session_id=? AND relation_type=? AND removed_at_ms IS NULL`, removedAtMs, subjectSessionId, relationType);
142
+ return insertEdge(tx, nextFromSessionId, subjectSessionId, relationType);
143
+ },
144
+ subscribe(tx, watcherSessionId, subjectSessionId, channel) {
145
+ return attach(tx, watcherSessionId, subjectSessionId, channel);
146
+ },
147
+ unsubscribe(tx, watcherSessionId, subjectSessionId, channel) {
148
+ requireTransaction(tx);
149
+ requireSessionId(watcherSessionId);
150
+ requireSessionId(subjectSessionId);
151
+ requireRelationType(channel);
152
+ const row = activeEdge(tx, watcherSessionId, subjectSessionId, channel);
153
+ if (!row)
154
+ failTopology('TOPOLOGY_EDGE_UNKNOWN', 'the active topology edge does not exist');
155
+ return detach(tx, String(row.edge_id));
156
+ },
157
+ parents(sessionId, relationType, tx) {
158
+ return queryEdges(sessionId, relationType, tx, 'to');
159
+ },
160
+ children(sessionId, relationType, tx) {
161
+ return queryEdges(sessionId, relationType, tx, 'from');
162
+ },
163
+ subscriptions(sessionId, tx) {
164
+ return queryEdges(sessionId, undefined, tx, 'from');
165
+ },
166
+ recipients(subjectSessionId, tx) {
167
+ requireSessionId(subjectSessionId);
168
+ return withRead(tx, active => active.query(`SELECT DISTINCT from_session_id FROM topology_edges INDEXED BY topology_active_to
169
+ WHERE to_session_id=? AND removed_at_ms IS NULL ORDER BY from_session_id`, subjectSessionId).map(row => String(row.from_session_id)));
170
+ },
171
+ };
172
+ }
173
+ export { TopologyError };
@@ -0,0 +1,3 @@
1
+ import type { ComponentMigration } from '@spexcode/session-protocol';
2
+ export declare const TOPOLOGY_MIGRATION_SQL = "\nCREATE TABLE topology_edges (\n edge_id TEXT NOT NULL PRIMARY KEY,\n from_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),\n to_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),\n relation_type TEXT NOT NULL,\n created_at_ms INTEGER NOT NULL,\n removed_at_ms INTEGER,\n CHECK (length(edge_id) = 32 AND edge_id NOT GLOB '*[^0-9a-f]*'),\n CHECK (from_session_id <> to_session_id),\n CHECK (length(relation_type) BETWEEN 1 AND 64),\n CHECK (relation_type NOT GLOB '*[^0-9A-Za-z._:-]*'),\n CHECK (created_at_ms >= 0),\n CHECK (removed_at_ms IS NULL OR removed_at_ms >= 0)\n) STRICT;\n\nCREATE UNIQUE INDEX topology_active_edge\n ON topology_edges (from_session_id, to_session_id, relation_type)\n WHERE removed_at_ms IS NULL;\n\nCREATE INDEX topology_active_to\n ON topology_edges (to_session_id, relation_type, from_session_id)\n WHERE removed_at_ms IS NULL;\n";
3
+ export declare const TOPOLOGY_MIGRATIONS: readonly ComponentMigration[];
@@ -0,0 +1,27 @@
1
+ export const TOPOLOGY_MIGRATION_SQL = `
2
+ CREATE TABLE topology_edges (
3
+ edge_id TEXT NOT NULL PRIMARY KEY,
4
+ from_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
5
+ to_session_id TEXT NOT NULL REFERENCES protocol_sessions(session_id),
6
+ relation_type TEXT NOT NULL,
7
+ created_at_ms INTEGER NOT NULL,
8
+ removed_at_ms INTEGER,
9
+ CHECK (length(edge_id) = 32 AND edge_id NOT GLOB '*[^0-9a-f]*'),
10
+ CHECK (from_session_id <> to_session_id),
11
+ CHECK (length(relation_type) BETWEEN 1 AND 64),
12
+ CHECK (relation_type NOT GLOB '*[^0-9A-Za-z._:-]*'),
13
+ CHECK (created_at_ms >= 0),
14
+ CHECK (removed_at_ms IS NULL OR removed_at_ms >= 0)
15
+ ) STRICT;
16
+
17
+ CREATE UNIQUE INDEX topology_active_edge
18
+ ON topology_edges (from_session_id, to_session_id, relation_type)
19
+ WHERE removed_at_ms IS NULL;
20
+
21
+ CREATE INDEX topology_active_to
22
+ ON topology_edges (to_session_id, relation_type, from_session_id)
23
+ WHERE removed_at_ms IS NULL;
24
+ `;
25
+ export const TOPOLOGY_MIGRATIONS = [
26
+ { version: 1, sql: TOPOLOGY_MIGRATION_SQL },
27
+ ];
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@spexcode/session-topology",
3
+ "version": "0.7.0-next.0",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "description": "A neutral SQLite relation model for session addresses.",
7
+ "files": ["dist"],
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "engines": { "node": ">=22" },
13
+ "scripts": {
14
+ "build": "node ../../scripts/build-dist.mjs",
15
+ "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
16
+ "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
17
+ },
18
+ "dependencies": { "@spexcode/session-protocol": "0.7.0-next.0" },
19
+ "devDependencies": {
20
+ "@types/node": "^20.16.0",
21
+ "tsx": "^4.19.2",
22
+ "typescript": "^5.6.3"
23
+ }
24
+ }
@@ -2,7 +2,8 @@
2
2
  // @@@ spex launcher ([[release-launcher]]) - package installs execute the package's compiled CLI directly;
3
3
  // tsx remains a development tool and never enters an adopter's runtime closure.
4
4
  import { spawn, spawnSync } from 'node:child_process'
5
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
5
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs'
6
+ import { createHash } from 'node:crypto'
6
7
  import { fileURLToPath } from 'node:url'
7
8
  import { dirname, join } from 'node:path'
8
9
 
@@ -16,7 +17,19 @@ const workspace = join(pkg, '..')
16
17
  // carry unresolved conflict markers, so hooks keep the retryable exit-75 contract during a merge.
17
18
  const sourceRoot = join(pkg, 'src')
18
19
  if (existsSync(sourceRoot)) {
19
- const srcRoots = [sourceRoot, join(pkg, '..', 'packages', 'spec-core', 'src'), join(pkg, '..', 'packages', 'session-core', 'src'), join(pkg, '..', 'spec-eval', 'src'), join(pkg, '..', 'spec-forge', 'src')]
20
+ const buildTargets = [
21
+ ['packages/session-protocol', 'dist/index.js'],
22
+ ['packages/session-topology', 'dist/index.js'],
23
+ ['packages/session-runtime', 'dist/index.js'],
24
+ ['packages/session-events', 'dist/index.js'],
25
+ ['packages/session-application', 'dist/index.js'],
26
+ ['packages/session-selflaunch', 'dist/index.js'],
27
+ ['packages/spec-core', 'dist/index.js'],
28
+ ['spec-eval', 'dist/index.js'],
29
+ ['spec-forge', 'dist/index.js'],
30
+ ['spec-cli', 'dist/cli.js'],
31
+ ]
32
+ const srcRoots = buildTargets.map(([packagePath]) => join(pkg, '..', packagePath, 'src'))
20
33
  const conflicted = srcRoots.flatMap((root) => {
21
34
  if (!existsSync(root)) return []
22
35
  return readdirSync(root, { recursive: true })
@@ -34,33 +47,174 @@ if (existsSync(sourceRoot)) {
34
47
  }
35
48
 
36
49
  // @@@ source-workspace build ([[release-launcher]]) - the git hooks invoke this launcher straight from a
37
- // checkout, where dist is intentionally untracked. Rebuild the complete runtime closure before linting
38
- // candidate source; an installed package has no src tree and stays a pure Node -> dist execution path.
50
+ // checkout, where dist is intentionally untracked. Import main's fresh closure and rebuild only changed
51
+ // packages plus dependents before linting candidate source; an untrusted baseline falls back to the complete
52
+ // ordered driver. Every package still publishes its dist atomically. An installed package has no src tree
53
+ // and stays a pure Node -> dist execution path.
39
54
  const runtimeEntries = [
40
55
  cli,
41
56
  join(workspace, 'packages', 'spec-core', 'dist', 'index.js'),
42
- join(workspace, 'packages', 'session-core', 'dist', 'index.js'),
43
57
  join(workspace, 'spec-eval', 'dist', 'index.js'),
44
58
  join(workspace, 'spec-forge', 'dist', 'index.js'),
45
59
  ]
46
- const newestSource = srcRoots.reduce((newest, root) => {
47
- if (!existsSync(root)) return newest
48
- for (const entry of readdirSync(root, { recursive: true })) {
49
- const path = join(root, String(entry))
50
- try {
51
- if (/\.(ts|tsx|js|mjs)$/.test(path)) newest = Math.max(newest, statSync(path).mtimeMs)
52
- } catch { /* a concurrent source edit can only make the next invocation rebuild again */ }
60
+ // Tests and declaration files are typecheck inputs, not runtime build inputs. Counting them here makes a
61
+ // harmless test edit look like a stale CLI and rebuilds every workspace on the next hook invocation.
62
+ const isRuntimeSource = (path) => {
63
+ return /\.(ts|tsx|js|mjs)$/.test(path)
64
+ && !/\.d\.ts$/.test(path)
65
+ && !/(^|[./])[^/]+\.test\.(ts|tsx|js|mjs)$/.test(path)
66
+ }
67
+ const newestRuntimeSource = (root) => {
68
+ const roots = buildTargets.map(([packagePath]) => join(root, packagePath, 'src'))
69
+ return roots.reduce((newest, root) => {
70
+ if (!existsSync(root)) return newest
71
+ for (const entry of readdirSync(root, { recursive: true })) {
72
+ const path = join(root, String(entry))
73
+ try {
74
+ if (isRuntimeSource(path)) newest = Math.max(newest, statSync(path).mtimeMs)
75
+ } catch { /* a concurrent source edit can only make the next invocation rebuild again */ }
76
+ }
77
+ return newest
78
+ }, 0)
79
+ }
80
+ const newestPackageSource = (root, packagePath) => {
81
+ const source = join(root, packagePath, 'src')
82
+ if (!existsSync(source)) return 0
83
+ let newest = 0
84
+ const visit = (dir) => {
85
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
86
+ const path = join(dir, entry.name)
87
+ if (entry.isDirectory()) visit(path)
88
+ else if (isRuntimeSource(path)) {
89
+ try { newest = Math.max(newest, statSync(path).mtimeMs) } catch { /* retry on the next launch */ }
90
+ }
91
+ }
53
92
  }
93
+ visit(source)
54
94
  return newest
55
- }, 0)
56
- const rebuild = runtimeEntries.some((entry) => {
57
- try { return statSync(entry).mtimeMs < newestSource } catch { return true }
58
- })
59
- if (rebuild) {
60
- const build = spawnSync('npm', ['run', 'build'], { cwd: workspace, stdio: 'inherit' })
61
- if (build.error || build.status !== 0 || !existsSync(cli)) {
62
- console.error('spex: source workspace build failed; fix it, then retry.')
63
- process.exit(build.status ?? 1)
95
+ }
96
+ const entriesAreFresh = (root) => {
97
+ const newestSource = newestRuntimeSource(root)
98
+ return buildTargets.every(([packagePath, entry]) => {
99
+ try { return statSync(join(root, packagePath, entry)).mtimeMs >= newestSource } catch { return false }
100
+ })
101
+ }
102
+ const packageFingerprint = (root, packagePath) => {
103
+ const hash = createHash('sha256')
104
+ const packageRoot = join(root, packagePath)
105
+ const files = ['package.json', 'tsconfig.build.json'].map((name) => join(packageRoot, name)).filter(existsSync)
106
+ const visit = (dir, relative = '') => {
107
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
108
+ const path = join(dir, entry.name)
109
+ const child = relative ? `${relative}/${entry.name}` : entry.name
110
+ if (entry.isDirectory()) visit(path, child)
111
+ else if (isRuntimeSource(path)) files.push(path)
112
+ }
113
+ }
114
+ const source = join(packageRoot, 'src')
115
+ if (existsSync(source)) visit(source)
116
+ files.sort()
117
+ for (const path of files) hash.update(path.slice(packageRoot.length)).update('\0').update(readFileSync(path)).update('\0')
118
+ return hash.digest('hex')
119
+ }
120
+ const packageDependents = {
121
+ 'packages/session-protocol': ['packages/session-topology', 'packages/session-runtime', 'packages/session-events', 'packages/session-application', 'packages/session-selflaunch', 'spec-cli'],
122
+ 'packages/session-topology': ['packages/session-application', 'spec-cli'],
123
+ 'packages/session-runtime': ['packages/session-application', 'packages/session-selflaunch', 'spec-cli'],
124
+ 'packages/session-events': ['packages/session-application', 'spec-cli'],
125
+ 'packages/session-application': ['spec-cli'],
126
+ 'packages/session-selflaunch': ['spec-cli'],
127
+ 'packages/spec-core': ['spec-eval', 'spec-forge', 'spec-cli'],
128
+ 'spec-eval': ['spec-cli'],
129
+ 'spec-forge': ['spec-cli'],
130
+ 'spec-cli': [],
131
+ }
132
+ const changedBuildPackages = (root) => {
133
+ const changed = new Set(buildTargets
134
+ .map(([packagePath]) => packagePath)
135
+ .filter((packagePath) => packageFingerprint(workspace, packagePath) !== packageFingerprint(root, packagePath)))
136
+ for (const [packagePath, entry] of buildTargets) {
137
+ try {
138
+ if (statSync(join(root, packagePath, entry)).mtimeMs < newestPackageSource(workspace, packagePath)) changed.add(packagePath)
139
+ } catch { changed.add(packagePath) }
140
+ }
141
+ for (const packagePath of [...changed]) for (const dependent of packageDependents[packagePath]) changed.add(dependent)
142
+ return buildTargets.map(([packagePath]) => packagePath).filter((packagePath) => changed.has(packagePath))
143
+ }
144
+ const mainRoot = (() => {
145
+ const result = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: workspace, encoding: 'utf8' })
146
+ if (result.status !== 0) return null
147
+ const commonDir = result.stdout.trim()
148
+ return commonDir ? dirname(commonDir) : null
149
+ })()
150
+ const reusableMainBuild = () => mainRoot && mainRoot !== workspace && entriesAreFresh(mainRoot)
151
+ const importMainBuild = () => {
152
+ for (const [packagePath] of buildTargets) {
153
+ const source = join(mainRoot, packagePath, 'dist')
154
+ const target = join(workspace, packagePath, 'dist')
155
+ const temporary = `${target}.from-main-${process.pid}`
156
+ const previous = `${target}.from-main-previous-${process.pid}`
157
+ rmSync(temporary, { recursive: true, force: true })
158
+ rmSync(previous, { recursive: true, force: true })
159
+ cpSync(source, temporary, { recursive: true })
160
+ if (existsSync(target)) renameSync(target, previous)
161
+ renameSync(temporary, target)
162
+ rmSync(previous, { recursive: true, force: true })
163
+ }
164
+ }
165
+ const sourceIsStale = () => {
166
+ const newestSource = newestRuntimeSource(workspace)
167
+ return runtimeEntries.some((entry) => {
168
+ try { return statSync(entry).mtimeMs < newestSource } catch { return true }
169
+ })
170
+ }
171
+ if (sourceIsStale()) {
172
+ // A hook, poller, and human shell can all invoke spex at once. The lock is deliberately at the
173
+ // launcher boundary: only one process pays the full workspace build, and waiters re-check freshness
174
+ // after the owner exits. A dead owner is recoverable; a live build gets two minutes before it is stale.
175
+ const buildLock = join(workspace, '.spex-build.lock')
176
+ const wait = new Int32Array(new SharedArrayBuffer(4))
177
+ const deadline = Date.now() + 120_000
178
+ let owner = false
179
+ while (!owner) {
180
+ try {
181
+ mkdirSync(buildLock)
182
+ owner = true
183
+ } catch (error) {
184
+ if (error?.code !== 'EEXIST') throw error
185
+ try {
186
+ if (Date.now() - statSync(buildLock).mtimeMs > 120_000) rmSync(buildLock, { recursive: true, force: true })
187
+ } catch { /* the owner may have completed between the stat and the next check */ }
188
+ if (Date.now() >= deadline) {
189
+ console.error('spex: source workspace build lock did not clear; remove .spex-build.lock after confirming no build is running.')
190
+ process.exit(75)
191
+ }
192
+ Atomics.wait(wait, 0, 0, 100)
193
+ }
194
+ }
195
+ try {
196
+ if (sourceIsStale()) {
197
+ if (reusableMainBuild()) {
198
+ const changed = changedBuildPackages(mainRoot)
199
+ console.error(`spex: importing fresh runtime closure from main; rebuilding ${changed.length} package(s)`)
200
+ importMainBuild()
201
+ if (changed.length) {
202
+ const build = spawnSync(process.execPath, [join(workspace, 'scripts', 'build-workspaces.mjs'), ...changed], { cwd: workspace, stdio: 'inherit' })
203
+ if (build.error || build.status !== 0 || !existsSync(cli)) {
204
+ console.error('spex: source workspace selective build failed; fix it, then retry.')
205
+ process.exit(build.status ?? 1)
206
+ }
207
+ }
208
+ } else {
209
+ const build = spawnSync('npm', ['run', 'build'], { cwd: workspace, stdio: 'inherit' })
210
+ if (build.error || build.status !== 0 || !existsSync(cli)) {
211
+ console.error('spex: source workspace build failed; fix it, then retry.')
212
+ process.exit(build.status ?? 1)
213
+ }
214
+ }
215
+ }
216
+ } finally {
217
+ rmSync(buildLock, { recursive: true, force: true })
64
218
  }
65
219
  }
66
220
  }