@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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `exec <file>`: the repository's integration test. A code snippet runs on this machine, and every
|
|
3
|
+
* `ctx.api` call it makes reaches one environment's real data, through the same RPC and checks as a
|
|
4
|
+
* function the platform runs (the backend's `exec/exec-run.ts` scopes its run token: this company, one
|
|
5
|
+
* non-prod environment, optionally one case). What it writes is written.
|
|
6
|
+
*
|
|
7
|
+
* The snippet is a `.ts` file whose default export is `(ctx, args) => value`. To try a sequence's
|
|
8
|
+
* function on real data, the snippet imports it and calls it with `ctx`. While it runs, this repository
|
|
9
|
+
* serves the environment (as `serve` does): local code and schema, the environment's data.
|
|
10
|
+
*
|
|
11
|
+
* Everything a run did is kept under `.superpanel/exec/<time>-<name>/`, for a person or an agent to read
|
|
12
|
+
* after: `result.json` (the value returned or the error, with the environment and the case), `calls.jsonl`
|
|
13
|
+
* (each `ctx.api` call: its arguments, its answer or error, how long it took) and `log.jsonl` (the
|
|
14
|
+
* snippet's `console` and `ctx.api.log` lines).
|
|
15
|
+
*/
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { openOverlay } from '../serve/index.js';
|
|
20
|
+
import { openExecSession } from './session.js';
|
|
21
|
+
import { traceApi } from './trace.js';
|
|
22
|
+
export { traceApi };
|
|
23
|
+
const LOG_METHODS = new Set(['debug', 'info', 'warn', 'error']);
|
|
24
|
+
/** `YYYYMMDD-HHMMSS` of `date`, local time: the run's directory sorts by when it ran. */
|
|
25
|
+
function stamp(date) {
|
|
26
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
27
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
28
|
+
}
|
|
29
|
+
/** A value as JSON for the files: what JSON cannot hold (a function, a cycle, a BigInt) as text. */
|
|
30
|
+
function jsonLine(value) {
|
|
31
|
+
const seen = new WeakSet();
|
|
32
|
+
return JSON.stringify(value, (_key, v) => {
|
|
33
|
+
if (typeof v === 'bigint')
|
|
34
|
+
return `${v}n`;
|
|
35
|
+
if (typeof v === 'function')
|
|
36
|
+
return `[function ${v.name || 'anonymous'}]`;
|
|
37
|
+
if (v && typeof v === 'object') {
|
|
38
|
+
if (seen.has(v))
|
|
39
|
+
return '[cycle]';
|
|
40
|
+
seen.add(v);
|
|
41
|
+
}
|
|
42
|
+
return v;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** A backend error that has a known fix, with the fix said. */
|
|
46
|
+
function explain(message) {
|
|
47
|
+
if (/No overlay or published company build/.test(message)) {
|
|
48
|
+
return `${message}: this repository's code did not reach the environment; run again`;
|
|
49
|
+
}
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
export async function runExec(repoRoot, file, options = {}) {
|
|
53
|
+
const source = path.resolve(file);
|
|
54
|
+
if (!existsSync(source))
|
|
55
|
+
throw new Error(`${file}: no such file`);
|
|
56
|
+
let args = undefined;
|
|
57
|
+
if (options.args !== undefined) {
|
|
58
|
+
try {
|
|
59
|
+
args = JSON.parse(options.args);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error('--args is JSON, e.g. --args \'{"contact":"…"}\'');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const startedAt = new Date();
|
|
66
|
+
const name = path.basename(source).replace(/\.[cm]?tsx?$/, '');
|
|
67
|
+
const dir = path.join(repoRoot, '.superpanel', 'exec', `${stamp(startedAt)}-${name}`);
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const callsFile = path.join(dir, 'calls.jsonl');
|
|
70
|
+
const logFile = path.join(dir, 'log.jsonl');
|
|
71
|
+
writeFileSync(callsFile, '');
|
|
72
|
+
writeFileSync(logFile, '');
|
|
73
|
+
const calls = { total: 0, failed: 0 };
|
|
74
|
+
const say = (line) => {
|
|
75
|
+
if (!options.json)
|
|
76
|
+
process.stderr.write(`${line}\n`);
|
|
77
|
+
};
|
|
78
|
+
const log = (level, source, message) => {
|
|
79
|
+
appendFileSync(logFile, `${jsonLine({ at: new Date().toISOString(), level, source, message: message.length === 1 ? message[0] : message })}\n`);
|
|
80
|
+
};
|
|
81
|
+
const record = (call) => {
|
|
82
|
+
calls.total += 1;
|
|
83
|
+
if (!call.ok)
|
|
84
|
+
calls.failed += 1;
|
|
85
|
+
appendFileSync(callsFile, `${jsonLine(call)}\n`);
|
|
86
|
+
const [surface, method] = call.call.split('.');
|
|
87
|
+
if (surface === 'log' && method && LOG_METHODS.has(method))
|
|
88
|
+
log(method, 'ctx.api.log', Array.isArray(call.input) ? call.input : [call.input]);
|
|
89
|
+
else
|
|
90
|
+
say(` ${call.ok ? '·' : '✗'} ${call.call} ${call.ms}ms${call.ok ? '' : `: ${call.error}`}`);
|
|
91
|
+
};
|
|
92
|
+
const session = await openExecSession(repoRoot, { env: options.env, instance: options.instance, url: options.url, record });
|
|
93
|
+
const { run, ctx: traced } = session;
|
|
94
|
+
say(`exec ${path.relative(repoRoot, source)} in ${run.environment.name}${run.instance ? ` on ${run.instance.sequence} case ${run.instance.id}` : ''}`);
|
|
95
|
+
// This repository's code and schema serve the environment while the snippet runs: local code, the environment's data.
|
|
96
|
+
const overlay = await openOverlay(repoRoot, { backendUrl: session.backendUrl, token: session.loginToken, environmentId: run.environment.id });
|
|
97
|
+
say(`serving ${overlay.sequences.join(', ')} from this repository`);
|
|
98
|
+
// The snippet's console: kept in log.jsonl, and shown as usual unless --json.
|
|
99
|
+
const original = { ...console };
|
|
100
|
+
for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
|
|
101
|
+
console[level] = (...message) => {
|
|
102
|
+
log(level === 'log' ? 'info' : level, 'console', message);
|
|
103
|
+
if (!options.json)
|
|
104
|
+
original[level](...message);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let result;
|
|
108
|
+
try {
|
|
109
|
+
const { tsImport } = await import('tsx/esm/api');
|
|
110
|
+
const mod = (await tsImport(pathToFileURL(source).href, import.meta.url));
|
|
111
|
+
if (typeof mod.default !== 'function') {
|
|
112
|
+
throw new Error(`${path.relative(repoRoot, source)}: the default export is the snippet, (ctx, args) => value`);
|
|
113
|
+
}
|
|
114
|
+
const value = await mod.default(traced, args);
|
|
115
|
+
result = { ok: true, file: path.relative(repoRoot, source), environment: run.environment, instance: run.instance, startedAt: startedAt.toISOString(), durationMs: Date.now() - startedAt.getTime(), calls, value, dir: path.relative(repoRoot, dir) };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const err = error instanceof Error ? { message: explain(error.message), ...(error.stack ? { stack: error.stack } : {}) } : { message: String(error) };
|
|
119
|
+
result = { ok: false, file: path.relative(repoRoot, source), environment: run.environment, instance: run.instance, startedAt: startedAt.toISOString(), durationMs: Date.now() - startedAt.getTime(), calls, error: err, dir: path.relative(repoRoot, dir) };
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
Object.assign(console, original);
|
|
123
|
+
session.close();
|
|
124
|
+
await overlay.close();
|
|
125
|
+
}
|
|
126
|
+
writeFileSync(path.join(dir, 'result.json'), `${JSON.stringify(JSON.parse(jsonLine(result)), null, 2)}\n`);
|
|
127
|
+
if (options.json) {
|
|
128
|
+
process.stdout.write(`${jsonLine(result)}\n`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
say(result.ok ? `ok in ${result.durationMs}ms, ${calls.total} ctx.api calls${calls.failed ? ` (${calls.failed} failed)` : ''}` : `failed: ${result.error.message}`);
|
|
132
|
+
if (result.ok && result.value !== undefined)
|
|
133
|
+
process.stdout.write(`${JSON.stringify(JSON.parse(jsonLine(result.value)), null, 2)}\n`);
|
|
134
|
+
say(`files: ${result.dir}/ (result.json, calls.jsonl, log.jsonl)`);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SystemContext } from '../../core/context/types.js';
|
|
2
|
+
import type { ActionDataSlice } from '../../fn-context/types.js';
|
|
3
|
+
import { type ApiCall } from './trace.js';
|
|
4
|
+
export type ExecRun = {
|
|
5
|
+
environment: {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
instance: {
|
|
10
|
+
id: string;
|
|
11
|
+
sequence: string;
|
|
12
|
+
} | null;
|
|
13
|
+
runToken: string;
|
|
14
|
+
data: ActionDataSlice;
|
|
15
|
+
};
|
|
16
|
+
export type ExecSession = {
|
|
17
|
+
repoRoot: string;
|
|
18
|
+
backendUrl: string;
|
|
19
|
+
/** The CLI's login (serve token): what opens a Local Serve on the environment. */
|
|
20
|
+
loginToken: string;
|
|
21
|
+
run: ExecRun;
|
|
22
|
+
/** The real `ctx`: its `ctx.api` calls reach the environment, each one recorded. */
|
|
23
|
+
ctx: SystemContext;
|
|
24
|
+
/**
|
|
25
|
+
* The run token, renewed before it expires (it lasts 15 minutes: a long test file or an open `mobile`
|
|
26
|
+
* outlives it); `force` renews it now (a request that was refused). `run.runToken` is the current one.
|
|
27
|
+
*/
|
|
28
|
+
token(force?: boolean): Promise<string>;
|
|
29
|
+
/** Stops renewing the token. */
|
|
30
|
+
close(): void;
|
|
31
|
+
};
|
|
32
|
+
/** A run token older than this is renewed before it is used; it expires at 15 minutes. */
|
|
33
|
+
export declare const EXEC_TOKEN_RENEW_MS: number;
|
|
34
|
+
/**
|
|
35
|
+
* A token renewed by `renew` once it is older than {@link EXEC_TOKEN_RENEW_MS}, or when `force`d; calls
|
|
36
|
+
* made while one renewal is under way share it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function renewingToken(initial: string, renew: () => Promise<string>, now?: () => number): (force?: boolean) => Promise<string>;
|
|
39
|
+
export declare function rpcUrlOf(backendUrl: string): string;
|
|
40
|
+
/** Asks the backend for the run: its token, scope and `ctx` data. */
|
|
41
|
+
export declare function requestExecRun(backendUrl: string, token: string, env: string, instance: string | undefined): Promise<ExecRun>;
|
|
42
|
+
export declare function openExecSession(repoRoot: string, options: {
|
|
43
|
+
env?: string;
|
|
44
|
+
instance?: string;
|
|
45
|
+
url?: string;
|
|
46
|
+
record: (call: ApiCall) => void;
|
|
47
|
+
}): Promise<ExecSession>;
|
|
48
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One exec session: the CLI's login traded for a run token on one environment (the backend's
|
|
3
|
+
* `exec/exec-run.ts`), and a real `ctx` built with it, every `ctx.api` call recorded. `exec <file>` runs
|
|
4
|
+
* a snippet in one; the integration tests' `connect()` keeps one open for a test file.
|
|
5
|
+
*/
|
|
6
|
+
import { runtimePackageSource, runtimePackageVersion } from '../../builder/compile/compile-sequence.js';
|
|
7
|
+
import { buildActionContext } from '../../fn-context/build-context.js';
|
|
8
|
+
import { createRpcApiHost } from '../../fn-context/rpc/trpc-client.js';
|
|
9
|
+
import { setRuntimeLogLevel } from '../../logger.js';
|
|
10
|
+
import { loadCachedServeAuthorization, resolveBackendUrl } from '../auth/auth.js';
|
|
11
|
+
import { toHttpUrl } from '../auth/backend-url.js';
|
|
12
|
+
import { apiUrl, requestJson } from '../auth/http.js';
|
|
13
|
+
import { traceApi } from './trace.js';
|
|
14
|
+
/** A run token older than this is renewed before it is used; it expires at 15 minutes. */
|
|
15
|
+
export const EXEC_TOKEN_RENEW_MS = 10 * 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* A token renewed by `renew` once it is older than {@link EXEC_TOKEN_RENEW_MS}, or when `force`d; calls
|
|
18
|
+
* made while one renewal is under way share it.
|
|
19
|
+
*/
|
|
20
|
+
export function renewingToken(initial, renew, now = Date.now) {
|
|
21
|
+
let current = initial;
|
|
22
|
+
let issuedAt = now();
|
|
23
|
+
let renewing = null;
|
|
24
|
+
return (force = false) => {
|
|
25
|
+
if (!force && now() - issuedAt < EXEC_TOKEN_RENEW_MS)
|
|
26
|
+
return Promise.resolve(current);
|
|
27
|
+
renewing ??= renew()
|
|
28
|
+
.then((next) => {
|
|
29
|
+
current = next;
|
|
30
|
+
issuedAt = now();
|
|
31
|
+
return current;
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
renewing = null;
|
|
35
|
+
});
|
|
36
|
+
return renewing;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function rpcUrlOf(backendUrl) {
|
|
40
|
+
const url = toHttpUrl(backendUrl);
|
|
41
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/trpc`;
|
|
42
|
+
url.search = '';
|
|
43
|
+
url.hash = '';
|
|
44
|
+
return url.toString();
|
|
45
|
+
}
|
|
46
|
+
/** Asks the backend for the run: its token, scope and `ctx` data. */
|
|
47
|
+
export async function requestExecRun(backendUrl, token, env, instance) {
|
|
48
|
+
const { response, payload } = await requestJson(apiUrl(backendUrl, 'cli-auth/exec'), {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ environment: env, ...(instance ? { instanceId: instance } : {}) }),
|
|
52
|
+
});
|
|
53
|
+
if (response.status === 401)
|
|
54
|
+
throw new Error('the login was refused: run `superpanel-sdk-runtime login` again');
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(`exec: ${payload.error ?? `the backend answered ${response.status}`}`);
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
export async function openExecSession(repoRoot, options) {
|
|
60
|
+
const backendUrl = resolveBackendUrl(repoRoot, options.url);
|
|
61
|
+
const authorization = await loadCachedServeAuthorization(backendUrl);
|
|
62
|
+
if (!authorization)
|
|
63
|
+
throw new Error('not logged in: run `superpanel-sdk-runtime login` first');
|
|
64
|
+
// The runtime's own routine lines (each RPC's start and end) are not the caller's: only its warnings show.
|
|
65
|
+
setRuntimeLogLevel('WARN');
|
|
66
|
+
const run = await requestExecRun(backendUrl, authorization.token, options.env ?? 'sandbox', options.instance);
|
|
67
|
+
const token = renewingToken(run.runToken, async () => (run.runToken = (await requestExecRun(backendUrl, authorization.token, options.env ?? 'sandbox', options.instance)).runToken));
|
|
68
|
+
// Kept fresh while nothing calls (a `mobile` left open): the timer does not keep the process alive.
|
|
69
|
+
const keepFresh = setInterval(() => void token().catch(() => undefined), 60_000);
|
|
70
|
+
keepFresh.unref();
|
|
71
|
+
const data = { ...run.data, config: { ...run.data.config, backendUrl: rpcUrlOf(backendUrl) } };
|
|
72
|
+
const host = createRpcApiHost({
|
|
73
|
+
baseUrl: data.config.backendUrl,
|
|
74
|
+
// Renewed first when it is old (a session left open past its life, the timer held back by a sleep).
|
|
75
|
+
getToken: () => token(),
|
|
76
|
+
runtimeVersion: runtimePackageVersion(),
|
|
77
|
+
runtimeSource: runtimePackageSource(repoRoot),
|
|
78
|
+
});
|
|
79
|
+
const ctx = await buildActionContext(data, host);
|
|
80
|
+
return { repoRoot, backendUrl, loginToken: authorization.token, run, ctx: { ...ctx, api: traceApi(ctx.api, options.record) }, token, close: () => clearInterval(keepFresh) };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Every `ctx.api` call of an exec session, recorded: what `calls.jsonl` and an integration test's `calls` hold. */
|
|
2
|
+
/** One `ctx.api` call, as `calls.jsonl` keeps it. */
|
|
3
|
+
export type ApiCall = {
|
|
4
|
+
at: string;
|
|
5
|
+
ms: number;
|
|
6
|
+
call: string;
|
|
7
|
+
input: unknown;
|
|
8
|
+
ok: boolean;
|
|
9
|
+
output?: unknown;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* `api` with every method recorded: its path (`db.contact.findMany`, `instances.notify`,
|
|
14
|
+
* `integration.envs.0.get`), arguments and answer. `then` is left alone, so the object is not taken for a
|
|
15
|
+
* promise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function traceApi<T extends object>(api: T, record: (call: ApiCall) => void, prefix?: string[]): T;
|
|
18
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Every `ctx.api` call of an exec session, recorded: what `calls.jsonl` and an integration test's `calls` hold. */
|
|
2
|
+
/**
|
|
3
|
+
* `api` with every method recorded: its path (`db.contact.findMany`, `instances.notify`,
|
|
4
|
+
* `integration.envs.0.get`), arguments and answer. `then` is left alone, so the object is not taken for a
|
|
5
|
+
* promise.
|
|
6
|
+
*/
|
|
7
|
+
export function traceApi(api, record, prefix = []) {
|
|
8
|
+
const wrapped = new WeakMap();
|
|
9
|
+
const wrap = (value, at) => {
|
|
10
|
+
if (typeof value === 'function') {
|
|
11
|
+
return (...args) => {
|
|
12
|
+
const started = Date.now();
|
|
13
|
+
const call = at.join('.');
|
|
14
|
+
const input = args.length <= 1 ? args[0] : args;
|
|
15
|
+
const done = (ok, outcome) => record({ at: new Date(started).toISOString(), ms: Date.now() - started, call, input, ok, ...outcome });
|
|
16
|
+
try {
|
|
17
|
+
const out = value(...args);
|
|
18
|
+
if (out && typeof out.then === 'function') {
|
|
19
|
+
return out.then((output) => (done(true, { output }), output), (error) => {
|
|
20
|
+
done(false, { error: error instanceof Error ? error.message : String(error) });
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
done(true, { output: out });
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
done(false, { error: error instanceof Error ? error.message : String(error) });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (value && typeof value === 'object') {
|
|
34
|
+
const hit = wrapped.get(value);
|
|
35
|
+
if (hit)
|
|
36
|
+
return hit;
|
|
37
|
+
const proxy = new Proxy(value, {
|
|
38
|
+
get: (target, key, receiver) => {
|
|
39
|
+
const inner = Reflect.get(target, key, receiver);
|
|
40
|
+
if (typeof key === 'symbol' || key === 'then')
|
|
41
|
+
return inner;
|
|
42
|
+
return wrap(inner, [...at, key]);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
wrapped.set(value, proxy);
|
|
46
|
+
return proxy;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
return wrap(api, prefix);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=trace.js.map
|
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { runLogin, runLogout, runWhoami } from './auth/index.js';
|
|
5
6
|
import { runCompile } from './compile/index.js';
|
|
7
|
+
import { runExec } from './exec/index.js';
|
|
8
|
+
import { runLogs } from './logs/index.js';
|
|
9
|
+
import { runMobile } from './mobile/index.js';
|
|
10
|
+
import { runTurnCli } from './turns/run.js';
|
|
11
|
+
import { runTurnExport, runTurns } from './turns/index.js';
|
|
6
12
|
import { runServe } from './serve/index.js';
|
|
7
13
|
import { runTypeGen } from './type-gen/index.js';
|
|
8
14
|
function loadCwdEnv() {
|
|
@@ -14,7 +20,7 @@ function loadCwdEnv() {
|
|
|
14
20
|
const program = new Command();
|
|
15
21
|
program
|
|
16
22
|
.name('superpanel-sdk-runtime')
|
|
17
|
-
.description('Company-repo tooling: type generation
|
|
23
|
+
.description('Company-repo tooling: type generation, sequence compile, Local Serve, code snippets and logs')
|
|
18
24
|
.showHelpAfterError()
|
|
19
25
|
.configureHelp({ sortSubcommands: true });
|
|
20
26
|
program
|
|
@@ -35,19 +41,116 @@ program
|
|
|
35
41
|
.action(async (options) => {
|
|
36
42
|
await runCompile(process.cwd(), { sequenceId: options.sequence });
|
|
37
43
|
});
|
|
44
|
+
program
|
|
45
|
+
.command('exec <file>')
|
|
46
|
+
.description("Run a code snippet (a .ts file whose default export is (ctx, args) => value) on this machine against an environment's real data; what it writes is written. Its result, every ctx.api call and its logs go to .superpanel/exec/<time>-<name>/")
|
|
47
|
+
.option('--env <name|id>', 'The environment (not prod)', 'sandbox')
|
|
48
|
+
.option('--instance <id>', 'Run on this case: its sequence, and writes scoped to it')
|
|
49
|
+
.option('--args <json>', "The snippet's second argument, as JSON")
|
|
50
|
+
.option('--json', 'Print only the result as JSON')
|
|
51
|
+
.option('--url <url>', 'Backend host URL')
|
|
52
|
+
.action(async (file, options) => {
|
|
53
|
+
const result = await runExec(process.cwd(), file, options);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command('logs')
|
|
59
|
+
.description("What this repository's code wrote (ctx.api.log, the page's console, its functions' failures) in an environment, from the platform's log store: oldest first, kept in .superpanel/logs/<time>.jsonl")
|
|
60
|
+
.option('--env <name|id>', 'The environment (prod too: reading only)', 'sandbox')
|
|
61
|
+
.option('--since <time>', 'From: 15m, 2h, 3d ago, or ISO 8601 (default: the last hour)')
|
|
62
|
+
.option('--until <time>', 'Until: the same forms')
|
|
63
|
+
.option('--instance <id>', "One case's lines")
|
|
64
|
+
.option('--run <id>', "One run's lines")
|
|
65
|
+
.option('--contact <id>', "One contact's lines")
|
|
66
|
+
.option('--sequence <name>', "One sequence's lines")
|
|
67
|
+
.option('--request <id>', "One request's lines")
|
|
68
|
+
.option('--level <level>', 'warn: warn and error; or a list: info,error')
|
|
69
|
+
.option('--search <text>', 'Text anywhere in the line or its data')
|
|
70
|
+
.option('--limit <n>', 'Lines at most (default 200, at most 500)', (value) => Number(value))
|
|
71
|
+
.option('--follow', 'Then keep showing new lines as they come')
|
|
72
|
+
.option('--json', 'One JSON per line')
|
|
73
|
+
.option('--url <url>', 'Backend host URL')
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
await runLogs(process.cwd(), options);
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command('turns [action] [turnId]')
|
|
79
|
+
.description("The turns of a case, a run or a call in a non-prod environment, one line each (what the customer said, what the AI said and did); `turns export <turnId>` writes one turn and what it began with to .superpanel/turns/<turnId>/")
|
|
80
|
+
.option('--env <name|id>', 'The environment (not prod)', 'sandbox')
|
|
81
|
+
.option('--instance <id>', "One case's turns")
|
|
82
|
+
.option('--call <id>', "One call's turns")
|
|
83
|
+
.option('--run <id>', "One run's turns")
|
|
84
|
+
.option('--since <time>', 'From: 15m, 2h, 3d ago, or ISO 8601')
|
|
85
|
+
.option('--limit <n>', 'Turns at most (default and at most 200)', (value) => Number(value))
|
|
86
|
+
.option('--out <dir>', 'export: the directory to write (default .superpanel/turns/<turnId>/)')
|
|
87
|
+
.option('--json', 'One JSON per turn')
|
|
88
|
+
.option('--url <url>', 'Backend host URL')
|
|
89
|
+
.action(async (action, turnId, options) => {
|
|
90
|
+
if (action === undefined)
|
|
91
|
+
return runTurns(process.cwd(), options);
|
|
92
|
+
if (action !== 'export' || !turnId)
|
|
93
|
+
throw new Error('turns: `turns --instance <id>` lists, `turns export <turnId>` writes one');
|
|
94
|
+
return runTurnExport(process.cwd(), turnId, options);
|
|
95
|
+
});
|
|
96
|
+
program
|
|
97
|
+
.command('turn')
|
|
98
|
+
.description("One turn as a shadow (68 §8), with this repository's code: from a turn that ran (--from: its id or exported directory) or on a case now (--instance, --say); what it said, pressed and wrote, nothing kept")
|
|
99
|
+
.option('--env <name|id>', 'The environment (not prod)', 'sandbox')
|
|
100
|
+
.option('--from <turnId|dir>', 'Start where this turn started')
|
|
101
|
+
.option('--instance <id>', 'The case, as it is now')
|
|
102
|
+
.option('--channel <channel>', 'sms, email or call (with --instance; default sms)')
|
|
103
|
+
.option('--say <text>', "What the customer says (with --from: in place of what they said then)")
|
|
104
|
+
.option('--caller <id>', "The conversation's contact (default: the case's subject)")
|
|
105
|
+
.option('--repeat <n>', 'Run it n times (1-10)', (value) => Number(value))
|
|
106
|
+
.option('--json', 'One JSON per run')
|
|
107
|
+
.option('--url <url>', 'Backend host URL')
|
|
108
|
+
.action(async (options) => {
|
|
109
|
+
await runTurnCli(process.cwd(), options);
|
|
110
|
+
});
|
|
111
|
+
program
|
|
112
|
+
.command('mobile')
|
|
113
|
+
.description("A test customer's phone on a non-prod environment, served by this repository's code: call a line and talk in text, text and email it, and get its texts, emails and calls as they come (68 §5.4); /help inside")
|
|
114
|
+
.option('--env <name|id>', 'The environment (not prod)', 'sandbox')
|
|
115
|
+
.option('--line <name>', 'The line you call and text (default Main line)')
|
|
116
|
+
.option('--mailbox <name>', 'The mailbox you email (default Support)')
|
|
117
|
+
.option('--new', "A new test customer instead of this repository's last one")
|
|
118
|
+
.option('--url <url>', 'Backend host URL')
|
|
119
|
+
.action(async (options) => {
|
|
120
|
+
await runMobile(process.cwd(), options);
|
|
121
|
+
});
|
|
122
|
+
program
|
|
123
|
+
.command('login')
|
|
124
|
+
.description('Authorize Local Serve and cache the credential')
|
|
125
|
+
.option('--url <url>', 'Backend host URL')
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
await runLogin(process.cwd(), options);
|
|
128
|
+
});
|
|
129
|
+
program
|
|
130
|
+
.command('logout')
|
|
131
|
+
.description('Clear the cached Local Serve credential')
|
|
132
|
+
.option('--url <url>', 'Backend host URL')
|
|
133
|
+
.action(async (options) => {
|
|
134
|
+
await runLogout(process.cwd(), options);
|
|
135
|
+
});
|
|
136
|
+
program
|
|
137
|
+
.command('whoami')
|
|
138
|
+
.description('Show the cached Local Serve login')
|
|
139
|
+
.option('--url <url>', 'Backend host URL')
|
|
140
|
+
.action(async (options) => {
|
|
141
|
+
await runWhoami(process.cwd(), options);
|
|
142
|
+
});
|
|
38
143
|
program
|
|
39
144
|
.command('serve')
|
|
40
|
-
.description('Watch
|
|
41
|
-
.option('--url <url>', 'Backend host
|
|
42
|
-
.option('--
|
|
43
|
-
.option('--
|
|
44
|
-
.option('--sequence <id>', 'Serve a single sequence (default: all with slots)')
|
|
145
|
+
.description('Watch the whole repository and serve it in place of an environment\'s live release, over WebSocket')
|
|
146
|
+
.option('--url <url>', 'Backend host URL (env SUPERPANEL_SDK_BACKEND_URL)')
|
|
147
|
+
.option('--no-open', 'Copy the authorization URL to the clipboard instead of opening a browser')
|
|
148
|
+
.option('--environment <id>', 'Target non-production environment id (prompts in a terminal if omitted)')
|
|
45
149
|
.action(async (options) => {
|
|
46
150
|
await runServe(process.cwd(), {
|
|
47
151
|
url: options.url,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
sequenceIds: options.sequence ? [options.sequence] : undefined,
|
|
152
|
+
openBrowser: options.open,
|
|
153
|
+
environment: options.environment,
|
|
51
154
|
});
|
|
52
155
|
});
|
|
53
156
|
async function main() {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type LogsQuery = {
|
|
2
|
+
env?: string;
|
|
3
|
+
since?: string;
|
|
4
|
+
until?: string;
|
|
5
|
+
instance?: string;
|
|
6
|
+
run?: string;
|
|
7
|
+
contact?: string;
|
|
8
|
+
sequence?: string;
|
|
9
|
+
request?: string;
|
|
10
|
+
level?: string;
|
|
11
|
+
search?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
};
|
|
14
|
+
export type LogLine = {
|
|
15
|
+
id: string;
|
|
16
|
+
at: string;
|
|
17
|
+
level: string;
|
|
18
|
+
message: string | null;
|
|
19
|
+
tags: string[];
|
|
20
|
+
sequence: string | null;
|
|
21
|
+
contactId: string | null;
|
|
22
|
+
requestId: string | null;
|
|
23
|
+
data: unknown;
|
|
24
|
+
};
|
|
25
|
+
/** `15m`, `2h`, `3d` back from `now`, or an ISO time, as ISO; undefined when not given. */
|
|
26
|
+
export declare function timeOf(value: string | undefined, now?: number): string | undefined;
|
|
27
|
+
/** `--level warn`: that level and above; `--level info,error`: those. */
|
|
28
|
+
export declare function levelsOf(value: string | undefined): string[] | undefined;
|
|
29
|
+
/** One line as the terminal shows it: time, level, sequence, message, its data. */
|
|
30
|
+
export declare function formatLine(line: LogLine): string;
|
|
31
|
+
/** A logs reader as the CLI's login: one read of the window asked. */
|
|
32
|
+
export declare function openLogReader(repoRoot: string, url?: string): Promise<(query: LogsQuery & {
|
|
33
|
+
order?: 'asc' | 'desc';
|
|
34
|
+
}) => Promise<{
|
|
35
|
+
environment: {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
39
|
+
lines: LogLine[];
|
|
40
|
+
hasMore: boolean;
|
|
41
|
+
}>>;
|
|
42
|
+
export declare function runLogs(repoRoot: string, query: LogsQuery & {
|
|
43
|
+
follow?: boolean;
|
|
44
|
+
json?: boolean;
|
|
45
|
+
url?: string;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `logs`: what the repository's code wrote (`ctx.api.log`, the page's `console`, its functions' failures)
|
|
3
|
+
* in one environment, read from the platform's log store on demand as the CLI's login (the backend's
|
|
4
|
+
* `logging/cli-logs.ts`). Nothing is collected: each read names a window and takes a page, and `--follow`
|
|
5
|
+
* reads only what came after the last line it showed. Lines are shown oldest first and kept in
|
|
6
|
+
* `.superpanel/logs/<time>.jsonl`, one JSON per line, for `grep` / `jq`.
|
|
7
|
+
*/
|
|
8
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { loadCachedServeAuthorization, resolveBackendUrl } from '../auth/auth.js';
|
|
11
|
+
import { apiUrl } from '../auth/http.js';
|
|
12
|
+
const LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
13
|
+
/** `15m`, `2h`, `3d` back from `now`, or an ISO time, as ISO; undefined when not given. */
|
|
14
|
+
export function timeOf(value, now = Date.now()) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return undefined;
|
|
17
|
+
const ago = /^(\d+)\s*(s|m|h|d)$/.exec(value.trim());
|
|
18
|
+
if (ago) {
|
|
19
|
+
const unit = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[ago[2]];
|
|
20
|
+
return new Date(now - Number(ago[1]) * unit).toISOString();
|
|
21
|
+
}
|
|
22
|
+
const at = Date.parse(value);
|
|
23
|
+
if (Number.isNaN(at))
|
|
24
|
+
throw new Error(`${value}: a time is 15m, 2h, 3d (that long ago) or ISO 8601`);
|
|
25
|
+
return new Date(at).toISOString();
|
|
26
|
+
}
|
|
27
|
+
/** `--level warn`: that level and above; `--level info,error`: those. */
|
|
28
|
+
export function levelsOf(value) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return undefined;
|
|
31
|
+
const asked = value.split(',').map((level) => level.trim().toLowerCase()).filter(Boolean);
|
|
32
|
+
for (const level of asked)
|
|
33
|
+
if (!LEVELS.includes(level))
|
|
34
|
+
throw new Error(`--level: ${level} is not one of ${LEVELS.join(', ')}`);
|
|
35
|
+
return asked.length === 1 ? LEVELS.slice(LEVELS.indexOf(asked[0])) : asked;
|
|
36
|
+
}
|
|
37
|
+
/** One line as the terminal shows it: time, level, sequence, message, its data. */
|
|
38
|
+
export function formatLine(line) {
|
|
39
|
+
const time = line.at.replace('T', ' ').replace(/Z$/, '');
|
|
40
|
+
const data = line.data && (typeof line.data !== 'object' || Object.keys(line.data).length > 0) ? ` ${typeof line.data === 'string' ? line.data : JSON.stringify(line.data)}` : '';
|
|
41
|
+
return `${time} ${line.level.toUpperCase().padEnd(5)} ${line.sequence ? `[${line.sequence}] ` : ''}${line.message ?? ''}${data}`;
|
|
42
|
+
}
|
|
43
|
+
/** A logs reader as the CLI's login: one read of the window asked. */
|
|
44
|
+
export async function openLogReader(repoRoot, url) {
|
|
45
|
+
const backendUrl = resolveBackendUrl(repoRoot, url);
|
|
46
|
+
const authorization = await loadCachedServeAuthorization(backendUrl);
|
|
47
|
+
if (!authorization)
|
|
48
|
+
throw new Error('not logged in: run `superpanel-sdk-runtime login` first');
|
|
49
|
+
return async (query) => {
|
|
50
|
+
const response = await fetch(apiUrl(backendUrl, 'cli-auth/logs'), {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { Authorization: `Bearer ${authorization.token}`, 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
environment: query.env ?? 'sandbox',
|
|
55
|
+
...(timeOf(query.since) ? { since: timeOf(query.since) } : {}),
|
|
56
|
+
...(timeOf(query.until) ? { until: timeOf(query.until) } : {}),
|
|
57
|
+
...(query.instance ? { instanceId: query.instance } : {}),
|
|
58
|
+
...(query.run ? { runId: query.run } : {}),
|
|
59
|
+
...(query.contact ? { contactId: query.contact } : {}),
|
|
60
|
+
...(query.sequence ? { sequence: query.sequence } : {}),
|
|
61
|
+
...(query.request ? { requestId: query.request } : {}),
|
|
62
|
+
...(levelsOf(query.level) ? { levels: levelsOf(query.level) } : {}),
|
|
63
|
+
...(query.search ? { search: query.search } : {}),
|
|
64
|
+
...(query.order ? { order: query.order } : {}),
|
|
65
|
+
...(query.limit ? { limit: query.limit } : {}),
|
|
66
|
+
}),
|
|
67
|
+
signal: AbortSignal.timeout(60_000),
|
|
68
|
+
});
|
|
69
|
+
const payload = (await response.json().catch(() => ({})));
|
|
70
|
+
if (response.status === 401)
|
|
71
|
+
throw new Error('the login was refused: run `superpanel-sdk-runtime login` again');
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
throw new Error(`logs: ${payload.error ?? `the backend answered ${response.status}`}`);
|
|
74
|
+
return payload;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function stamp(date) {
|
|
78
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
79
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
80
|
+
}
|
|
81
|
+
export async function runLogs(repoRoot, query) {
|
|
82
|
+
const read = await openLogReader(repoRoot, query.url);
|
|
83
|
+
const dir = path.join(repoRoot, '.superpanel', 'logs');
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
const file = path.join(dir, `${stamp(new Date())}.jsonl`);
|
|
86
|
+
writeFileSync(file, '');
|
|
87
|
+
const show = (line) => {
|
|
88
|
+
appendFileSync(file, `${JSON.stringify(line)}\n`);
|
|
89
|
+
process.stdout.write(`${query.json ? JSON.stringify(line) : formatLine(line)}\n`);
|
|
90
|
+
};
|
|
91
|
+
// The latest page, shown oldest first.
|
|
92
|
+
const first = await read({ ...query, order: 'desc' });
|
|
93
|
+
const lines = [...first.lines].reverse();
|
|
94
|
+
lines.forEach(show);
|
|
95
|
+
process.stderr.write(`${lines.length} line${lines.length === 1 ? '' : 's'} from ${first.environment.name}${first.hasMore ? ` (more before: narrow it, or --since / --limit)` : ''}; kept in ${path.relative(repoRoot, file)}\n`);
|
|
96
|
+
if (!query.follow)
|
|
97
|
+
return;
|
|
98
|
+
// Then what comes after the last line shown, every few seconds, until stopped.
|
|
99
|
+
let after = lines.at(-1)?.at ?? new Date().toISOString();
|
|
100
|
+
const seen = new Set(lines.map((line) => line.id));
|
|
101
|
+
for (;;) {
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 3_000));
|
|
103
|
+
const next = await read({ ...query, since: after, until: undefined, order: 'asc' });
|
|
104
|
+
for (const line of next.lines) {
|
|
105
|
+
if (seen.has(line.id))
|
|
106
|
+
continue;
|
|
107
|
+
seen.add(line.id);
|
|
108
|
+
show(line);
|
|
109
|
+
after = line.at;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MobileOptions = {
|
|
2
|
+
env?: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
/** The line calls and texts go to (its name or number); `Main line` when left out. */
|
|
5
|
+
line?: string;
|
|
6
|
+
/** The mailbox emails go to (its name or address); `Support` when left out. */
|
|
7
|
+
mailbox?: string;
|
|
8
|
+
/** A new test customer instead of this repository's last one for the environment. */
|
|
9
|
+
new?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function runMobile(repoRoot: string, options: MobileOptions): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|