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