@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.
- package/README.md +30 -10
- package/dist/builder/codegen/emit.d.ts +25 -12
- package/dist/builder/codegen/emit.js +339 -78
- package/dist/builder/codegen/generate.d.ts +21 -2
- package/dist/builder/codegen/generate.js +147 -34
- package/dist/builder/company-import-graph.d.ts +2 -0
- package/dist/builder/company-import-graph.js +40 -8
- package/dist/builder/company-imports.d.ts +29 -0
- package/dist/builder/company-imports.js +122 -0
- package/dist/builder/compile/compile-sequence.d.ts +61 -8
- package/dist/builder/compile/compile-sequence.js +334 -80
- package/dist/builder/compile/function-code.d.ts +19 -3
- package/dist/builder/compile/function-code.js +123 -45
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +30 -27
- package/dist/builder/errors.d.ts +9 -0
- package/dist/builder/errors.js +17 -0
- package/dist/builder/function-params.d.ts +10 -0
- package/dist/builder/function-params.js +203 -0
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +258 -67
- package/dist/builder/paths.d.ts +9 -1
- package/dist/builder/paths.js +32 -2
- package/dist/builder/schedules.d.ts +25 -0
- package/dist/builder/schedules.js +101 -0
- package/dist/builder/slot-schema-validation.d.ts +4 -0
- package/dist/builder/slot-schema-validation.js +10 -0
- package/dist/builder/typecheck.d.ts +8 -0
- package/dist/builder/typecheck.js +102 -0
- package/dist/builder/types.d.ts +127 -19
- package/dist/builder/types.js +21 -1
- package/dist/cli/auth/auth.d.ts +37 -0
- package/dist/cli/auth/auth.js +211 -0
- package/dist/cli/auth/backend-url.d.ts +5 -0
- package/dist/cli/auth/backend-url.js +25 -0
- package/dist/cli/auth/clipboard.d.ts +9 -0
- package/dist/cli/auth/clipboard.js +38 -0
- package/dist/cli/auth/http.d.ts +20 -0
- package/dist/cli/auth/http.js +27 -0
- package/dist/cli/auth/index.d.ts +10 -0
- package/dist/cli/auth/index.js +27 -0
- package/dist/cli/auth/progress.d.ts +12 -0
- package/dist/cli/auth/progress.js +15 -0
- package/dist/cli/auth/serve-login.d.ts +8 -0
- package/dist/cli/auth/serve-login.js +24 -0
- package/dist/cli/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -0
- package/dist/cli/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/debug/index.d.ts +31 -0
- package/dist/cli/debug/index.js +119 -0
- package/dist/cli/exec/index.d.ts +39 -0
- package/dist/cli/exec/index.js +138 -0
- package/dist/cli/exec/session.d.ts +48 -0
- package/dist/cli/exec/session.js +82 -0
- package/dist/cli/exec/trace.d.ts +18 -0
- package/dist/cli/exec/trace.js +52 -0
- package/dist/cli/index.js +112 -9
- package/dist/cli/logs/index.d.ts +47 -0
- package/dist/cli/logs/index.js +113 -0
- package/dist/cli/mobile/index.d.ts +12 -0
- package/dist/cli/mobile/index.js +405 -0
- package/dist/cli/serve/index.d.ts +109 -3
- package/dist/cli/serve/index.js +506 -118
- package/dist/cli/turns/index.d.ts +105 -0
- package/dist/cli/turns/index.js +128 -0
- package/dist/cli/turns/run.d.ts +13 -0
- package/dist/cli/turns/run.js +57 -0
- package/dist/cli/turns/shadow.d.ts +112 -0
- package/dist/cli/turns/shadow.js +54 -0
- package/dist/cli/type-gen/index.js +9 -9
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +14 -23
- package/dist/compute-host/compute-host.js +18 -30
- package/dist/compute-host/index.d.ts +2 -4
- package/dist/compute-host/index.js +1 -2
- package/dist/compute-host/source-compute-host.d.ts +10 -10
- package/dist/compute-host/source-compute-host.js +33 -40
- package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
- package/dist/core/case-orchestrator/contact-hours.js +127 -0
- package/dist/core/case-orchestrator/index.d.ts +3 -0
- package/dist/core/case-orchestrator/index.js +3 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +244 -0
- package/dist/core/case-orchestrator/time.d.ts +11 -0
- package/dist/core/case-orchestrator/time.js +29 -0
- package/dist/core/case-orchestrator/types.d.ts +708 -0
- package/dist/core/case-orchestrator/types.js +42 -0
- package/dist/core/context/call.d.ts +291 -0
- package/dist/core/context/call.js +94 -0
- package/dist/core/context/channels.d.ts +60 -0
- package/dist/core/context/channels.js +35 -0
- package/dist/core/context/contact.d.ts +96 -6
- package/dist/core/context/contact.js +48 -2
- package/dist/core/context/email-message.d.ts +198 -0
- package/dist/core/context/email-message.js +86 -0
- package/dist/core/context/email.d.ts +109 -0
- package/dist/core/context/email.js +90 -0
- package/dist/core/context/exchanges.d.ts +167 -0
- package/dist/core/context/exchanges.js +81 -0
- package/dist/core/context/instances.d.ts +307 -0
- package/dist/core/context/instances.js +137 -0
- package/dist/core/context/kv.d.ts +113 -0
- package/dist/core/context/kv.js +60 -0
- package/dist/core/context/platform-registry.d.ts +544 -4
- package/dist/core/context/platform-registry.js +14 -2
- package/dist/core/context/platform.d.ts +2 -1
- package/dist/core/context/sms.d.ts +177 -0
- package/dist/core/context/sms.js +77 -0
- package/dist/core/context/types.d.ts +79 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +85 -0
- package/dist/core/conversation/index.js +133 -0
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +192 -0
- package/dist/core/functions/events.js +23 -0
- package/dist/core/functions/schedules.d.ts +50 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +29 -73
- package/dist/core/functions/types.js +1 -4
- package/dist/core/office-hours/index.d.ts +39 -0
- package/dist/core/office-hours/index.js +187 -0
- package/dist/core/page/define.d.ts +904 -0
- package/dist/core/page/define.js +518 -0
- package/dist/core/page/envelope.d.ts +55 -0
- package/dist/core/page/envelope.js +4 -0
- package/dist/core/page/fns.d.ts +448 -0
- package/dist/core/page/fns.js +539 -0
- package/dist/core/page/memory-reader.d.ts +27 -0
- package/dist/core/page/memory-reader.js +299 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/page-host.d.ts +20 -0
- package/dist/core/page/page-host.js +57 -0
- package/dist/core/page/records-query.d.ts +234 -0
- package/dist/core/page/records-query.js +780 -0
- package/dist/core/page/render.d.ts +295 -0
- package/dist/core/page/render.js +1060 -0
- package/dist/core/page/sandbox.d.ts +85 -0
- package/dist/core/page/sandbox.js +357 -0
- package/dist/core/page/time-format.d.ts +46 -0
- package/dist/core/page/time-format.js +125 -0
- package/dist/core/page/timeline.d.ts +40 -0
- package/dist/core/page/timeline.js +2 -0
- package/dist/core/page/typed.d.ts +648 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +57 -0
- package/dist/core/page/workspace.js +266 -0
- package/dist/core/party/index.d.ts +3 -0
- package/dist/core/party/index.js +3 -0
- package/dist/core/party/schema.d.ts +208 -0
- package/dist/core/party/schema.js +113 -0
- package/dist/core/party/types.d.ts +35 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +56 -0
- package/dist/core/sequence/define.js +10 -0
- package/dist/core/sequence/index.d.ts +63 -0
- package/dist/core/sequence/index.js +231 -0
- package/dist/core/slot/binding.d.ts +40 -0
- package/dist/core/slot/binding.js +2 -0
- package/dist/core/slot/company-metadata.d.ts +102 -0
- package/dist/core/slot/company-metadata.js +536 -0
- package/dist/core/slot/compile-schema.d.ts +19 -13
- package/dist/core/slot/compile-schema.js +51 -248
- package/dist/core/slot/contact-timezone.d.ts +110 -0
- package/dist/core/slot/contact-timezone.js +186 -0
- package/dist/core/slot/field.d.ts +2 -25
- package/dist/core/slot/field.js +0 -71
- package/dist/core/slot/id-ref.d.ts +17 -2
- package/dist/core/slot/id-ref.js +16 -8
- package/dist/core/slot/index.d.ts +12 -13
- package/dist/core/slot/index.js +11 -12
- package/dist/core/slot/naming.d.ts +15 -0
- package/dist/core/slot/naming.js +96 -0
- package/dist/core/slot/platform-fields.d.ts +119 -0
- package/dist/core/slot/platform-fields.js +121 -0
- package/dist/core/slot/ref.d.ts +16 -0
- package/dist/core/slot/ref.js +14 -0
- package/dist/core/slot/row.d.ts +2 -9
- package/dist/core/slot/row.js +13 -23
- package/dist/core/slot/schema-compat.d.ts +17 -0
- package/dist/core/slot/schema-compat.js +103 -0
- package/dist/core/slot/schema.d.ts +353 -76
- package/dist/core/slot/schema.js +163 -10
- package/dist/core/slot/types.d.ts +30 -103
- package/dist/core/slot/value.d.ts +28 -3
- package/dist/core/slot/value.js +93 -57
- package/dist/dev-overlay/protocol.d.ts +74 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +26 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +13 -4
- package/dist/fn-context/fake/index.js +29 -2
- package/dist/fn-context/fn-params.d.ts +3 -0
- package/dist/fn-context/fn-params.js +10 -0
- package/dist/fn-context/index.d.ts +5 -5
- package/dist/fn-context/index.js +4 -4
- package/dist/fn-context/log.js +2 -2
- package/dist/fn-context/make-ctx.d.ts +11 -0
- package/dist/fn-context/make-ctx.js +20 -9
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
- package/dist/fn-context/rpc/errors.d.ts +13 -0
- package/dist/fn-context/rpc/errors.js +22 -0
- package/dist/fn-context/rpc/index.d.ts +4 -2
- package/dist/fn-context/rpc/index.js +4 -2
- package/dist/fn-context/rpc/integration/index.d.ts +15 -5
- package/dist/fn-context/rpc/integration/index.js +1 -1
- package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
- package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
- package/dist/fn-context/rpc/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +13 -3
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +85 -0
- package/dist/fn-context/rpc/records.js +24 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
- package/dist/fn-context/rpc/trpc-client.js +222 -12
- package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
- package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
- package/dist/fn-context/run-scope.d.ts +4 -0
- package/dist/fn-context/run-scope.js +9 -0
- package/dist/fn-context/types.d.ts +52 -5
- package/dist/index.d.ts +36 -46
- package/dist/index.js +30 -33
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +49 -0
- package/dist/platform.d.ts +75 -0
- package/dist/platform.js +57 -0
- package/dist/testing-compile-child.d.ts +2 -0
- package/dist/testing-compile-child.js +20 -0
- package/dist/testing.d.ts +368 -0
- package/dist/testing.js +439 -0
- package/dist/voice-service/actions.d.ts +50 -6
- package/dist/voice-service/index.d.ts +2 -0
- package/dist/voice-service/index.js +2 -0
- package/dist/voice-service/protocol.d.ts +604 -0
- package/dist/voice-service/protocol.js +67 -0
- package/dist/voice-service/types.d.ts +55 -8
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +11 -0
- package/package.json +38 -15
- package/dist/builder/compile/compile-agenda.d.ts +0 -15
- package/dist/builder/compile/compile-agenda.js +0 -64
- package/dist/builder/merge.d.ts +0 -4
- package/dist/builder/merge.js +0 -19
- package/dist/compute-host/read-only-cache.d.ts +0 -20
- package/dist/compute-host/read-only-cache.js +0 -61
- package/dist/core/context/db.d.ts +0 -69
- package/dist/core/context/db.js +0 -44
- package/dist/core/context/docusign.d.ts +0 -134
- package/dist/core/context/docusign.js +0 -108
- package/dist/core/context/filevine.d.ts +0 -323
- package/dist/core/context/filevine.js +0 -254
- package/dist/core/context/http.d.ts +0 -9
- package/dist/core/context/http.js +0 -4
- package/dist/core/context/hubspot.d.ts +0 -250
- package/dist/core/context/hubspot.js +0 -191
- package/dist/core/context/twilio.d.ts +0 -50
- package/dist/core/context/twilio.js +0 -42
- package/dist/core/functions/define/case-context.d.ts +0 -14
- package/dist/core/functions/define/case-context.js +0 -15
- package/dist/core/functions/define/compute.d.ts +0 -7
- package/dist/core/functions/define/compute.js +0 -17
- package/dist/core/functions/define/custom.d.ts +0 -4
- package/dist/core/functions/define/custom.js +0 -8
- package/dist/core/functions/define/system.d.ts +0 -7
- package/dist/core/functions/define/system.js +0 -12
- package/dist/core/functions/func-ref.d.ts +0 -16
- package/dist/core/functions/func-ref.js +0 -20
- package/dist/core/slot/deps.d.ts +0 -15
- package/dist/core/slot/deps.js +0 -88
- package/dist/core/slot/derived.d.ts +0 -32
- package/dist/core/slot/derived.js +0 -115
- package/dist/core/slot/platform-session-fields.d.ts +0 -38
- package/dist/core/slot/platform-session-fields.js +0 -48
- package/dist/core/slot/sf-schema.d.ts +0 -23
- package/dist/core/slot/sf-schema.js +0 -125
- package/dist/core/slot/sf-validate.d.ts +0 -14
- package/dist/core/slot/sf-validate.js +0 -132
- package/dist/core/slot/snapshot.d.ts +0 -18
- package/dist/core/slot/snapshot.js +0 -138
- package/dist/core/slot/write.d.ts +0 -48
- package/dist/core/slot/write.js +0 -426
- package/dist/core/stage/builders.d.ts +0 -54
- package/dist/core/stage/builders.js +0 -31
- package/dist/core/stage/condition.d.ts +0 -17
- package/dist/core/stage/condition.js +0 -198
- package/dist/core/stage/drive.d.ts +0 -43
- package/dist/core/stage/drive.js +0 -78
- package/dist/core/stage/finish-when.d.ts +0 -20
- package/dist/core/stage/finish-when.js +0 -82
- package/dist/core/stage/flatten.d.ts +0 -27
- package/dist/core/stage/flatten.js +0 -466
- package/dist/core/stage/index.d.ts +0 -15
- package/dist/core/stage/index.js +0 -13
- package/dist/core/stage/interrupts.d.ts +0 -28
- package/dist/core/stage/interrupts.js +0 -120
- package/dist/core/stage/ivr-fields.d.ts +0 -9
- package/dist/core/stage/ivr-fields.js +0 -75
- package/dist/core/stage/simulate.d.ts +0 -24
- package/dist/core/stage/simulate.js +0 -33
- package/dist/core/stage/transfer-fields.d.ts +0 -19
- package/dist/core/stage/transfer-fields.js +0 -129
- package/dist/core/stage/transfer-spec.d.ts +0 -48
- package/dist/core/stage/transfer-spec.js +0 -113
- package/dist/core/stage/types.d.ts +0 -208
- package/dist/core/stage/types.js +0 -2
- package/dist/core/stage/validate.d.ts +0 -11
- package/dist/core/stage/validate.js +0 -330
- package/dist/core/stage/walk.d.ts +0 -39
- package/dist/core/stage/walk.js +0 -119
- package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
- package/dist/fn-context/system-args.d.ts +0 -6
- package/dist/fn-context/system-args.js +0 -14
package/dist/cli/serve/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
events: loaded.events,
|
|
101
180
|
data: data,
|
|
102
181
|
runToken: message.runToken,
|
|
103
|
-
runtimeVersion: loaded.
|
|
104
|
-
runtimeSource: loaded.
|
|
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.
|
|
188
|
+
return await host.execute(message.name, message.invocation);
|
|
110
189
|
}
|
|
111
190
|
finally {
|
|
112
191
|
host.dispose();
|
|
113
192
|
}
|
|
114
193
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
372
|
+
logger.error('bad server message', error);
|
|
211
373
|
return;
|
|
212
374
|
}
|
|
213
375
|
if (message.type !== 'execute')
|
|
214
376
|
return;
|
|
215
|
-
const
|
|
216
|
-
|
|
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
|
-
|
|
407
|
+
let result;
|
|
230
408
|
try {
|
|
231
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (!
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
logField('
|
|
299
|
-
logField('
|
|
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
|
|
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
|
-
|
|
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 (
|
|
315
|
-
const job =
|
|
316
|
-
pending = { all: false, ids: new Set() };
|
|
620
|
+
while (queue.hasChanges()) {
|
|
621
|
+
const job = queue.take();
|
|
317
622
|
generation += 1;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
sequences
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
643
|
+
logger.error('reload failed', error);
|
|
332
644
|
}
|
|
333
645
|
finally {
|
|
334
646
|
flushing = false;
|
|
335
|
-
if (
|
|
647
|
+
if (queue.hasChanges())
|
|
336
648
|
flush();
|
|
337
649
|
}
|
|
338
650
|
})();
|
|
339
651
|
};
|
|
340
652
|
chokidar
|
|
341
|
-
.watch(
|
|
653
|
+
.watch(repoRoot, {
|
|
342
654
|
ignoreInitial: true,
|
|
343
|
-
ignored: (p) =>
|
|
655
|
+
ignored: (p) => isIgnoredServePath(repoRoot, p),
|
|
344
656
|
})
|
|
345
657
|
.on('all', (event, file) => {
|
|
346
658
|
if (!file)
|
|
347
659
|
return;
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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,
|
|
380
|
-
|
|
677
|
+
attachExecuteHandler(ws, find, rpcUrl, repoRoot);
|
|
678
|
+
attachNoticeHandler(ws);
|
|
679
|
+
pushOverlay(ws, company, sequences, generation);
|
|
381
680
|
delayMs = 500;
|
|
382
|
-
|
|
681
|
+
logger.info('connected', { url });
|
|
383
682
|
const end = await waitSessionEnd(ws);
|
|
384
683
|
if (end === 'replaced')
|
|
385
684
|
throw new OverlayReplacedError();
|
|
386
|
-
|
|
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
|
-
|
|
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
|