@superpanel-sdk/runtime 0.2.3 → 0.7.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 (319) hide show
  1. package/README.md +30 -10
  2. package/dist/builder/codegen/emit.d.ts +25 -12
  3. package/dist/builder/codegen/emit.js +339 -78
  4. package/dist/builder/codegen/generate.d.ts +21 -2
  5. package/dist/builder/codegen/generate.js +147 -34
  6. package/dist/builder/company-import-graph.d.ts +2 -0
  7. package/dist/builder/company-import-graph.js +40 -8
  8. package/dist/builder/company-imports.d.ts +29 -0
  9. package/dist/builder/company-imports.js +122 -0
  10. package/dist/builder/compile/compile-sequence.d.ts +61 -8
  11. package/dist/builder/compile/compile-sequence.js +334 -80
  12. package/dist/builder/compile/function-code.d.ts +19 -3
  13. package/dist/builder/compile/function-code.js +123 -45
  14. package/dist/builder/compile/registry.d.ts +7 -3
  15. package/dist/builder/compile/registry.js +30 -27
  16. package/dist/builder/errors.d.ts +9 -0
  17. package/dist/builder/errors.js +17 -0
  18. package/dist/builder/function-params.d.ts +10 -0
  19. package/dist/builder/function-params.js +203 -0
  20. package/dist/builder/load.d.ts +70 -10
  21. package/dist/builder/load.js +258 -67
  22. package/dist/builder/paths.d.ts +9 -1
  23. package/dist/builder/paths.js +32 -2
  24. package/dist/builder/schedules.d.ts +25 -0
  25. package/dist/builder/schedules.js +101 -0
  26. package/dist/builder/slot-schema-validation.d.ts +4 -0
  27. package/dist/builder/slot-schema-validation.js +10 -0
  28. package/dist/builder/typecheck.d.ts +8 -0
  29. package/dist/builder/typecheck.js +102 -0
  30. package/dist/builder/types.d.ts +127 -19
  31. package/dist/builder/types.js +21 -1
  32. package/dist/cli/auth/auth.d.ts +37 -0
  33. package/dist/cli/auth/auth.js +211 -0
  34. package/dist/cli/auth/backend-url.d.ts +5 -0
  35. package/dist/cli/auth/backend-url.js +25 -0
  36. package/dist/cli/auth/clipboard.d.ts +9 -0
  37. package/dist/cli/auth/clipboard.js +38 -0
  38. package/dist/cli/auth/http.d.ts +20 -0
  39. package/dist/cli/auth/http.js +27 -0
  40. package/dist/cli/auth/index.d.ts +10 -0
  41. package/dist/cli/auth/index.js +27 -0
  42. package/dist/cli/auth/progress.d.ts +12 -0
  43. package/dist/cli/auth/progress.js +15 -0
  44. package/dist/cli/auth/serve-login.d.ts +8 -0
  45. package/dist/cli/auth/serve-login.js +24 -0
  46. package/dist/cli/calls/index.d.ts +177 -0
  47. package/dist/cli/calls/index.js +96 -0
  48. package/dist/cli/compile/index.js +3 -0
  49. package/dist/cli/config.d.ts +10 -0
  50. package/dist/cli/config.js +52 -0
  51. package/dist/cli/debug/index.d.ts +31 -0
  52. package/dist/cli/debug/index.js +119 -0
  53. package/dist/cli/exec/index.d.ts +39 -0
  54. package/dist/cli/exec/index.js +138 -0
  55. package/dist/cli/exec/session.d.ts +48 -0
  56. package/dist/cli/exec/session.js +82 -0
  57. package/dist/cli/exec/trace.d.ts +18 -0
  58. package/dist/cli/exec/trace.js +52 -0
  59. package/dist/cli/index.js +112 -9
  60. package/dist/cli/logs/index.d.ts +47 -0
  61. package/dist/cli/logs/index.js +113 -0
  62. package/dist/cli/mobile/index.d.ts +12 -0
  63. package/dist/cli/mobile/index.js +405 -0
  64. package/dist/cli/serve/index.d.ts +109 -3
  65. package/dist/cli/serve/index.js +506 -118
  66. package/dist/cli/turns/index.d.ts +105 -0
  67. package/dist/cli/turns/index.js +128 -0
  68. package/dist/cli/turns/run.d.ts +13 -0
  69. package/dist/cli/turns/run.js +57 -0
  70. package/dist/cli/turns/shadow.d.ts +112 -0
  71. package/dist/cli/turns/shadow.js +54 -0
  72. package/dist/cli/type-gen/index.js +9 -9
  73. package/dist/compiler.d.ts +7 -0
  74. package/dist/compiler.js +7 -0
  75. package/dist/compute-host/compute-host.d.ts +14 -23
  76. package/dist/compute-host/compute-host.js +18 -30
  77. package/dist/compute-host/index.d.ts +2 -4
  78. package/dist/compute-host/index.js +1 -2
  79. package/dist/compute-host/source-compute-host.d.ts +10 -10
  80. package/dist/compute-host/source-compute-host.js +33 -40
  81. package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
  82. package/dist/core/case-orchestrator/contact-hours.js +127 -0
  83. package/dist/core/case-orchestrator/index.d.ts +3 -0
  84. package/dist/core/case-orchestrator/index.js +3 -0
  85. package/dist/core/case-orchestrator/render.d.ts +75 -0
  86. package/dist/core/case-orchestrator/render.js +244 -0
  87. package/dist/core/case-orchestrator/time.d.ts +11 -0
  88. package/dist/core/case-orchestrator/time.js +29 -0
  89. package/dist/core/case-orchestrator/types.d.ts +708 -0
  90. package/dist/core/case-orchestrator/types.js +42 -0
  91. package/dist/core/context/call.d.ts +291 -0
  92. package/dist/core/context/call.js +94 -0
  93. package/dist/core/context/channels.d.ts +60 -0
  94. package/dist/core/context/channels.js +35 -0
  95. package/dist/core/context/contact.d.ts +96 -6
  96. package/dist/core/context/contact.js +48 -2
  97. package/dist/core/context/email-message.d.ts +198 -0
  98. package/dist/core/context/email-message.js +86 -0
  99. package/dist/core/context/email.d.ts +109 -0
  100. package/dist/core/context/email.js +90 -0
  101. package/dist/core/context/exchanges.d.ts +167 -0
  102. package/dist/core/context/exchanges.js +81 -0
  103. package/dist/core/context/instances.d.ts +307 -0
  104. package/dist/core/context/instances.js +137 -0
  105. package/dist/core/context/kv.d.ts +113 -0
  106. package/dist/core/context/kv.js +60 -0
  107. package/dist/core/context/platform-registry.d.ts +544 -4
  108. package/dist/core/context/platform-registry.js +14 -2
  109. package/dist/core/context/platform.d.ts +2 -1
  110. package/dist/core/context/sms.d.ts +177 -0
  111. package/dist/core/context/sms.js +77 -0
  112. package/dist/core/context/types.d.ts +79 -48
  113. package/dist/core/context/types.js +1 -7
  114. package/dist/core/conversation/index.d.ts +85 -0
  115. package/dist/core/conversation/index.js +133 -0
  116. package/dist/core/functions/errors.d.ts +74 -0
  117. package/dist/core/functions/errors.js +26 -0
  118. package/dist/core/functions/events.d.ts +192 -0
  119. package/dist/core/functions/events.js +23 -0
  120. package/dist/core/functions/schedules.d.ts +50 -0
  121. package/dist/core/functions/schedules.js +20 -0
  122. package/dist/core/functions/types.d.ts +29 -73
  123. package/dist/core/functions/types.js +1 -4
  124. package/dist/core/office-hours/index.d.ts +39 -0
  125. package/dist/core/office-hours/index.js +187 -0
  126. package/dist/core/page/define.d.ts +904 -0
  127. package/dist/core/page/define.js +518 -0
  128. package/dist/core/page/envelope.d.ts +55 -0
  129. package/dist/core/page/envelope.js +4 -0
  130. package/dist/core/page/fns.d.ts +448 -0
  131. package/dist/core/page/fns.js +539 -0
  132. package/dist/core/page/memory-reader.d.ts +27 -0
  133. package/dist/core/page/memory-reader.js +299 -0
  134. package/dist/core/page/ops.d.ts +58 -0
  135. package/dist/core/page/ops.js +220 -0
  136. package/dist/core/page/page-host.d.ts +20 -0
  137. package/dist/core/page/page-host.js +57 -0
  138. package/dist/core/page/records-query.d.ts +234 -0
  139. package/dist/core/page/records-query.js +780 -0
  140. package/dist/core/page/render.d.ts +295 -0
  141. package/dist/core/page/render.js +1060 -0
  142. package/dist/core/page/sandbox.d.ts +85 -0
  143. package/dist/core/page/sandbox.js +357 -0
  144. package/dist/core/page/time-format.d.ts +46 -0
  145. package/dist/core/page/time-format.js +125 -0
  146. package/dist/core/page/timeline.d.ts +40 -0
  147. package/dist/core/page/timeline.js +2 -0
  148. package/dist/core/page/typed.d.ts +648 -0
  149. package/dist/core/page/typed.js +2 -0
  150. package/dist/core/page/workspace.d.ts +57 -0
  151. package/dist/core/page/workspace.js +266 -0
  152. package/dist/core/party/index.d.ts +3 -0
  153. package/dist/core/party/index.js +3 -0
  154. package/dist/core/party/schema.d.ts +208 -0
  155. package/dist/core/party/schema.js +113 -0
  156. package/dist/core/party/types.d.ts +35 -0
  157. package/dist/core/party/types.js +15 -0
  158. package/dist/core/sequence/define.d.ts +56 -0
  159. package/dist/core/sequence/define.js +10 -0
  160. package/dist/core/sequence/index.d.ts +63 -0
  161. package/dist/core/sequence/index.js +231 -0
  162. package/dist/core/slot/binding.d.ts +40 -0
  163. package/dist/core/slot/binding.js +2 -0
  164. package/dist/core/slot/company-metadata.d.ts +102 -0
  165. package/dist/core/slot/company-metadata.js +536 -0
  166. package/dist/core/slot/compile-schema.d.ts +19 -13
  167. package/dist/core/slot/compile-schema.js +51 -248
  168. package/dist/core/slot/contact-timezone.d.ts +110 -0
  169. package/dist/core/slot/contact-timezone.js +186 -0
  170. package/dist/core/slot/field.d.ts +2 -25
  171. package/dist/core/slot/field.js +0 -71
  172. package/dist/core/slot/id-ref.d.ts +17 -2
  173. package/dist/core/slot/id-ref.js +16 -8
  174. package/dist/core/slot/index.d.ts +12 -13
  175. package/dist/core/slot/index.js +11 -12
  176. package/dist/core/slot/naming.d.ts +15 -0
  177. package/dist/core/slot/naming.js +96 -0
  178. package/dist/core/slot/platform-fields.d.ts +119 -0
  179. package/dist/core/slot/platform-fields.js +121 -0
  180. package/dist/core/slot/ref.d.ts +16 -0
  181. package/dist/core/slot/ref.js +14 -0
  182. package/dist/core/slot/row.d.ts +2 -9
  183. package/dist/core/slot/row.js +13 -23
  184. package/dist/core/slot/schema-compat.d.ts +17 -0
  185. package/dist/core/slot/schema-compat.js +103 -0
  186. package/dist/core/slot/schema.d.ts +353 -76
  187. package/dist/core/slot/schema.js +163 -10
  188. package/dist/core/slot/types.d.ts +30 -103
  189. package/dist/core/slot/value.d.ts +28 -3
  190. package/dist/core/slot/value.js +93 -57
  191. package/dist/dev-overlay/protocol.d.ts +74 -8
  192. package/dist/fn-context/build-context.d.ts +3 -1
  193. package/dist/fn-context/build-context.js +26 -10
  194. package/dist/fn-context/fake/empty-api.js +9 -28
  195. package/dist/fn-context/fake/index.d.ts +13 -4
  196. package/dist/fn-context/fake/index.js +29 -2
  197. package/dist/fn-context/fn-params.d.ts +3 -0
  198. package/dist/fn-context/fn-params.js +10 -0
  199. package/dist/fn-context/index.d.ts +5 -5
  200. package/dist/fn-context/index.js +4 -4
  201. package/dist/fn-context/log.js +2 -2
  202. package/dist/fn-context/make-ctx.d.ts +11 -0
  203. package/dist/fn-context/make-ctx.js +20 -9
  204. package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
  205. package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
  206. package/dist/fn-context/rpc/errors.d.ts +13 -0
  207. package/dist/fn-context/rpc/errors.js +22 -0
  208. package/dist/fn-context/rpc/index.d.ts +4 -2
  209. package/dist/fn-context/rpc/index.js +4 -2
  210. package/dist/fn-context/rpc/integration/index.d.ts +15 -5
  211. package/dist/fn-context/rpc/integration/index.js +1 -1
  212. package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
  213. package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
  214. package/dist/fn-context/rpc/log.d.ts +11 -1
  215. package/dist/fn-context/rpc/platform.d.ts +13 -3
  216. package/dist/fn-context/rpc/platform.js +1 -1
  217. package/dist/fn-context/rpc/records.d.ts +85 -0
  218. package/dist/fn-context/rpc/records.js +24 -0
  219. package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
  220. package/dist/fn-context/rpc/trpc-client.js +222 -12
  221. package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
  222. package/dist/fn-context/rpc/trpc-init.js +10 -4
  223. package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
  224. package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
  225. package/dist/fn-context/run-scope.d.ts +4 -0
  226. package/dist/fn-context/run-scope.js +9 -0
  227. package/dist/fn-context/types.d.ts +52 -5
  228. package/dist/index.d.ts +36 -46
  229. package/dist/index.js +30 -33
  230. package/dist/logger.d.ts +11 -0
  231. package/dist/logger.js +49 -0
  232. package/dist/platform.d.ts +75 -0
  233. package/dist/platform.js +57 -0
  234. package/dist/testing-compile-child.d.ts +2 -0
  235. package/dist/testing-compile-child.js +20 -0
  236. package/dist/testing.d.ts +368 -0
  237. package/dist/testing.js +439 -0
  238. package/dist/voice-service/actions.d.ts +50 -6
  239. package/dist/voice-service/index.d.ts +2 -0
  240. package/dist/voice-service/index.js +2 -0
  241. package/dist/voice-service/protocol.d.ts +604 -0
  242. package/dist/voice-service/protocol.js +67 -0
  243. package/dist/voice-service/types.d.ts +55 -8
  244. package/dist/worker.d.ts +11 -0
  245. package/dist/worker.js +11 -0
  246. package/package.json +38 -15
  247. package/dist/builder/compile/compile-agenda.d.ts +0 -15
  248. package/dist/builder/compile/compile-agenda.js +0 -64
  249. package/dist/builder/merge.d.ts +0 -4
  250. package/dist/builder/merge.js +0 -19
  251. package/dist/compute-host/read-only-cache.d.ts +0 -20
  252. package/dist/compute-host/read-only-cache.js +0 -61
  253. package/dist/core/context/db.d.ts +0 -69
  254. package/dist/core/context/db.js +0 -44
  255. package/dist/core/context/docusign.d.ts +0 -134
  256. package/dist/core/context/docusign.js +0 -108
  257. package/dist/core/context/filevine.d.ts +0 -323
  258. package/dist/core/context/filevine.js +0 -254
  259. package/dist/core/context/http.d.ts +0 -9
  260. package/dist/core/context/http.js +0 -4
  261. package/dist/core/context/hubspot.d.ts +0 -250
  262. package/dist/core/context/hubspot.js +0 -191
  263. package/dist/core/context/twilio.d.ts +0 -50
  264. package/dist/core/context/twilio.js +0 -42
  265. package/dist/core/functions/define/case-context.d.ts +0 -14
  266. package/dist/core/functions/define/case-context.js +0 -15
  267. package/dist/core/functions/define/compute.d.ts +0 -7
  268. package/dist/core/functions/define/compute.js +0 -17
  269. package/dist/core/functions/define/custom.d.ts +0 -4
  270. package/dist/core/functions/define/custom.js +0 -8
  271. package/dist/core/functions/define/system.d.ts +0 -7
  272. package/dist/core/functions/define/system.js +0 -12
  273. package/dist/core/functions/func-ref.d.ts +0 -16
  274. package/dist/core/functions/func-ref.js +0 -20
  275. package/dist/core/slot/deps.d.ts +0 -15
  276. package/dist/core/slot/deps.js +0 -88
  277. package/dist/core/slot/derived.d.ts +0 -32
  278. package/dist/core/slot/derived.js +0 -115
  279. package/dist/core/slot/platform-session-fields.d.ts +0 -38
  280. package/dist/core/slot/platform-session-fields.js +0 -48
  281. package/dist/core/slot/sf-schema.d.ts +0 -23
  282. package/dist/core/slot/sf-schema.js +0 -125
  283. package/dist/core/slot/sf-validate.d.ts +0 -14
  284. package/dist/core/slot/sf-validate.js +0 -132
  285. package/dist/core/slot/snapshot.d.ts +0 -18
  286. package/dist/core/slot/snapshot.js +0 -138
  287. package/dist/core/slot/write.d.ts +0 -48
  288. package/dist/core/slot/write.js +0 -426
  289. package/dist/core/stage/builders.d.ts +0 -54
  290. package/dist/core/stage/builders.js +0 -31
  291. package/dist/core/stage/condition.d.ts +0 -17
  292. package/dist/core/stage/condition.js +0 -198
  293. package/dist/core/stage/drive.d.ts +0 -43
  294. package/dist/core/stage/drive.js +0 -78
  295. package/dist/core/stage/finish-when.d.ts +0 -20
  296. package/dist/core/stage/finish-when.js +0 -82
  297. package/dist/core/stage/flatten.d.ts +0 -27
  298. package/dist/core/stage/flatten.js +0 -466
  299. package/dist/core/stage/index.d.ts +0 -15
  300. package/dist/core/stage/index.js +0 -13
  301. package/dist/core/stage/interrupts.d.ts +0 -28
  302. package/dist/core/stage/interrupts.js +0 -120
  303. package/dist/core/stage/ivr-fields.d.ts +0 -9
  304. package/dist/core/stage/ivr-fields.js +0 -75
  305. package/dist/core/stage/simulate.d.ts +0 -24
  306. package/dist/core/stage/simulate.js +0 -33
  307. package/dist/core/stage/transfer-fields.d.ts +0 -19
  308. package/dist/core/stage/transfer-fields.js +0 -129
  309. package/dist/core/stage/transfer-spec.d.ts +0 -48
  310. package/dist/core/stage/transfer-spec.js +0 -113
  311. package/dist/core/stage/types.d.ts +0 -208
  312. package/dist/core/stage/types.js +0 -2
  313. package/dist/core/stage/validate.d.ts +0 -11
  314. package/dist/core/stage/validate.js +0 -330
  315. package/dist/core/stage/walk.d.ts +0 -39
  316. package/dist/core/stage/walk.js +0 -119
  317. package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
  318. package/dist/fn-context/system-args.d.ts +0 -6
  319. package/dist/fn-context/system-args.js +0 -14
@@ -1,26 +1,29 @@
1
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { cancel, isCancel, select } from '@clack/prompts';
2
4
  import chokidar from 'chokidar';
3
- import { compileAll } from '../../builder/compile/compile-sequence.js';
5
+ import { compileAll, compileCompanyPart, runtimePackageSource, runtimePackageVersion, } from '../../builder/compile/compile-sequence.js';
4
6
  import { withCompanyImportGraph } from '../../builder/company-import-graph.js';
5
- import { findTsModule } from '../../builder/paths.js';
6
- import { listSequenceIds, loadSequenceCaseContextView, loadSequenceFunctions } from '../../builder/load.js';
7
+ import { findTsModule, requireCompanyResolveModule, requireResolveModule } from '../../builder/paths.js';
8
+ import { listSequenceIds, loadCompanyWebhook, loadSequenceCaseOrchestrator, WEBHOOK_MODULE, loadCompanyResolver, loadSequenceResolver, loadSequenceEvents, loadSequenceFunctions, } from '../../builder/load.js';
9
+ import { loadSchedules } from '../../builder/schedules.js';
10
+ import { loadOnError } from '../../builder/errors.js';
11
+ import { ORCHESTRATOR_FUNCTION } from '../../core/case-orchestrator/types.js';
12
+ import { WEBHOOK_RECEIVED_FUNCTION } from '../../core/functions/events.js';
13
+ import { COMPANY_RESOLVE_FUNCTION, RESOLVE_FUNCTION } from '../../core/party/types.js';
14
+ import { COMPANY_SCOPE, } from '../../builder/types.js';
15
+ import { realPath } from '../../builder/company-imports.js';
7
16
  import { createSourceComputeHost } from '../../compute-host/source-compute-host.js';
8
17
  import { formatRuntimeVersion } from '../../fn-context/rpc/request-id.js';
9
18
  import { encodeOverlayMessage, OVERLAY_REPLACED_CLOSE_CODE, parseOverlayMessage, } from '../../dev-overlay/protocol.js';
10
- const DEFAULT_BACKEND_URL = 'http://127.0.0.1:8100';
11
- function env(name) {
12
- const value = process.env[name];
13
- return value && value.length > 0 ? value : undefined;
14
- }
19
+ import { getLogger } from '../../logger.js';
20
+ import { listServeEnvironments, loadCachedServeAuthorization, resolveBackendUrl, ServeAuthorizationRejectedError, } from '../auth/auth.js';
21
+ import { forgetServeAuthorization, loginServe } from '../auth/serve-login.js';
22
+ import { toHttpUrl, toWsUrl } from '../auth/backend-url.js';
23
+ import { progressIndicator } from '../auth/progress.js';
24
+ const logger = getLogger(['runtime', 'overlay']);
15
25
  export function overlayWebSocketUrl(raw) {
16
- const url = new URL(raw);
17
- if (url.protocol === 'http:')
18
- url.protocol = 'ws:';
19
- else if (url.protocol === 'https:')
20
- url.protocol = 'wss:';
21
- else if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
22
- throw new Error(`unsupported backend url: ${raw}`);
23
- }
26
+ const url = toWsUrl(raw);
24
27
  const path = url.pathname.replace(/\/$/, '');
25
28
  if (path === '')
26
29
  url.pathname = '/overlay';
@@ -29,14 +32,7 @@ export function overlayWebSocketUrl(raw) {
29
32
  return url.toString();
30
33
  }
31
34
  function overlayRpcUrl(raw) {
32
- const url = new URL(raw);
33
- if (url.protocol === 'ws:')
34
- url.protocol = 'http:';
35
- else if (url.protocol === 'wss:')
36
- url.protocol = 'https:';
37
- else if (url.protocol !== 'http:' && url.protocol !== 'https:') {
38
- throw new Error(`unsupported backend url: ${raw}`);
39
- }
35
+ const url = toHttpUrl(raw);
40
36
  url.pathname = `${url.pathname.replace(/\/$/, '')}/trpc`;
41
37
  url.search = '';
42
38
  url.hash = '';
@@ -48,9 +44,20 @@ function sequenceIdFromPath(repoRoot, file) {
48
44
  const match = /^sequences[/\\]([^/\\]+)(?:[/\\]|$)/.exec(rel);
49
45
  return match?.[1] ?? null;
50
46
  }
51
- function isSchemaPath(repoRoot, file) {
52
- const rel = path.relative(repoRoot, file);
53
- return rel === 'schema.ts' || rel.startsWith(`schema${path.sep}`) || rel.startsWith('schema/');
47
+ /** Not the repository's code: dependencies, git, and what compiling writes. */
48
+ export function isIgnoredServePath(repoRoot, file) {
49
+ const parts = path.relative(repoRoot, file).split(/[/\\]/);
50
+ return parts.some((part) => part === 'node_modules' || part === '__gen' || part === 'build' || part === 'dist' || part.startsWith('.'));
51
+ }
52
+ /**
53
+ * What a changed file recompiles (22-sequence-as-business-unit §6): a file of one sequence, that
54
+ * sequence; anything else (`schema/`, `resolve.ts`, shared code) can reach every sequence, so all.
55
+ */
56
+ export function reloadScopeForPath(repoRoot, file) {
57
+ if (isIgnoredServePath(repoRoot, file))
58
+ return null;
59
+ const sequenceId = sequenceIdFromPath(repoRoot, file);
60
+ return sequenceId ? { sequenceId } : 'all';
54
61
  }
55
62
  async function loadOne(repoRoot, sequenceId, generation) {
56
63
  return withCompanyImportGraph(repoRoot, async () => {
@@ -63,30 +70,102 @@ async function loadOne(repoRoot, sequenceId, generation) {
63
70
  throw new Error(`Failed to compile sequence ${sequenceId}`);
64
71
  }
65
72
  const sequenceDir = path.join(repoRoot, 'sequences', sequenceId);
73
+ // A sequence without `functions/` has none (76 §5.5), as the compile above takes it.
66
74
  const functionsPath = findTsModule(sequenceDir, 'functions');
67
- if (!functionsPath) {
68
- throw new Error(`No functions module for sequence ${sequenceId}`);
69
- }
70
- const functions = await loadSequenceFunctions(functionsPath);
71
- const viewPath = findTsModule(sequenceDir, 'view');
72
- const view = viewPath ? await loadSequenceCaseContextView(viewPath) : undefined;
73
- return { build: compiled.build, functions, view, generation };
75
+ const caseOrchestratorPath = findTsModule(sequenceDir, 'orchestrator');
76
+ const authored = functionsPath ? await loadSequenceFunctions(functionsPath) : {};
77
+ const functions = {
78
+ ...authored,
79
+ [RESOLVE_FUNCTION]: (await loadSequenceResolver(requireResolveModule(sequenceDir, sequenceId))),
80
+ ...(caseOrchestratorPath ? { [ORCHESTRATOR_FUNCTION]: await loadSequenceCaseOrchestrator(caseOrchestratorPath) } : {}),
81
+ };
82
+ const eventsPath = findTsModule(sequenceDir, 'events');
83
+ const events = eventsPath ? await loadSequenceEvents(eventsPath) : {};
84
+ return { build: compiled.build, sourceMaps: compiled.sourceMaps, functions, events, generation };
74
85
  });
75
86
  }
87
+ export async function loadCompany(repoRoot) {
88
+ return withCompanyImportGraph(repoRoot, async () => {
89
+ const webhookPath = findTsModule(repoRoot, WEBHOOK_MODULE);
90
+ return {
91
+ part: await compileCompanyPart(repoRoot, { includeCode: false }),
92
+ functions: {
93
+ [COMPANY_RESOLVE_FUNCTION]: (await loadCompanyResolver(requireCompanyResolveModule(repoRoot))),
94
+ ...(webhookPath ? { [WEBHOOK_RECEIVED_FUNCTION]: (await loadCompanyWebhook(webhookPath)) } : {}),
95
+ ...(await loadSchedules(repoRoot))?.functions,
96
+ ...(await loadOnError(repoRoot))?.functions,
97
+ },
98
+ };
99
+ });
100
+ }
101
+ /** The repository's sequences, compiled; a listed sequence missing from the repository is left out. */
76
102
  async function loadSequences(repoRoot, sequenceIds, generation) {
77
103
  return withCompanyImportGraph(repoRoot, async () => {
104
+ const present = new Set(listSequenceIds(repoRoot));
78
105
  const out = new Map();
79
106
  for (const sequenceId of sequenceIds) {
80
- out.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
107
+ if (present.has(sequenceId))
108
+ out.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
81
109
  }
82
110
  return out;
83
111
  });
84
112
  }
113
+ /**
114
+ * The changes waiting to be reloaded. A reload that fails keeps its scope (`fail`) for the next one,
115
+ * which a new file change starts (no retry on its own: the failure is in the code until it is
116
+ * edited); it is cleared only once a reload of it loads.
117
+ */
118
+ export class ReloadQueue {
119
+ pending = { all: false, ids: new Set() };
120
+ failed = { all: false, ids: new Set() };
121
+ add(scope) {
122
+ if (scope === 'all')
123
+ this.pending.all = true;
124
+ else
125
+ this.pending.ids.add(scope.sequenceId);
126
+ }
127
+ /** Something changed since the last reload. */
128
+ hasChanges() {
129
+ return this.pending.all || this.pending.ids.size > 0;
130
+ }
131
+ /** The next reload: the new changes plus what failed before. */
132
+ take() {
133
+ const all = this.pending.all || this.failed.all;
134
+ const job = { all, ids: all ? new Set() : new Set([...this.failed.ids, ...this.pending.ids]) };
135
+ this.pending = { all: false, ids: new Set() };
136
+ this.failed = { all: false, ids: new Set() };
137
+ return job;
138
+ }
139
+ fail(job) {
140
+ this.failed = { all: this.failed.all || job.all, ids: new Set([...this.failed.ids, ...job.ids]) };
141
+ }
142
+ }
143
+ /**
144
+ * The state after a reload (`all`: the whole repository; else the sequences `ids` names), built
145
+ * aside: if any part fails to load it throws and the current state stays whole, never new company
146
+ * code with old sequences.
147
+ */
148
+ export async function reloadServeState(repoRoot, current, job, generation) {
149
+ // Recompiling everything also picks up sequence directories added or removed since.
150
+ const ids = job.all ? listSequenceIds(repoRoot) : [...job.ids];
151
+ // The company's code is outside `sequences/`, so only a recompile of everything reaches it.
152
+ const company = job.all ? await loadCompany(repoRoot) : current.company;
153
+ const loaded = await loadSequences(repoRoot, ids, generation);
154
+ const sequences = job.all ? new Map() : new Map(current.sequences);
155
+ for (const id of ids) {
156
+ const next = loaded.get(id);
157
+ if (next)
158
+ sequences.set(id, next);
159
+ else
160
+ sequences.delete(id);
161
+ }
162
+ return { company, sequences };
163
+ }
85
164
  function send(ws, message) {
86
165
  ws.send(encodeOverlayMessage(message));
87
166
  }
88
167
  function logField(label, value) {
89
- console.log(` ${label.padEnd(10)} ${value}`);
168
+ logger.info(label, value);
90
169
  }
91
170
  async function handleExecute(loaded, message, rpcUrl) {
92
171
  const data = message.data
@@ -97,30 +176,78 @@ async function handleExecute(loaded, message, rpcUrl) {
97
176
  : undefined;
98
177
  const host = createSourceComputeHost({
99
178
  functions: loaded.functions,
100
- caseContextView: loaded.view,
179
+ events: loaded.events,
101
180
  data: data,
102
181
  runToken: message.runToken,
103
- runtimeVersion: loaded.build.engineVersion,
104
- runtimeSource: loaded.build.runtimeSource ?? 'npm',
182
+ runtimeVersion: loaded.runtimeVersion,
183
+ runtimeSource: loaded.runtimeSource,
105
184
  getRequestId: () => message.logRequestId,
106
185
  getTags: () => message.logTags,
107
186
  });
108
187
  try {
109
- return await host.execute(message.name, message.args);
188
+ return await host.execute(message.name, message.invocation);
110
189
  }
111
190
  finally {
112
191
  host.dispose();
113
192
  }
114
193
  }
115
- function pushOverlays(ws, sequences) {
116
- for (const [sequenceId, loaded] of sequences) {
117
- send(ws, {
118
- type: 'overlay',
119
- sequenceId,
120
- generation: loaded.generation,
121
- build: loaded.build,
122
- });
194
+ /**
195
+ * Push the local company build: the company's functions and every loaded sequence, as one overlay.
196
+ * With no sequence left it is still pushed, so the environment drops the ones it had (22 §11).
197
+ */
198
+ export function pushOverlay(ws, company, sequences, generation) {
199
+ const build = {
200
+ company: company.part,
201
+ sequences: Object.fromEntries([...sequences].map(([sequenceId, sequence]) => [sequenceId, sequence.build])),
202
+ };
203
+ const sourceMaps = Object.assign({}, ...[...sequences.values()].map((sequence) => sequence.sourceMaps));
204
+ send(ws, { type: 'overlay', generation, build, sourceMaps });
205
+ }
206
+ /** A stack frame as V8 prints it: `at name (file:line:col)` or `at file:line:col`. */
207
+ const STACK_FRAME = /^\s*at (?:(.*?) \()?(.+?):(\d+):(\d+)\)?$/;
208
+ /**
209
+ * What the company is shown of a failed function's stack (the backend's company logs): the error's first
210
+ * line and the frames of the repository's own files, by their path from its root. tsx runs the original
211
+ * files, so their lines are already the company's. Undefined when no frame is the repository's.
212
+ */
213
+ export function repositoryStack(error, repoRoot) {
214
+ if (!(error instanceof Error) || !error.stack)
215
+ return undefined;
216
+ const lines = error.stack.split('\n');
217
+ const firstFrame = lines.findIndex((line) => /^\s*at /.test(line));
218
+ if (firstFrame === -1)
219
+ return undefined;
220
+ const roots = [...new Set([path.resolve(repoRoot), realPath(repoRoot)])];
221
+ const frames = lines.slice(firstFrame).flatMap((line) => {
222
+ const frame = STACK_FRAME.exec(line);
223
+ const file = frame ? repositoryFile(frame[2], roots) : null;
224
+ if (!frame || !file)
225
+ return [];
226
+ return [` at ${frame[1] ? `${frame[1]} (` : ''}${file}:${frame[3]}:${frame[4]}${frame[1] ? ')' : ''}`];
227
+ });
228
+ return frames.length > 0 ? [...lines.slice(0, firstFrame), ...frames].join('\n') : undefined;
229
+ }
230
+ /** A frame's file as a path from the repository root; null outside its own files (node_modules included). */
231
+ function repositoryFile(location, roots) {
232
+ let file = location;
233
+ if (file.startsWith('file://')) {
234
+ try {
235
+ // The import graph stamps the URLs it loads (`?spgen=…`).
236
+ file = fileURLToPath(file.replace(/[?#].*$/, ''));
237
+ }
238
+ catch {
239
+ return null;
240
+ }
123
241
  }
242
+ if (!path.isAbsolute(file))
243
+ return null;
244
+ for (const root of roots) {
245
+ const relative = path.relative(root, file);
246
+ if (relative && !relative.startsWith('..') && !path.isAbsolute(relative) && !relative.split(path.sep).includes('node_modules')) {
247
+ return relative.split(path.sep).join('/');
248
+ }
249
+ }
250
+ return null;
124
251
  }
125
252
  function closeDetail(event) {
126
253
  return event.reason ? `${event.code} ${event.reason}` : String(event.code);
@@ -160,6 +287,36 @@ class OverlayReplacedError extends Error {
160
287
  this.name = 'OverlayReplacedError';
161
288
  }
162
289
  }
290
+ /** Transient: an infra hiccup, or a lease that was just replaced. Retry with backoff. */
291
+ class OverlayUnavailableError extends Error {
292
+ constructor(message) {
293
+ super(message);
294
+ this.name = 'OverlayUnavailableError';
295
+ }
296
+ }
297
+ /** Permanent: the request itself is invalid (missing or unknown environment, prod). Fatal. */
298
+ class OverlayRequestError extends Error {
299
+ constructor(message) {
300
+ super(message);
301
+ this.name = 'OverlayRequestError';
302
+ }
303
+ }
304
+ export function toOverlayError(message) {
305
+ switch (message.code) {
306
+ case 'authorization':
307
+ return new ServeAuthorizationRejectedError(message.error);
308
+ case 'request':
309
+ return new OverlayRequestError(message.error);
310
+ case 'unavailable':
311
+ return new OverlayUnavailableError(message.error);
312
+ default: {
313
+ // The wire format is not type-checked: a code this CLI doesn't know is a protocol bug, so
314
+ // stop and say so instead of reconnecting forever on an `undefined` error.
315
+ const unknown = message.code;
316
+ return new OverlayRequestError(`unknown auth.error code ${JSON.stringify(unknown)}: ${message.error}`);
317
+ }
318
+ }
319
+ }
163
320
  function waitAuthOk(ws) {
164
321
  return new Promise((resolve, reject) => {
165
322
  const onMessage = (event) => {
@@ -177,7 +334,7 @@ function waitAuthOk(ws) {
177
334
  }
178
335
  if (message.type === 'auth.error') {
179
336
  cleanup();
180
- reject(new Error(message.error));
337
+ reject(toOverlayError(message));
181
338
  }
182
339
  }
183
340
  catch (error) {
@@ -199,7 +356,12 @@ function waitAuthOk(ws) {
199
356
  ws.addEventListener('close', onClose);
200
357
  });
201
358
  }
202
- function attachExecuteHandler(ws, sequences, rpcUrl) {
359
+ /**
360
+ * Runs each `execute` the backend sends, unless its deadline has passed: the backend has then reported
361
+ * it as failed (a paused process, a laptop that slept), and running it would repeat its effects when
362
+ * the call is retried. One already started is not interrupted.
363
+ */
364
+ export function attachExecuteHandler(ws, find, rpcUrl, repoRoot) {
203
365
  ws.addEventListener('message', (event) => {
204
366
  void (async () => {
205
367
  let message;
@@ -207,46 +369,92 @@ function attachExecuteHandler(ws, sequences, rpcUrl) {
207
369
  message = parseOverlayMessage(String(event.data));
208
370
  }
209
371
  catch (error) {
210
- console.error('bad server message:', error instanceof Error ? error.message : error);
372
+ logger.error('bad server message', error);
211
373
  return;
212
374
  }
213
375
  if (message.type !== 'execute')
214
376
  return;
215
- const call = `${message.sequenceId} ${message.name}`;
216
- const loaded = sequences.get(message.sequenceId);
377
+ const lateMs = Date.now() - message.deadline;
378
+ if (lateMs > 0) {
379
+ send(ws, {
380
+ type: 'execute.result',
381
+ requestId: message.requestId,
382
+ ok: false,
383
+ error: `expired: the backend stopped waiting ${lateMs} ms ago, so it was not run`,
384
+ notStarted: true,
385
+ });
386
+ logger.warn('execute skipped: it arrived after the backend stopped waiting', {
387
+ sequenceId: message.sequenceId,
388
+ name: message.name,
389
+ lateMs,
390
+ });
391
+ return;
392
+ }
393
+ const loaded = find(message.sequenceId);
217
394
  if (!loaded) {
218
395
  const error = `unknown sequence ${message.sequenceId}`;
219
- console.error(`execute ${call} failed: ${error}`);
220
396
  send(ws, {
221
397
  type: 'execute.result',
222
398
  requestId: message.requestId,
223
399
  ok: false,
224
400
  error,
401
+ notStarted: true,
225
402
  });
403
+ logger.error('execute failed', { sequenceId: message.sequenceId, name: message.name, error });
226
404
  return;
227
405
  }
228
406
  const started = Date.now();
229
- console.log(`execute ${call}`);
407
+ let result;
230
408
  try {
231
- const result = await handleExecute(loaded, message, rpcUrl);
232
- console.log(`execute ${call} ok ${Date.now() - started}ms`);
233
- send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
409
+ result = await handleExecute(loaded, message, rpcUrl);
234
410
  }
235
411
  catch (error) {
236
412
  const text = error instanceof Error ? error.message : String(error);
237
- console.error(`execute ${call} failed: ${text}`);
413
+ const stack = repositoryStack(error, repoRoot);
238
414
  send(ws, {
239
415
  type: 'execute.result',
240
416
  requestId: message.requestId,
241
417
  ok: false,
242
418
  error: text,
419
+ ...(stack ? { stack } : {}),
420
+ ...(error instanceof Error && error.name === 'PlatformUnavailableError' ? { platform: true } : {}),
243
421
  });
422
+ logger.error('execute failed', {
423
+ sequenceId: message.sequenceId,
424
+ name: message.name,
425
+ durationMs: Date.now() - started,
426
+ error: text,
427
+ }, ...(stack ? [`\n${stack}`] : []));
428
+ return;
244
429
  }
430
+ send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
431
+ logger.info('execute ok', {
432
+ sequenceId: message.sequenceId,
433
+ name: message.name,
434
+ durationMs: Date.now() - started,
435
+ });
245
436
  })();
246
437
  });
247
438
  }
439
+ function attachNoticeHandler(ws) {
440
+ ws.addEventListener('message', (event) => {
441
+ let message;
442
+ try {
443
+ message = parseOverlayMessage(String(event.data));
444
+ }
445
+ catch {
446
+ return;
447
+ }
448
+ if (message.type !== 'notice')
449
+ return;
450
+ if (message.details.length > 0)
451
+ logger.warn(message.message, message.details);
452
+ else
453
+ logger.warn(message.message);
454
+ });
455
+ }
248
456
  function waitSessionEnd(ws) {
249
- return new Promise((resolve) => {
457
+ return new Promise((resolve, reject) => {
250
458
  if (ws.readyState === WebSocket.CLOSED) {
251
459
  resolve({ closed: 'already closed' });
252
460
  return;
@@ -255,9 +463,14 @@ function waitSessionEnd(ws) {
255
463
  try {
256
464
  const message = parseOverlayMessage(String(event.data));
257
465
  if (message.type === 'replaced') {
466
+ logger.warn('another serve took the environment');
258
467
  cleanup();
259
468
  resolve('replaced');
260
469
  }
470
+ else if (message.type === 'auth.error') {
471
+ cleanup();
472
+ reject(toOverlayError(message));
473
+ }
261
474
  }
262
475
  catch {
263
476
  return;
@@ -275,35 +488,128 @@ function waitSessionEnd(ws) {
275
488
  ws.addEventListener('close', onClose);
276
489
  });
277
490
  }
491
+ function exitOnCancel() {
492
+ cancel('Cancelled.');
493
+ process.exit(1);
494
+ }
495
+ /**
496
+ * Only prompt when there's an actual person at a terminal to answer — a CI runner, a piped/
497
+ * redirected stdin, or any other non-interactive invocation must keep failing fast on a missing
498
+ * flag instead of hanging on a readline prompt with undefined behavior off a real TTY.
499
+ */
500
+ function requireInteractiveOrThrow(flag) {
501
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
502
+ throw new Error(`${flag} is required (not running in an interactive terminal to prompt for it)`);
503
+ }
504
+ }
505
+ /** The token's non-prod environments. A rejected token surfaces as `ServeAuthorizationRejectedError`. */
506
+ async function fetchEnvironments(backendUrl, authorization) {
507
+ const progress = progressIndicator();
508
+ const company = authorization.companyName ? ` in ${authorization.companyName}` : '';
509
+ progress.start(`Fetching environments${company}…`);
510
+ let environments;
511
+ try {
512
+ environments = await listServeEnvironments(backendUrl, authorization.token);
513
+ }
514
+ catch (error) {
515
+ if (error instanceof ServeAuthorizationRejectedError) {
516
+ progress.error('Login was rejected by the backend.');
517
+ throw error;
518
+ }
519
+ progress.error('Could not fetch environments.');
520
+ throw new Error(`Could not list environments: ${error instanceof Error ? error.message : String(error)}`);
521
+ }
522
+ progress.stop(`Found ${environments.length} environment${environments.length === 1 ? '' : 's'}${company}.`);
523
+ return environments;
524
+ }
525
+ /**
526
+ * Login plus the company's environments. Listing them doubles as checking the cached token against
527
+ * the backend: it can be unexpired locally yet rejected (revoked, re-keyed, access removed), and
528
+ * then it is dropped for a fresh browser login. Rejections after startup stop serve instead.
529
+ */
530
+ async function authorizeWithEnvironments(backendUrl, openBrowser) {
531
+ const cached = await loadCachedServeAuthorization(backendUrl);
532
+ if (cached) {
533
+ logger.info('using cached serve authorization');
534
+ try {
535
+ return { authorization: cached, environments: await fetchEnvironments(backendUrl, cached) };
536
+ }
537
+ catch (error) {
538
+ if (!(error instanceof ServeAuthorizationRejectedError))
539
+ throw error;
540
+ logger.warn('cached serve authorization is no longer valid; authorizing again', { error: error.message });
541
+ await forgetServeAuthorization(backendUrl, cached);
542
+ }
543
+ }
544
+ const authorization = await loginServe(backendUrl, { openBrowser });
545
+ return { authorization, environments: await fetchEnvironments(backendUrl, authorization) };
546
+ }
547
+ /** Prompts for one non-prod environment when `--environment` was not given. Shows id and name. */
548
+ async function promptForEnvironment(environments) {
549
+ if (environments.length === 0) {
550
+ throw new Error('No non-prod environments available for this company.');
551
+ }
552
+ const picked = await select({
553
+ message: 'Which environment should Local Serve overlay?',
554
+ options: environments.map((environment) => ({
555
+ value: environment.id,
556
+ label: environment.name,
557
+ hint: environment.id,
558
+ })),
559
+ });
560
+ if (isCancel(picked))
561
+ exitOnCancel();
562
+ return picked;
563
+ }
278
564
  export async function runServe(repoRoot, options = {}) {
279
- const companyId = options.companyId ?? env('SUPERPANEL_SDK_COMPANY_ID');
280
- const apiKey = options.apiKey ?? env('SUPERPANEL_SDK_API_KEY');
281
- if (!companyId)
282
- throw new Error('SUPERPANEL_SDK_COMPANY_ID or --company is required');
283
- if (!apiKey)
284
- throw new Error('SUPERPANEL_SDK_API_KEY or --api-key is required');
285
- const backendUrl = options.url ?? env('SUPERPANEL_SDK_BACKEND_URL') ?? DEFAULT_BACKEND_URL;
286
- const url = overlayWebSocketUrl(backendUrl);
287
- const rpcUrl = overlayRpcUrl(backendUrl);
288
- const sequenceIds = options.sequenceIds ?? listSequenceIds(repoRoot);
289
- if (sequenceIds.length === 0) {
290
- throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')}`);
565
+ const backendUrl = resolveBackendUrl(repoRoot, options.url);
566
+ // A non-interactive caller cannot answer the prompt. Check this before authorization so a CI job
567
+ // with a missing flag fails instead of waiting on a browser-based device-auth flow.
568
+ let environment = options.environment?.trim();
569
+ if (!environment)
570
+ requireInteractiveOrThrow('--environment <id>');
571
+ if (listSequenceIds(repoRoot).length === 0) {
572
+ throw new Error(`No sequences found under ${path.join(repoRoot, 'sequences')}`);
291
573
  }
574
+ // Login is the first interactive step: the environment prompt needs the company-scoped token.
575
+ const { authorization, environments } = await authorizeWithEnvironments(backendUrl, options.openBrowser !== false);
576
+ if (!environment)
577
+ environment = await promptForEnvironment(environments);
578
+ // The whole repository serves the environment (22 §6): every sequence, and only these.
292
579
  let generation = 1;
293
- const sequences = await loadSequences(repoRoot, sequenceIds, generation);
580
+ let company = await loadCompany(repoRoot);
581
+ let sequences = await loadSequences(repoRoot, listSequenceIds(repoRoot), generation);
582
+ const runtimeSource = runtimePackageSource(repoRoot);
583
+ const find = (scope) => {
584
+ if (scope === COMPANY_SCOPE) {
585
+ return { functions: company.functions, events: {}, runtimeVersion: runtimePackageVersion(), runtimeSource };
586
+ }
587
+ const loaded = sequences.get(scope);
588
+ return loaded
589
+ ? {
590
+ functions: loaded.functions,
591
+ events: loaded.events,
592
+ runtimeVersion: loaded.build.engineVersion,
593
+ runtimeSource: loaded.build.runtimeSource,
594
+ }
595
+ : undefined;
596
+ };
597
+ const url = overlayWebSocketUrl(backendUrl);
598
+ const rpcUrl = overlayRpcUrl(backendUrl);
294
599
  let socket = null;
295
- const names = [...sequences.keys()];
296
600
  const firstBuild = sequences.values().next().value?.build;
297
- console.log('Local overlay');
298
- logField('company', companyId);
299
- logField('sequences', names.join(', '));
601
+ logger.info('Local overlay');
602
+ logField('user', authorization.user.email);
603
+ logField('company', authorization.companyId);
604
+ logField('environment', environment);
605
+ logField('sequences', [...sequences.keys()].join(', '));
300
606
  if (firstBuild) {
301
- logField('runtime', formatRuntimeVersion(firstBuild.engineVersion, firstBuild.runtimeSource ?? 'npm'));
607
+ logField('runtime', formatRuntimeVersion(firstBuild.engineVersion, firstBuild.runtimeSource));
302
608
  }
303
609
  logField('backend', backendUrl);
304
610
  logField('websocket', url);
305
611
  let timer;
306
- let pending = { all: false, ids: new Set() };
612
+ const queue = new ReloadQueue();
307
613
  let flushing = false;
308
614
  const flush = () => {
309
615
  if (flushing)
@@ -311,54 +617,51 @@ export async function runServe(repoRoot, options = {}) {
311
617
  flushing = true;
312
618
  void (async () => {
313
619
  try {
314
- while (pending.all || pending.ids.size > 0) {
315
- const job = pending;
316
- pending = { all: false, ids: new Set() };
620
+ while (queue.hasChanges()) {
621
+ const job = queue.take();
317
622
  generation += 1;
318
- const ids = job.all ? sequenceIds : [...job.ids];
319
- const next = await loadSequences(repoRoot, ids, generation);
320
- if (job.all)
321
- sequences.clear();
322
- for (const [id, loaded] of next)
323
- sequences.set(id, loaded);
623
+ // Swapped in only whole: a failed reload leaves the code that runs (and is pushed) as it was.
624
+ let next;
625
+ try {
626
+ next = await reloadServeState(repoRoot, { company, sequences }, job, generation);
627
+ }
628
+ catch (error) {
629
+ // Kept for the next change: fixing one sequence then also reloads what this one covered.
630
+ queue.fail(job);
631
+ logger.error('reload failed', error);
632
+ continue;
633
+ }
634
+ company = next.company;
635
+ sequences = next.sequences;
324
636
  if (socket && socket.readyState === WebSocket.OPEN) {
325
- pushOverlays(socket, sequences);
326
- console.log(`reloaded sequences ${ids.join(', ')}`);
637
+ pushOverlay(socket, company, sequences, generation);
638
+ logger.info('reloaded sequences', job.all ? 'all' : [...job.ids]);
327
639
  }
328
640
  }
329
641
  }
330
642
  catch (error) {
331
- console.error('reload failed:', error instanceof Error ? error.message : error);
643
+ logger.error('reload failed', error);
332
644
  }
333
645
  finally {
334
646
  flushing = false;
335
- if (pending.all || pending.ids.size > 0)
647
+ if (queue.hasChanges())
336
648
  flush();
337
649
  }
338
650
  })();
339
651
  };
340
652
  chokidar
341
- .watch([path.join(repoRoot, 'schema'), path.join(repoRoot, 'schema.ts'), path.join(repoRoot, 'sequences')], {
653
+ .watch(repoRoot, {
342
654
  ignoreInitial: true,
343
- ignored: (p) => p.includes(`${path.sep}__gen${path.sep}`) || p.includes(`${path.sep}build${path.sep}`),
655
+ ignored: (p) => isIgnoredServePath(repoRoot, p),
344
656
  })
345
657
  .on('all', (event, file) => {
346
658
  if (!file)
347
659
  return;
348
- if (file.includes(`${path.sep}__gen${path.sep}`))
349
- return;
350
- if (file.includes(`${path.sep}build${path.sep}`))
351
- return;
352
- const rel = path.relative(repoRoot, file);
353
- const schemaHit = isSchemaPath(repoRoot, file);
354
- const sequenceId = sequenceIdFromPath(repoRoot, file);
355
- if (!schemaHit && !sequenceId)
660
+ const scope = reloadScopeForPath(repoRoot, file);
661
+ if (!scope)
356
662
  return;
357
- console.log(`changed ${rel}`);
358
- if (schemaHit)
359
- pending.all = true;
360
- else if (sequenceId)
361
- pending.ids.add(sequenceId);
663
+ logger.info('changed', path.relative(repoRoot, file));
664
+ queue.add(scope);
362
665
  clearTimeout(timer);
363
666
  timer = setTimeout(flush, 100);
364
667
  });
@@ -369,26 +672,35 @@ export async function runServe(repoRoot, options = {}) {
369
672
  try {
370
673
  await waitOpen(ws);
371
674
  const authOk = waitAuthOk(ws);
372
- send(ws, {
373
- type: 'auth',
374
- apiKey,
375
- companyId,
376
- names: [...sequences.keys()],
377
- });
675
+ send(ws, { type: 'auth', token: authorization.token, environment });
378
676
  await authOk;
379
- attachExecuteHandler(ws, sequences, rpcUrl);
380
- pushOverlays(ws, sequences);
677
+ attachExecuteHandler(ws, find, rpcUrl, repoRoot);
678
+ attachNoticeHandler(ws);
679
+ pushOverlay(ws, company, sequences, generation);
381
680
  delayMs = 500;
382
- console.log(`connected overlay ${url}`);
681
+ logger.info('connected', { url });
383
682
  const end = await waitSessionEnd(ws);
384
683
  if (end === 'replaced')
385
684
  throw new OverlayReplacedError();
386
- console.log(`disconnected retrying ${end.closed}`);
685
+ logger.warn('disconnected; retrying', { detail: end.closed });
387
686
  }
388
687
  catch (error) {
389
688
  if (error instanceof OverlayReplacedError)
390
689
  throw error;
391
- console.error('connection failed:', error instanceof Error ? error.message : error);
690
+ if (error instanceof OverlayRequestError)
691
+ throw error;
692
+ if (error instanceof ServeAuthorizationRejectedError) {
693
+ // Mid-session the identity is fixed: stop rather than log in again, and drop the cached
694
+ // token so the user's next `serve` starts with a fresh login.
695
+ await forgetServeAuthorization(backendUrl, authorization);
696
+ throw new Error(`Serve authorization was rejected (${error.message}). Run serve again to log in.`);
697
+ }
698
+ if (error instanceof OverlayUnavailableError) {
699
+ logger.warn('serve is temporarily unavailable; retrying', { error: error.message });
700
+ }
701
+ else {
702
+ logger.error('connection failed', error);
703
+ }
392
704
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
393
705
  ws.close();
394
706
  }
@@ -398,4 +710,80 @@ export async function runServe(repoRoot, options = {}) {
398
710
  delayMs = Math.min(delayMs * 2, 5_000);
399
711
  }
400
712
  }
713
+ /**
714
+ * The repository served on `environmentId` as `serve` serves it, once (no file watch, no reconnect): its
715
+ * code loaded through the company import graph (an import the compile refuses fails here), pushed, and
716
+ * every function the environment runs meanwhile run here. Resolves once the environment took it.
717
+ */
718
+ export async function openOverlay(repoRoot, options) {
719
+ if (listSequenceIds(repoRoot).length === 0)
720
+ throw new Error(`No sequences found under ${path.join(repoRoot, 'sequences')}`);
721
+ const company = await loadCompany(repoRoot);
722
+ const sequences = await loadSequences(repoRoot, listSequenceIds(repoRoot), 1);
723
+ const runtimeSource = runtimePackageSource(repoRoot);
724
+ const find = (scope) => {
725
+ if (scope === COMPANY_SCOPE)
726
+ return { functions: company.functions, events: {}, runtimeVersion: runtimePackageVersion(), runtimeSource };
727
+ const loaded = sequences.get(scope);
728
+ return loaded
729
+ ? { functions: loaded.functions, events: loaded.events, runtimeVersion: loaded.build.engineVersion, runtimeSource: loaded.build.runtimeSource }
730
+ : undefined;
731
+ };
732
+ const ws = new WebSocket(overlayWebSocketUrl(options.backendUrl));
733
+ try {
734
+ await waitOpen(ws);
735
+ const authOk = waitAuthOk(ws);
736
+ send(ws, { type: 'auth', token: options.token, environment: options.environmentId });
737
+ await authOk;
738
+ }
739
+ catch (error) {
740
+ ws.close();
741
+ throw error;
742
+ }
743
+ attachExecuteHandler(ws, find, overlayRpcUrl(options.backendUrl), repoRoot);
744
+ attachNoticeHandler(ws);
745
+ // Served once the backend says so (`overlay.ok`): a call made before could still run the release.
746
+ const served = new Promise((resolve, reject) => {
747
+ const timer = setTimeout(() => reject(new Error('the environment did not take the code within 60 s')), 60_000);
748
+ ws.addEventListener('message', (event) => {
749
+ try {
750
+ const message = parseOverlayMessage(String(event.data));
751
+ if (message.type === 'overlay.ok' && message.generation === 1) {
752
+ clearTimeout(timer);
753
+ resolve();
754
+ }
755
+ else if (message.type === 'auth.error') {
756
+ clearTimeout(timer);
757
+ reject(new Error(`serve: ${message.error}`));
758
+ }
759
+ }
760
+ catch {
761
+ // not a message of ours
762
+ }
763
+ });
764
+ ws.addEventListener('close', () => {
765
+ clearTimeout(timer);
766
+ reject(new Error('the environment closed the connection before it took the code'));
767
+ });
768
+ });
769
+ pushOverlay(ws, company, sequences, 1);
770
+ try {
771
+ await served;
772
+ }
773
+ catch (error) {
774
+ ws.close();
775
+ throw error;
776
+ }
777
+ return {
778
+ sequences: [...sequences.keys()],
779
+ close: async () => {
780
+ if (ws.readyState === WebSocket.CLOSED)
781
+ return;
782
+ await new Promise((resolve) => {
783
+ ws.addEventListener('close', () => resolve(), { once: true });
784
+ ws.close();
785
+ });
786
+ },
787
+ };
788
+ }
401
789
  //# sourceMappingURL=index.js.map