primitive-admin 1.1.0-alpha.81 → 1.1.0-alpha.83
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/assets/skill/skills/primitive-platform/SKILL.md +12 -0
- package/dist/src/commands/documents.js +455 -22
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +889 -76
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/locks.js +15 -1
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/prompts.js +19 -2
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sync.d.ts +50 -0
- package/dist/src/commands/sync.js +587 -316
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +1 -29
- package/dist/src/commands/workflows.js +8 -109
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +191 -2
- package/dist/src/lib/api-client.js +283 -7
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +12 -4
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +78 -0
- package/dist/src/lib/document-ingest.js +231 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +2 -2
- package/dist/src/lib/env-resolver-core.js +2 -2
- package/dist/src/lib/function-bundle.d.ts +6 -0
- package/dist/src/lib/function-bundle.js +8 -0
- package/dist/src/lib/function-bundle.js.map +1 -1
- package/dist/src/lib/function-collect.d.ts +1 -1
- package/dist/src/lib/function-collect.js +16 -0
- package/dist/src/lib/function-collect.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +4 -0
- package/dist/src/lib/function-db-types.js +46 -2
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +83 -0
- package/dist/src/lib/function-document-types.js +137 -0
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/function-grants-preflight.js +5 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -1
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +51 -2
- package/dist/src/lib/function-log-tail.js +160 -13
- package/dist/src/lib/function-log-tail.js.map +1 -1
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +17 -9
- package/dist/src/lib/function-schema-codegen.js +30 -29
- package/dist/src/lib/function-schema-codegen.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +226 -30
- package/dist/src/lib/function-sync.js +731 -77
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +561 -91
- package/dist/src/lib/generated-config-surfaces.js +1858 -287
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +2 -0
- package/dist/src/lib/log-inspection.js +4 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.js +28 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/prompt-schema-codegen.d.ts +94 -0
- package/dist/src/lib/prompt-schema-codegen.js +212 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +26 -13
- package/dist/src/lib/swift-codegen/functionGenerator.js +38 -27
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +45 -2
- package/dist/src/lib/sync-selectors.js +66 -5
- package/dist/src/lib/sync-selectors.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +9 -5
- package/dist/src/lib/workflow-toml-validator.js +18 -10
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +2 -2
|
@@ -21,17 +21,22 @@ import { join, relative as relativePath } from "path";
|
|
|
21
21
|
import { ApiClient } from "../lib/api-client.js";
|
|
22
22
|
import { resolveAppId } from "../lib/config.js";
|
|
23
23
|
import { resolveSyncDir } from "../lib/sync-paths.js";
|
|
24
|
-
import { activeVersionMode } from "../lib/function-sync.js";
|
|
25
24
|
import { FunctionSchemaCodegenError, functionSchemaSources, } from "../lib/function-schema-codegen.js";
|
|
26
25
|
import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTypeArtifacts, } from "../lib/function-db-types.js";
|
|
27
26
|
import { generateFunctionSwiftTypes } from "../lib/swift-codegen/functionGenerator.js";
|
|
28
27
|
import { SwiftCodegenError } from "../lib/swift-codegen/schemaToSwift.js";
|
|
29
28
|
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatDate, formatStatus, json, jsonLine, divider, } from "../lib/output.js";
|
|
30
29
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
30
|
+
import { FUNCTION_LOG_COLUMNS, functionLogCells, functionLogLine, } from "../lib/function-log-row.js";
|
|
31
|
+
import { activatedLine, activationOutput, activationPrompt, activationRefusalLine, functionVersionRows, newerVersionLine, versionLine, } from "../lib/function-versions.js";
|
|
31
32
|
import { parseStatusFilter } from "../lib/object-status-filter.js";
|
|
33
|
+
import { renderStepRunTable } from "../lib/step-run-table.js";
|
|
34
|
+
import { renderRunLogs } from "../lib/function-log-lines.js";
|
|
35
|
+
import { buildWorkflowStepEnvelope } from "../lib/log-inspection.js";
|
|
32
36
|
import { followLoop } from "../lib/watch.js";
|
|
33
37
|
import { buildLogFollowSource } from "../lib/function-log-tail.js";
|
|
34
38
|
import { toFunctionLogInspectionRow } from "../lib/log-inspection.js";
|
|
39
|
+
import { decidePageContinuation, describeIdentity, exitCodeForInvokeStatus, exitCodeForRunStatus, invokeIdLines, logsFlagConflict, refusalLines, renderIdLines, resolveRunFlags, runRuntimeCell, runSliceCells, runtimeForExecutionMode, resumeCommand, startIdLines, userTokenName, waitDelayMs, withMintedToken, EXIT_INTERRUPTED, EXIT_TIMED_OUT, USER_TOKEN_TTL, WAIT_DEFAULT_TIMEOUT_SECONDS, } from "../lib/function-run.js";
|
|
35
40
|
/**
|
|
36
41
|
* How far back one `--follow` tick will page to meet its high-water mark.
|
|
37
42
|
*
|
|
@@ -45,25 +50,26 @@ import { toFunctionLogInspectionRow } from "../lib/log-inspection.js";
|
|
|
45
50
|
*/
|
|
46
51
|
const FOLLOW_MAX_PAGES_PER_TICK = 10;
|
|
47
52
|
/**
|
|
48
|
-
*
|
|
53
|
+
* Which cell each shared invocation column reads, and how it is rendered.
|
|
49
54
|
*
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
55
|
+
* The ORDER lives in `FUNCTION_LOG_COLUMNS` (#3462); this is only the mapping
|
|
56
|
+
* from a header to the cell `functionLogCells` builds for it, so adding a
|
|
57
|
+
* column is one edit in each place rather than three copies of the order.
|
|
52
58
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
59
|
+
const LOG_COLUMN_KEYS = {
|
|
60
|
+
TIME: "startedAt",
|
|
61
|
+
STATUS: "status",
|
|
62
|
+
TRIGGER: "trigger",
|
|
63
|
+
RUNTIME: "runtime",
|
|
64
|
+
VERSION: "configId",
|
|
65
|
+
"RUN/INVOCATION ID": "runOrInvocation",
|
|
66
|
+
CODE: "errorCode",
|
|
67
|
+
ERROR: "firstError",
|
|
68
|
+
};
|
|
69
|
+
const LOG_COLUMN_FORMATS = {
|
|
70
|
+
TIME: formatDate,
|
|
71
|
+
STATUS: formatStatus,
|
|
72
|
+
};
|
|
67
73
|
export function registerFunctionsCommands(program) {
|
|
68
74
|
const functions = program
|
|
69
75
|
.command("functions")
|
|
@@ -142,18 +148,56 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
142
148
|
// The versions are append-only and nameless; `activeConfigId` is the
|
|
143
149
|
// only thing that says which one runs, so a listing without it would
|
|
144
150
|
// report the newest rather than the live one.
|
|
151
|
+
//
|
|
152
|
+
// #3462 — a POINT READ, not a drained listing scanned for the id. Past
|
|
153
|
+
// 25 versions the listing is a page and the active version may not be
|
|
154
|
+
// on it at all; one read by id is both cheaper and correct.
|
|
145
155
|
let active = null;
|
|
146
156
|
if (record?.activeConfigId) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
// #3455 — this single read is also what carries
|
|
158
|
+
// `documentSchemaModels`: the models this version can seed are
|
|
159
|
+
// derived from the schema INSIDE the envelope rather than stored on
|
|
160
|
+
// the row, so the LISTING (one row per version, no R2 read) cannot
|
|
161
|
+
// carry them and only the point read can. Best-effort — an older
|
|
162
|
+
// server sends no such field, and the line then reads exactly as it
|
|
163
|
+
// does for a version pushed without a schema.
|
|
164
|
+
try {
|
|
165
|
+
active = await client.getFunctionConfig(resolvedAppId, functionId, String(record.activeConfigId));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
active = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// #3462 — one row of the listing answers "is the active version the
|
|
172
|
+
// newest", which is what tells an operator this function is rolled back.
|
|
173
|
+
let newest = null;
|
|
174
|
+
try {
|
|
175
|
+
const page = await client.listFunctionConfigs(resolvedAppId, functionId, {
|
|
176
|
+
limit: 1,
|
|
177
|
+
});
|
|
178
|
+
newest = (page.items || [])[0] ?? null;
|
|
150
179
|
}
|
|
180
|
+
catch {
|
|
181
|
+
newest = null;
|
|
182
|
+
}
|
|
183
|
+
const newer = newerVersionLine(newest, record?.activeConfigId, formatDate);
|
|
151
184
|
if (options.json) {
|
|
152
|
-
json({
|
|
185
|
+
json({
|
|
186
|
+
...record,
|
|
187
|
+
activeConfig: active,
|
|
188
|
+
...(newer ? { newerVersion: newest } : {}),
|
|
189
|
+
});
|
|
153
190
|
return;
|
|
154
191
|
}
|
|
155
192
|
printResult("Function ID", record.functionId);
|
|
156
193
|
printResult("Key", record.functionKey);
|
|
194
|
+
// #3462 — which code this function is running, beside its identity. The
|
|
195
|
+
// version was only reachable by reading the section below, or by asking
|
|
196
|
+
// the API; an operator answering "what is deployed" reads this line.
|
|
197
|
+
printResult("Version", versionLine({
|
|
198
|
+
activeConfigId: record.activeConfigId,
|
|
199
|
+
contentHash: active?.contentHash,
|
|
200
|
+
}));
|
|
157
201
|
printResult("Description", record.description || "-");
|
|
158
202
|
printResult("Status", formatStatus(record.status));
|
|
159
203
|
printResult("Access", record.access || "-");
|
|
@@ -168,9 +212,9 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
168
212
|
printResult(" Config ID", active.configId);
|
|
169
213
|
printResult(" Entry", active.entry);
|
|
170
214
|
printResult(" Content Hash", active.contentHash || "-");
|
|
171
|
-
// #
|
|
172
|
-
//
|
|
173
|
-
|
|
215
|
+
// #3482 — no Mode line. The config says nothing about how a
|
|
216
|
+
// function runs: `invoke` runs it inside the request, `start` runs
|
|
217
|
+
// it as a task, and every function takes both.
|
|
174
218
|
printResult(" Limits", active.limits || "-");
|
|
175
219
|
// #3182 — what this version declared: since #3279 the egress
|
|
176
220
|
// allowlist, the secrets that may enter the sandbox and the
|
|
@@ -180,6 +224,28 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
180
224
|
? active.capabilities
|
|
181
225
|
: [];
|
|
182
226
|
printResult(" Capabilities", capabilities.length > 0 ? capabilities.join(", ") : "-");
|
|
227
|
+
// #3462 — who put this version live and when, from either door (a
|
|
228
|
+
// push or `functions activate`). A header written before that change
|
|
229
|
+
// carries neither field and reads `unknown` rather than naming an
|
|
230
|
+
// actor nobody recorded.
|
|
231
|
+
printResult(" Activated", activatedLine(record, formatDate));
|
|
232
|
+
if (newer) {
|
|
233
|
+
printResult(" Newer version", newer);
|
|
234
|
+
}
|
|
235
|
+
// #3455 — the document models this version can SEED on a first
|
|
236
|
+
// write. An operator asking "why did the first write not seed
|
|
237
|
+
// `institution`" gets the answer here: either the model is in this
|
|
238
|
+
// list, or the version was pushed without a document schema at all.
|
|
239
|
+
// A server that predates the field sends nothing, which reads the
|
|
240
|
+
// same way as a version pushed without one.
|
|
241
|
+
const documentSchemaModels = Array.isArray(active.documentSchemaModels)
|
|
242
|
+
? active.documentSchemaModels
|
|
243
|
+
: null;
|
|
244
|
+
printResult(" Document schema", documentSchemaModels === null
|
|
245
|
+
? "none — this version was pushed without one"
|
|
246
|
+
: documentSchemaModels.length > 0
|
|
247
|
+
? documentSchemaModels.join(", ")
|
|
248
|
+
: "declares no models");
|
|
183
249
|
}
|
|
184
250
|
// #3181 — the ONE surface a function's triggers are visible on. The
|
|
185
251
|
// hidden `AppWebhook` and `CronTrigger` rows are omitted from
|
|
@@ -192,17 +258,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
192
258
|
const cronEntries = Array.isArray(triggers.cron)
|
|
193
259
|
? triggers.cron
|
|
194
260
|
: [];
|
|
195
|
-
// #
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
const databaseEntries = Array.isArray(triggers.databases)
|
|
201
|
-
? triggers.databases
|
|
202
|
-
: [];
|
|
203
|
-
if (!triggers.webhook &&
|
|
204
|
-
cronEntries.length === 0 &&
|
|
205
|
-
databaseEntries.length === 0) {
|
|
261
|
+
// #3483 — an OLDER server still answers a `databases` key, and this
|
|
262
|
+
// command ignores it rather than branching on it: the trigger it
|
|
263
|
+
// described was removed, so a section about it would tell an operator
|
|
264
|
+
// that a write runs this function when nothing does.
|
|
265
|
+
if (!triggers.webhook && cronEntries.length === 0) {
|
|
206
266
|
printResult(" Triggers", "none — this function is invoked over HTTP only");
|
|
207
267
|
}
|
|
208
268
|
if (triggers.webhook) {
|
|
@@ -220,6 +280,9 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
220
280
|
{ header: "NAME", key: "name" },
|
|
221
281
|
{ header: "ID", key: "triggerId" },
|
|
222
282
|
{ header: "CRON", key: "cron" },
|
|
283
|
+
// #3454 — which runner this schedule's fires use. Always one of
|
|
284
|
+
// the two words: an `any` function's entry names it, and a
|
|
285
|
+
// locked one's takes its lock.
|
|
223
286
|
{ header: "TZ", key: "timezone" },
|
|
224
287
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
225
288
|
{ header: "NEXT FIRE", key: "nextFireAt", format: formatDate },
|
|
@@ -227,15 +290,6 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
227
290
|
{ header: "LAST RUN", key: "lastTriggeredRunId" },
|
|
228
291
|
]));
|
|
229
292
|
}
|
|
230
|
-
if (databaseEntries.length > 0) {
|
|
231
|
-
printResult(" Database changes", "a committed write to one of these types runs this function");
|
|
232
|
-
// A fire carries the rows of every change of the type (the intent's
|
|
233
|
-
// 2026-09-09 decision), so the row says which type and no more —
|
|
234
|
-
// the READ GRANTS column retired with the grant grammar (#3279).
|
|
235
|
-
console.log(formatTable(databaseEntries, [
|
|
236
|
-
{ header: "DATABASE TYPE", key: "databaseType" },
|
|
237
|
-
]));
|
|
238
|
-
}
|
|
239
293
|
// #3187 / #3279 — the ACTIVE version's manifest: what it registered
|
|
240
294
|
// and what it TOUCHES, from the push-time collection. Documentation,
|
|
241
295
|
// never authorization — function code acts as the system, and this
|
|
@@ -298,11 +352,129 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
298
352
|
process.exit(1);
|
|
299
353
|
}
|
|
300
354
|
});
|
|
355
|
+
// ── #3462: a function's versions, and pointing it back at one ─────────
|
|
356
|
+
//
|
|
357
|
+
// Every changing push mints an immutable version and repoints the function at
|
|
358
|
+
// it. That was already true and already stored — and entirely invisible: an
|
|
359
|
+
// operator could not list a function's versions, and a bad push was undone by
|
|
360
|
+
// editing the tree and pushing again, with `disable` as the only immediate
|
|
361
|
+
// stop.
|
|
362
|
+
//
|
|
363
|
+
// Both are OPERATIONAL verbs on a configuration group in `docs/cli-design.md`'s
|
|
364
|
+
// sense: they author nothing. A function's versions are nameless, immutable
|
|
365
|
+
// and not in TOML — unlike a prompt's `[[configs]]` blocks, whose `active`
|
|
366
|
+
// marker is why `prompts configs activate` was retired — so there is no TOML
|
|
367
|
+
// spelling of activation to prefer to a verb.
|
|
368
|
+
functions
|
|
369
|
+
.command("configs")
|
|
370
|
+
.description("List a server function's versions, newest first")
|
|
371
|
+
.argument("<function-id>", "Function ID")
|
|
372
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
373
|
+
.option("--limit <n>", "Rows per page (default 25, max 100)")
|
|
374
|
+
.option("--cursor <cursor>", "Continue from a previous page")
|
|
375
|
+
.option("--json", "Output as JSON")
|
|
376
|
+
.action(async (functionId, options) => {
|
|
377
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
378
|
+
const client = new ApiClient();
|
|
379
|
+
try {
|
|
380
|
+
// Always a `limit`, so this pages. The server's no-parameter form
|
|
381
|
+
// drains every version and exists for a CLI installed before paging
|
|
382
|
+
// did — this one is not it.
|
|
383
|
+
const { items, nextCursor } = await client.listFunctionConfigs(resolvedAppId, functionId, {
|
|
384
|
+
limit: options.limit ? Number(options.limit) : 25,
|
|
385
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
386
|
+
});
|
|
387
|
+
if (options.json) {
|
|
388
|
+
// Full hashes, not the table's twelve characters: a consumer
|
|
389
|
+
// comparing versions needs the value the server stored.
|
|
390
|
+
json({ items, hasMore: !!nextCursor, ...(nextCursor ? { nextCursor } : {}) });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!items || items.length === 0) {
|
|
394
|
+
info("No versions pushed yet.");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
console.log(formatTable(functionVersionRows(items), [
|
|
398
|
+
{ header: "CONFIG ID", key: "configId" },
|
|
399
|
+
{ header: "PUSHED", key: "createdAt", format: formatDate },
|
|
400
|
+
{ header: "ACTIVE", key: "active" },
|
|
401
|
+
{ header: "CONTENT", key: "content" },
|
|
402
|
+
{ header: "ENVELOPE", key: "envelope" },
|
|
403
|
+
{ header: "TRIGGERS", key: "triggers" },
|
|
404
|
+
{ header: "BY", key: "by" },
|
|
405
|
+
]));
|
|
406
|
+
if (nextCursor) {
|
|
407
|
+
info(`More versions: --cursor ${nextCursor}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
error(err.message);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
functions
|
|
416
|
+
.command("activate")
|
|
417
|
+
.description("Point a server function at one of its own existing versions (roll back or forward)")
|
|
418
|
+
.argument("<function-id>", "Function ID")
|
|
419
|
+
.argument("<config-id>", "The version to activate (`functions configs`)")
|
|
420
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
421
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
422
|
+
.option("--json", "Output as JSON")
|
|
423
|
+
.addHelpText("after", `
|
|
424
|
+
What activating a version does, and does not:
|
|
425
|
+
Request calls and trigger fires use it from the NEXT call, under that
|
|
426
|
+
version's own capabilities, limits and triggers.
|
|
427
|
+
A task run already in flight finishes on the version it started on — the
|
|
428
|
+
content hash pins its code.
|
|
429
|
+
No version is created. \`config push\` of the tree is still what makes the
|
|
430
|
+
tree's code the newest version.
|
|
431
|
+
`)
|
|
432
|
+
.action(async (functionId, configId, options) => {
|
|
433
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
434
|
+
if (!options.yes) {
|
|
435
|
+
let confirmed;
|
|
436
|
+
try {
|
|
437
|
+
confirmed = await confirmPrompt(activationPrompt(String(functionId), String(configId)));
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
error(err.message);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
if (!confirmed) {
|
|
444
|
+
info("Cancelled.");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const client = new ApiClient();
|
|
449
|
+
try {
|
|
450
|
+
const answer = await client.activateFunctionConfig(resolvedAppId, functionId, configId);
|
|
451
|
+
if (options.json) {
|
|
452
|
+
json(answer);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const rendered = activationOutput(answer, formatDate);
|
|
456
|
+
if (!rendered.changed) {
|
|
457
|
+
info(rendered.headline);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
success(rendered.headline);
|
|
461
|
+
for (const [label, value] of rendered.lines)
|
|
462
|
+
printResult(label, value);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
// The server's own sentence and code: an archived function, an archived
|
|
466
|
+
// version, a held delete lease and a pointer another writer moved each
|
|
467
|
+
// owe the operator a different next move, and only the code says which
|
|
468
|
+
// one without reading English.
|
|
469
|
+
error(activationRefusalLine(err));
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
301
473
|
// #3181 — a trigger-fired invocation writes a run row, and a function run has
|
|
302
474
|
// no `workflowId`, so `primitive workflows runs` cannot see it. This verb is
|
|
303
475
|
// the only way to inspect what a webhook or a schedule actually did
|
|
304
476
|
// (principle 8).
|
|
305
|
-
functions
|
|
477
|
+
const runs = functions
|
|
306
478
|
.command("runs")
|
|
307
479
|
.description("List a server function's runs, newest first")
|
|
308
480
|
.argument("<function-id>", "Function ID")
|
|
@@ -318,8 +490,17 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
318
490
|
...(options.limit ? { limit: Number(options.limit) } : {}),
|
|
319
491
|
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
320
492
|
});
|
|
493
|
+
// #3482, C-6 — the runtime this run used, derived BEFORE the output is
|
|
494
|
+
// chosen so the table and `--json` say the same thing about the same
|
|
495
|
+
// row. `--json` takes the word or `null`, never the table's blank: a
|
|
496
|
+
// reader parsing this needs "no runtime" to be a value rather than an
|
|
497
|
+
// empty string that sorts beside real ones.
|
|
498
|
+
const withRuntime = (items ?? []).map((run) => ({
|
|
499
|
+
...run,
|
|
500
|
+
runtime: runtimeForExecutionMode(run?.executionMode),
|
|
501
|
+
}));
|
|
321
502
|
if (options.json) {
|
|
322
|
-
json({ items, nextCursor });
|
|
503
|
+
json({ items: withRuntime, nextCursor });
|
|
323
504
|
return;
|
|
324
505
|
}
|
|
325
506
|
if (!items || items.length === 0) {
|
|
@@ -327,18 +508,29 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
327
508
|
return;
|
|
328
509
|
}
|
|
329
510
|
console.log(formatTable(
|
|
330
|
-
// #3381 — flatten the slice's
|
|
331
|
-
//
|
|
332
|
-
//
|
|
511
|
+
// #3381, #3566 — flatten the slice record's two counts onto the
|
|
512
|
+
// row for the REFRESHES and RESETS columns. One helper decides
|
|
513
|
+
// both, so a row with no record gets a blank cell in each and a
|
|
514
|
+
// record with no count gets `0`.
|
|
333
515
|
items.map((run) => ({
|
|
334
516
|
...run,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
517
|
+
// #3482 — which RUNTIME this run used, from the row's own
|
|
518
|
+
// `executionMode`. One pure mapping, so the word an operator
|
|
519
|
+
// reads here is the word `ctx.runtime` gave the code.
|
|
520
|
+
runtime: runRuntimeCell(run),
|
|
521
|
+
...runSliceCells(run),
|
|
338
522
|
})), [
|
|
339
523
|
{ header: "RUN ID", key: "runId" },
|
|
340
524
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
341
525
|
{ header: "FIRED BY", key: "initiatorKind" },
|
|
526
|
+
// #3482 — the runtime this run used: `request` for a webhook
|
|
527
|
+
// delivery or a `workflow.call` leaf, `task` for everything the
|
|
528
|
+
// engine ran. Blank for a DSL row, which is not a function run.
|
|
529
|
+
{ header: "RUNTIME", key: "runtime" },
|
|
530
|
+
// #3462 — WHICH version this run executed. The row has carried
|
|
531
|
+
// `configId` since #3181; nothing showed it, so an operator
|
|
532
|
+
// reading a failed run could not tell which code failed.
|
|
533
|
+
{ header: "VERSION", key: "configId" },
|
|
342
534
|
// #3280 — FIRED BY already said `function`; this is the half that
|
|
343
535
|
// makes it actionable, so a failed page leads back to the
|
|
344
536
|
// orchestrator that started it. Blank for a root.
|
|
@@ -346,6 +538,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
346
538
|
// #3381 — how many times a task slice's token was refreshed
|
|
347
539
|
// through the gateway. Blank for a request run.
|
|
348
540
|
{ header: "REFRESHES", key: "refreshes" },
|
|
541
|
+
// #3566 — how many of this run's slices a platform deploy (or an
|
|
542
|
+
// isolate eviction) tore down. The engine re-runs the interrupted
|
|
543
|
+
// step, so a run that reset and then COMPLETED carries no failure
|
|
544
|
+
// and no error code: this cell is the only place it shows.
|
|
545
|
+
{ header: "RESETS", key: "resets" },
|
|
349
546
|
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
350
547
|
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
351
548
|
{ header: "CODE", key: "errorCode" },
|
|
@@ -359,6 +556,109 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
359
556
|
process.exit(1);
|
|
360
557
|
}
|
|
361
558
|
});
|
|
559
|
+
// #3348 — the step-level view of a durable run.
|
|
560
|
+
//
|
|
561
|
+
// A durable function's handler calls named steps and the engine memoizes each
|
|
562
|
+
// completed one to replay it, but nothing exposed them: `workflows runs
|
|
563
|
+
// steps|status|step-detail` resolve their first argument to a workflow
|
|
564
|
+
// DEFINITION, which a function id and a function key alike miss, and
|
|
565
|
+
// `functions runs` above reports only the run row — status, start, end, one
|
|
566
|
+
// error string. For a run that did partial work there was no way to see which
|
|
567
|
+
// steps completed, how long each took, or where the time went.
|
|
568
|
+
//
|
|
569
|
+
// A SUBCOMMAND of `runs`, which keeps its own `<function-id>` argument form.
|
|
570
|
+
// Commander dispatches to a subcommand only when the first operand names one,
|
|
571
|
+
// and a function id is a ULID, so `functions runs <function-id>` is untouched.
|
|
572
|
+
runs
|
|
573
|
+
.command("steps")
|
|
574
|
+
.description("Show step-level details for a function run")
|
|
575
|
+
.argument("<function-id>", "Function ID")
|
|
576
|
+
.argument("<run-id>", "Run ID")
|
|
577
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
578
|
+
.option("--json", "Output as JSON")
|
|
579
|
+
.action(async (functionId, runId, _options, command) => {
|
|
580
|
+
// `optsWithGlobals`, not the action's own `options`. `runs` is a command
|
|
581
|
+
// with arguments AND subcommands, so commander parses ITS options out of
|
|
582
|
+
// the argv before it dispatches here — and `runs` declares `--app` and
|
|
583
|
+
// `--json` too, so both land on the parent and the subcommand's copies
|
|
584
|
+
// read empty. The merged view is what the flags an operator typed
|
|
585
|
+
// actually are.
|
|
586
|
+
const options = command.optsWithGlobals();
|
|
587
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
588
|
+
const client = new ApiClient();
|
|
589
|
+
try {
|
|
590
|
+
const { items } = await client.getFunctionStepRuns(resolvedAppId, functionId, runId);
|
|
591
|
+
if (options.json) {
|
|
592
|
+
// The same envelope `workflows runs steps --json` emits: one run's
|
|
593
|
+
// full trace under `items`, never a bare array and never a cursor.
|
|
594
|
+
json(buildWorkflowStepEnvelope(items));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (!items || items.length === 0) {
|
|
598
|
+
info("No step runs found.");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
console.log(renderStepRunTable(items));
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
error(err.message);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
// #3348 — end a run that will not settle.
|
|
609
|
+
//
|
|
610
|
+
// `client.functions.terminate` has existed in the SDK since #3186 and nothing
|
|
611
|
+
// reached it from an operator's side, so a run left `running` after its
|
|
612
|
+
// context document was deleted stayed `running` with nothing to do about it.
|
|
613
|
+
runs
|
|
614
|
+
.command("terminate")
|
|
615
|
+
.description("End a function run that will not settle")
|
|
616
|
+
.argument("<function-id>", "Function ID")
|
|
617
|
+
.argument("<run-id>", "Run ID")
|
|
618
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
619
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
620
|
+
.option("--json", "Output as JSON")
|
|
621
|
+
.action(async (functionId, runId, _options, command) => {
|
|
622
|
+
// See `runs steps` above: `runs` parses its own `--app`/`--json` out of
|
|
623
|
+
// the argv before dispatching here, so the merged view is the real one.
|
|
624
|
+
const options = command.optsWithGlobals();
|
|
625
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
626
|
+
if (!options.yes) {
|
|
627
|
+
let confirm;
|
|
628
|
+
try {
|
|
629
|
+
confirm = await confirmPrompt(`Terminate run ${runId}? Its work stops where it is.`);
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
error(err.message);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
if (!confirm) {
|
|
636
|
+
info("Cancelled.");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const client = new ApiClient();
|
|
641
|
+
try {
|
|
642
|
+
const result = await client.terminateFunctionRun(resolvedAppId, functionId, runId);
|
|
643
|
+
if (options.json) {
|
|
644
|
+
json(result);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (!result.terminated) {
|
|
648
|
+
// The run finished on its own. Reporting a kill would be a lie, and
|
|
649
|
+
// the status it settled at is the answer the operator wanted.
|
|
650
|
+
info(`Run ${result.runId} had already settled (${result.status}); nothing to terminate.`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
success(`Run ${result.runId} terminated.`);
|
|
654
|
+
printResult("Status", formatStatus(result.status));
|
|
655
|
+
printResult("Ended", formatDate(result.endedAt));
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
error(err.message);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
362
662
|
// #3287 — the invocation logs. `runs` above answers what HAPPENED; this
|
|
363
663
|
// answers what the function PRINTED and what it threw, which is the thing
|
|
364
664
|
// an operator reaches for when a function is failing and the response says
|
|
@@ -372,6 +672,8 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
372
672
|
.option("--cursor <cursor>", "Continue from a previous page")
|
|
373
673
|
.option("--follow", "Append new invocations as they are recorded (tail)")
|
|
374
674
|
.option("--interval <seconds>", "Poll interval for --follow (default 2)")
|
|
675
|
+
.option("--run <run-id>", "Only this run's records (#3448)")
|
|
676
|
+
.option("--invocation <invocation-id>", "One record, by the id an invoke answered with (#3448)")
|
|
375
677
|
.option("--json", "Output as JSON")
|
|
376
678
|
.action(async (functionId, options) => {
|
|
377
679
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
@@ -383,7 +685,47 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
383
685
|
error("--follow and --cursor cannot be combined; --follow tails from now.");
|
|
384
686
|
process.exit(1);
|
|
385
687
|
}
|
|
688
|
+
// #3448 — `--invocation` names ONE record, so every flag that shapes a
|
|
689
|
+
// listing contradicts it, and `--run` is a filter a tail cannot honour.
|
|
690
|
+
const conflict = logsFlagConflict(options);
|
|
691
|
+
if (conflict) {
|
|
692
|
+
error(conflict);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
386
695
|
try {
|
|
696
|
+
if (options.invocation) {
|
|
697
|
+
// The point read. A record that never existed, one of ANOTHER
|
|
698
|
+
// function, and one the seven-day TTL has expired all answer the
|
|
699
|
+
// same 404 — which is the listing's own filter, said out loud.
|
|
700
|
+
let record;
|
|
701
|
+
try {
|
|
702
|
+
record = await client.getFunctionLog(resolvedAppId, functionId, String(options.invocation));
|
|
703
|
+
}
|
|
704
|
+
catch (err) {
|
|
705
|
+
// An id nothing wrote, one of ANOTHER function, and one the
|
|
706
|
+
// seven-day TTL has expired all answer the same 404 — so the
|
|
707
|
+
// message names the retention rather than implying the platform
|
|
708
|
+
// lost something (edge 37).
|
|
709
|
+
if (err?.statusCode === 404) {
|
|
710
|
+
error(`No invocation ${options.invocation} for this function. ` +
|
|
711
|
+
`Invocation records are kept for seven days.`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
throw err;
|
|
715
|
+
}
|
|
716
|
+
if (options.json) {
|
|
717
|
+
json(toFunctionLogInspectionRow(record));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
console.log(formatTable([functionLogCells(record)], FUNCTION_LOG_COLUMNS.map((header) => ({
|
|
721
|
+
header,
|
|
722
|
+
key: LOG_COLUMN_KEYS[header],
|
|
723
|
+
...(LOG_COLUMN_FORMATS[header]
|
|
724
|
+
? { format: LOG_COLUMN_FORMATS[header] }
|
|
725
|
+
: {}),
|
|
726
|
+
}))));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
387
729
|
if (options.follow) {
|
|
388
730
|
const intervalSeconds = options.interval ? Number(options.interval) : 2;
|
|
389
731
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
@@ -424,7 +766,7 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
424
766
|
return;
|
|
425
767
|
}
|
|
426
768
|
for (const row of items)
|
|
427
|
-
console.log(
|
|
769
|
+
console.log(functionLogLine(row));
|
|
428
770
|
},
|
|
429
771
|
});
|
|
430
772
|
}
|
|
@@ -433,10 +775,34 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
433
775
|
}
|
|
434
776
|
return;
|
|
435
777
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
778
|
+
// #3448 D3448-SO-006, the CLI half. A FILTERED page holding only
|
|
779
|
+
// other runs' records comes back EMPTY WITH A CURSOR, which is an
|
|
780
|
+
// intermediate page and not the end: returning on it — as this did —
|
|
781
|
+
// reports "no logs" for a run whose records are one page further down.
|
|
782
|
+
// The continuation is followed a bounded number of times, and if it is
|
|
783
|
+
// still holding nothing the empty state is printed WITH the cursor
|
|
784
|
+
// rather than as a bare "none".
|
|
785
|
+
let cursor = options.cursor;
|
|
786
|
+
let items = [];
|
|
787
|
+
let nextCursor;
|
|
788
|
+
let exhaustedCursor = null;
|
|
789
|
+
for (let followed = 0;; followed += 1) {
|
|
790
|
+
const page = await client.listFunctionLogs(resolvedAppId, functionId, {
|
|
791
|
+
...(limit ? { limit } : {}),
|
|
792
|
+
...(cursor ? { cursor } : {}),
|
|
793
|
+
...(options.run ? { runId: String(options.run) } : {}),
|
|
794
|
+
});
|
|
795
|
+
items = page.items;
|
|
796
|
+
nextCursor = page.nextCursor;
|
|
797
|
+
const decision = decidePageContinuation(page, followed);
|
|
798
|
+
if (decision.action === "continue") {
|
|
799
|
+
cursor = decision.cursor;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (decision.action === "exhausted")
|
|
803
|
+
exhaustedCursor = decision.cursor;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
440
806
|
if (options.json) {
|
|
441
807
|
// The shared envelope, whose items are the shared inspection shape:
|
|
442
808
|
// a consumer classifies and pivots on `outcome`, `nativeStatus` and
|
|
@@ -449,26 +815,49 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
449
815
|
return;
|
|
450
816
|
}
|
|
451
817
|
if (!items || items.length === 0) {
|
|
452
|
-
info(
|
|
818
|
+
info(options.run
|
|
819
|
+
? `No invocation logs found for run ${options.run}.`
|
|
820
|
+
: "No invocation logs found for this function.");
|
|
821
|
+
// Not the end: the scan's budget was spent with matching records
|
|
822
|
+
// possibly still below. Printing the cursor is what lets an operator
|
|
823
|
+
// continue rather than concluding there is nothing there.
|
|
824
|
+
if (exhaustedCursor)
|
|
825
|
+
info(`More logs: --cursor ${exhaustedCursor}`);
|
|
453
826
|
return;
|
|
454
827
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
828
|
+
if (options.run) {
|
|
829
|
+
// #3453 — reading ONE run, which is a different question from
|
|
830
|
+
// listing a function's invocations. A table of one row per record
|
|
831
|
+
// answers "which invocations were there"; what an operator opened
|
|
832
|
+
// `--run` for is what the run PRINTED, and until now that was only
|
|
833
|
+
// reachable through `--json`. So: the step trace first, then each
|
|
834
|
+
// record oldest first with its own lines under it.
|
|
835
|
+
let trace = null;
|
|
836
|
+
let traceError = null;
|
|
837
|
+
try {
|
|
838
|
+
const read = await client.getFunctionStepRuns(resolvedAppId, functionId, String(options.run));
|
|
839
|
+
trace = read?.items ?? [];
|
|
840
|
+
}
|
|
841
|
+
catch (traceErr) {
|
|
842
|
+
traceError = traceErr?.message ?? String(traceErr);
|
|
843
|
+
}
|
|
844
|
+
const rendered = renderRunLogs({ records: items, trace, traceError });
|
|
845
|
+
if (rendered.warning)
|
|
846
|
+
warn(rendered.warning);
|
|
847
|
+
for (const line of rendered.lines)
|
|
848
|
+
console.log(line);
|
|
849
|
+
if (nextCursor) {
|
|
850
|
+
info(`More logs: --cursor ${nextCursor}`);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
console.log(formatTable(items.map((row) => functionLogCells(row)), FUNCTION_LOG_COLUMNS.map((header) => ({
|
|
855
|
+
header,
|
|
856
|
+
key: LOG_COLUMN_KEYS[header],
|
|
857
|
+
...(LOG_COLUMN_FORMATS[header]
|
|
858
|
+
? { format: LOG_COLUMN_FORMATS[header] }
|
|
859
|
+
: {}),
|
|
860
|
+
}))));
|
|
472
861
|
if (nextCursor) {
|
|
473
862
|
info(`More logs: --cursor ${nextCursor}`);
|
|
474
863
|
}
|
|
@@ -761,5 +1150,429 @@ Reclaiming the key means hard-deleting the row:
|
|
|
761
1150
|
process.exit(1);
|
|
762
1151
|
}
|
|
763
1152
|
});
|
|
1153
|
+
// ── #3448: running a function ───────────────────────────────────────
|
|
1154
|
+
//
|
|
1155
|
+
// `functions list|get|runs|logs` could INSPECT a function and nothing could
|
|
1156
|
+
// run one: invoking a request function, starting a task and waiting for a
|
|
1157
|
+
// run all needed a signed-in app client or a test harness, and stopping an
|
|
1158
|
+
// experimental run needed SDK code (Compound developer feedback SF3).
|
|
1159
|
+
//
|
|
1160
|
+
// Two identities were missing with it. Every HTTP invocation ran as a
|
|
1161
|
+
// caller, so a function fired by cron, a webhook or a database change —
|
|
1162
|
+
// `ctx.user` null, the system principal — could not be exercised from
|
|
1163
|
+
// outside without faking its trigger.
|
|
1164
|
+
//
|
|
1165
|
+
// Every decision these verbs make is in `cli/src/lib/function-run.ts`, where
|
|
1166
|
+
// it is pinned without a server; what is here is the I/O and the rendering.
|
|
1167
|
+
/**
|
|
1168
|
+
* Key → the function's id.
|
|
1169
|
+
*
|
|
1170
|
+
* `invoke` and `start` take the KEY, because that is the public route's
|
|
1171
|
+
* argument and what a developer wrote in their TOML — while `runs`, `logs`
|
|
1172
|
+
* and `steps` keep the function id they have always taken. So the key has to
|
|
1173
|
+
* be resolved, and the drained admin listing is what `functions list` reads.
|
|
1174
|
+
*
|
|
1175
|
+
* #3482 — the ACTIVE VERSION is no longer read. It was point-read for one
|
|
1176
|
+
* reason: the mode pre-check, which refused the wrong verb before a round
|
|
1177
|
+
* trip. There is no wrong verb any more — the caller picks the runtime and
|
|
1178
|
+
* every function takes both — so the read and the pre-check go together.
|
|
1179
|
+
*/
|
|
1180
|
+
async function resolveFunctionByKey(client, appId, functionKey) {
|
|
1181
|
+
const { items } = await client.listFunctions(appId);
|
|
1182
|
+
const wanted = functionKey.trim().toLowerCase();
|
|
1183
|
+
const match = (items || []).find((item) => String(item.functionKey ?? "").toLowerCase() === wanted);
|
|
1184
|
+
if (!match) {
|
|
1185
|
+
throw new Error(`No function with key '${functionKey}' in this app. ` +
|
|
1186
|
+
`List them with 'primitive functions list'.`);
|
|
1187
|
+
}
|
|
1188
|
+
return { functionId: String(match.functionId) };
|
|
1189
|
+
}
|
|
1190
|
+
/** The identity a call runs under, and the bearer it presents. */
|
|
1191
|
+
async function planIdentity(client, appId, identity) {
|
|
1192
|
+
if (identity.kind === "system") {
|
|
1193
|
+
const me = await client.getAppProfile(appId);
|
|
1194
|
+
return {
|
|
1195
|
+
describe: describeIdentity({ kind: "system", byUserId: me.userId }),
|
|
1196
|
+
actingUserId: null,
|
|
1197
|
+
asSystem: true,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
if (identity.kind === "user") {
|
|
1201
|
+
return {
|
|
1202
|
+
describe: describeIdentity({ kind: "user", userId: identity.userId }),
|
|
1203
|
+
actingUserId: identity.userId,
|
|
1204
|
+
asSystem: false,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
const me = await client.getAppProfile(appId);
|
|
1208
|
+
return {
|
|
1209
|
+
describe: describeIdentity({
|
|
1210
|
+
kind: "self",
|
|
1211
|
+
userId: me.userId,
|
|
1212
|
+
appRole: me.appRole,
|
|
1213
|
+
}),
|
|
1214
|
+
actingUserId: me.userId,
|
|
1215
|
+
asSystem: false,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
/** Print an envelope's ids, each followed by the command that takes it. */
|
|
1219
|
+
function printIdLines(lines) {
|
|
1220
|
+
for (const line of renderIdLines(lines))
|
|
1221
|
+
console.log(line);
|
|
1222
|
+
}
|
|
1223
|
+
functions
|
|
1224
|
+
.command("invoke")
|
|
1225
|
+
.allowExcessArguments(false)
|
|
1226
|
+
.description("Run a REQUEST function over the public route and print its result — the same call `client.functions.invoke` makes")
|
|
1227
|
+
.argument("<key>", "Function key (as written in functions/<key>.toml)")
|
|
1228
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1229
|
+
.option("--input <json>", "The handler's input, as JSON (default {})")
|
|
1230
|
+
.option("--context-doc-id <id>", "Context document for the invocation")
|
|
1231
|
+
.option("--timeout <seconds>", "Wall-clock budget; the platform clamps at 30s")
|
|
1232
|
+
.option("--user <user-id>", "Run as this app user (admin/owner only)")
|
|
1233
|
+
.option("--as <principal>", "Run with no caller: --as system (admin/owner only)")
|
|
1234
|
+
.option("--json", "Output as JSON")
|
|
1235
|
+
.action(async (functionKey, options) => {
|
|
1236
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1237
|
+
const flags = resolveRunFlags(options);
|
|
1238
|
+
if (flags.ok === false) {
|
|
1239
|
+
error(flags.error);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
const client = new ApiClient();
|
|
1243
|
+
try {
|
|
1244
|
+
const target = await resolveFunctionByKey(client, resolvedAppId, functionKey);
|
|
1245
|
+
const identity = await planIdentity(client, resolvedAppId, flags.identity);
|
|
1246
|
+
const body = {
|
|
1247
|
+
rootInput: flags.input,
|
|
1248
|
+
...(options.contextDocId ? { contextDocId: options.contextDocId } : {}),
|
|
1249
|
+
...(flags.timeoutSeconds
|
|
1250
|
+
? { timeoutMs: Math.round(flags.timeoutSeconds * 1000) }
|
|
1251
|
+
: {}),
|
|
1252
|
+
};
|
|
1253
|
+
const answer = await runAs(client, resolvedAppId, flags.identity, "invoke", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
|
|
1254
|
+
...(bearer ? { bearer } : {}),
|
|
1255
|
+
asSystem: identity.asSystem,
|
|
1256
|
+
// #3482 — the ROUTE says which runtime this call means. The
|
|
1257
|
+
// admin-only system door is one route for both, so the body still
|
|
1258
|
+
// names it there and only there.
|
|
1259
|
+
runtime: "request",
|
|
1260
|
+
...(identity.asSystem ? {} : {}),
|
|
1261
|
+
}));
|
|
1262
|
+
const envelope = answer.body ?? {};
|
|
1263
|
+
if (options.json) {
|
|
1264
|
+
json({
|
|
1265
|
+
functionId: target.functionId,
|
|
1266
|
+
functionKey,
|
|
1267
|
+
identity: identity.describe,
|
|
1268
|
+
httpStatus: answer.httpStatus,
|
|
1269
|
+
...(answer.retryAfterSeconds !== null
|
|
1270
|
+
? { retryAfterSeconds: answer.retryAfterSeconds }
|
|
1271
|
+
: {}),
|
|
1272
|
+
...envelope,
|
|
1273
|
+
});
|
|
1274
|
+
process.exit(exitCodeForInvokeAnswer(answer));
|
|
1275
|
+
}
|
|
1276
|
+
if (answer.httpStatus >= 400) {
|
|
1277
|
+
renderRefusal(answer);
|
|
1278
|
+
// The ids, each with the command that takes it — the same rendering
|
|
1279
|
+
// a success gets, because a refusal is when an operator most needs
|
|
1280
|
+
// the next command (CR3448-004). A REFUSAL CAN CARRY AN INVOCATION
|
|
1281
|
+
// ID: the two 500s the platform records answer one (edge 35), and
|
|
1282
|
+
// printing the function id bare beside it left the record findable
|
|
1283
|
+
// only by someone who already knew the flag.
|
|
1284
|
+
printIdLines(invokeIdLines({
|
|
1285
|
+
functionId: target.functionId,
|
|
1286
|
+
invocationId: envelope.invocationId,
|
|
1287
|
+
}));
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
printResult("Status", formatStatus(String(envelope.status ?? "")));
|
|
1291
|
+
if (envelope.output !== undefined) {
|
|
1292
|
+
console.log("Output");
|
|
1293
|
+
console.log(JSON.stringify(envelope.output, null, 2));
|
|
1294
|
+
}
|
|
1295
|
+
if (envelope.error)
|
|
1296
|
+
printResult("Error", String(envelope.error));
|
|
1297
|
+
if (envelope.errorCode)
|
|
1298
|
+
printResult("Code", String(envelope.errorCode));
|
|
1299
|
+
if (envelope.limits) {
|
|
1300
|
+
printResult("Limits", `cpuMs ${envelope.limits.cpuMs}, subRequests ${envelope.limits.subRequests}, ratePerMinute ${envelope.limits.ratePerMinute}`);
|
|
1301
|
+
}
|
|
1302
|
+
console.log(identity.describe);
|
|
1303
|
+
printIdLines(invokeIdLines({
|
|
1304
|
+
functionId: target.functionId,
|
|
1305
|
+
invocationId: envelope.invocationId,
|
|
1306
|
+
}));
|
|
1307
|
+
process.exit(exitCodeForInvokeAnswer(answer));
|
|
1308
|
+
}
|
|
1309
|
+
catch (err) {
|
|
1310
|
+
error(err.message);
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
functions
|
|
1315
|
+
.command("start")
|
|
1316
|
+
.allowExcessArguments(false)
|
|
1317
|
+
.description("Start a TASK function and print its run id")
|
|
1318
|
+
.argument("<key>", "Function key (as written in functions/<key>.toml)")
|
|
1319
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1320
|
+
.option("--input <json>", "The handler's input, as JSON (default {})")
|
|
1321
|
+
.option("--context-doc-id <id>", "Context document the run is keyed under")
|
|
1322
|
+
.option("--run-key <key>", "Idempotency key; a repeat replays the existing run")
|
|
1323
|
+
.option("--user <user-id>", "Run as this app user (admin/owner only)")
|
|
1324
|
+
.option("--as <principal>", "Run with no caller: --as system (admin/owner only)")
|
|
1325
|
+
.option("--wait", "Wait for the run to settle, then print its outcome")
|
|
1326
|
+
.option("--timeout <seconds>", "Budget for --wait (default 900)")
|
|
1327
|
+
.option("--json", "Output as JSON")
|
|
1328
|
+
.action(async (functionKey, options) => {
|
|
1329
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1330
|
+
const flags = resolveRunFlags(options);
|
|
1331
|
+
if (flags.ok === false) {
|
|
1332
|
+
error(flags.error);
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
const client = new ApiClient();
|
|
1336
|
+
try {
|
|
1337
|
+
const target = await resolveFunctionByKey(client, resolvedAppId, functionKey);
|
|
1338
|
+
const identity = await planIdentity(client, resolvedAppId, flags.identity);
|
|
1339
|
+
// D3448-014 — the task path defaults the context to the acting user's
|
|
1340
|
+
// `AppUser.rootDocId`, and the app user provisioned for an admin has
|
|
1341
|
+
// none, so the advertised `functions start <key> --wait` would refuse
|
|
1342
|
+
// `CONTEXT_DOC_REQUIRED` on a fresh app. The route that mints one is
|
|
1343
|
+
// idempotent, so asking for it is free on the second call.
|
|
1344
|
+
//
|
|
1345
|
+
// `--as system` keeps the synthetic `fn:<functionId>` context and does
|
|
1346
|
+
// NOT call the route: there is no app user to mint a document for.
|
|
1347
|
+
let contextDocId = options.contextDocId;
|
|
1348
|
+
if (!contextDocId && !identity.asSystem && identity.actingUserId) {
|
|
1349
|
+
const root = await client.ensureUserRootDocument(resolvedAppId, identity.actingUserId);
|
|
1350
|
+
contextDocId = root.rootDocId;
|
|
1351
|
+
}
|
|
1352
|
+
const body = {
|
|
1353
|
+
rootInput: flags.input,
|
|
1354
|
+
// #3482 — `--as system` is the ONE door that is a single route for
|
|
1355
|
+
// both runtimes, so it keeps the deprecated body selector; every
|
|
1356
|
+
// other call says it with the route.
|
|
1357
|
+
...(identity.asSystem ? { mode: "task" } : {}),
|
|
1358
|
+
...(contextDocId ? { contextDocId } : {}),
|
|
1359
|
+
...(options.runKey ? { runKey: options.runKey } : {}),
|
|
1360
|
+
};
|
|
1361
|
+
const answer = await runAs(client, resolvedAppId, flags.identity, "start", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
|
|
1362
|
+
...(bearer ? { bearer } : {}),
|
|
1363
|
+
asSystem: identity.asSystem,
|
|
1364
|
+
runtime: "task",
|
|
1365
|
+
}));
|
|
1366
|
+
const envelope = answer.body ?? {};
|
|
1367
|
+
if (answer.httpStatus >= 400) {
|
|
1368
|
+
if (options.json) {
|
|
1369
|
+
json({
|
|
1370
|
+
functionId: target.functionId,
|
|
1371
|
+
functionKey,
|
|
1372
|
+
identity: identity.describe,
|
|
1373
|
+
httpStatus: answer.httpStatus,
|
|
1374
|
+
...(answer.retryAfterSeconds !== null
|
|
1375
|
+
? { retryAfterSeconds: answer.retryAfterSeconds }
|
|
1376
|
+
: {}),
|
|
1377
|
+
...envelope,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
renderRefusal(answer);
|
|
1382
|
+
}
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
const runId = String(envelope.runId ?? "");
|
|
1386
|
+
if (!options.json) {
|
|
1387
|
+
if (envelope.existing) {
|
|
1388
|
+
info(`Replayed the existing run for this run key (existing).`);
|
|
1389
|
+
}
|
|
1390
|
+
console.log(identity.describe);
|
|
1391
|
+
printIdLines(startIdLines({ functionId: target.functionId, runId }));
|
|
1392
|
+
}
|
|
1393
|
+
if (!options.wait) {
|
|
1394
|
+
if (options.json) {
|
|
1395
|
+
json({
|
|
1396
|
+
functionId: target.functionId,
|
|
1397
|
+
functionKey,
|
|
1398
|
+
identity: identity.describe,
|
|
1399
|
+
httpStatus: answer.httpStatus,
|
|
1400
|
+
...envelope,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
process.exit(0);
|
|
1404
|
+
}
|
|
1405
|
+
// The run id is printed BEFORE the wait begins, so an operator who
|
|
1406
|
+
// interrupts still holds the thing they need to resume.
|
|
1407
|
+
const outcome = await waitForFunctionRun({
|
|
1408
|
+
client,
|
|
1409
|
+
appId: resolvedAppId,
|
|
1410
|
+
functionId: target.functionId,
|
|
1411
|
+
runId,
|
|
1412
|
+
timeoutSeconds: flags.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_SECONDS,
|
|
1413
|
+
json: !!options.json,
|
|
1414
|
+
});
|
|
1415
|
+
process.exit(outcome);
|
|
1416
|
+
}
|
|
1417
|
+
catch (err) {
|
|
1418
|
+
error(err.message);
|
|
1419
|
+
process.exit(1);
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
runs
|
|
1423
|
+
.command("wait")
|
|
1424
|
+
.allowExcessArguments(false)
|
|
1425
|
+
.description("Poll a function run until it settles")
|
|
1426
|
+
.argument("<function-id>", "Function ID")
|
|
1427
|
+
.argument("<run-id>", "Run ID")
|
|
1428
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1429
|
+
.option("--timeout <seconds>", "Give up after this many seconds (default 900)")
|
|
1430
|
+
.option("--json", "Output as JSON")
|
|
1431
|
+
.action(async (functionId, runId, _options, command) => {
|
|
1432
|
+
// `optsWithGlobals`, for the reason `runs steps` records: `runs` declares
|
|
1433
|
+
// `--app` and `--json` of its own, so both land on the parent.
|
|
1434
|
+
const options = command.optsWithGlobals();
|
|
1435
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1436
|
+
const flags = resolveRunFlags({ timeout: options.timeout });
|
|
1437
|
+
if (flags.ok === false) {
|
|
1438
|
+
error(flags.error);
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
const client = new ApiClient();
|
|
1442
|
+
try {
|
|
1443
|
+
const outcome = await waitForFunctionRun({
|
|
1444
|
+
client,
|
|
1445
|
+
appId: resolvedAppId,
|
|
1446
|
+
functionId,
|
|
1447
|
+
runId,
|
|
1448
|
+
timeoutSeconds: flags.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_SECONDS,
|
|
1449
|
+
json: !!options.json,
|
|
1450
|
+
});
|
|
1451
|
+
process.exit(outcome);
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
error(err.message);
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* A refusal, rendered from the envelope the server sent — #3448.
|
|
1461
|
+
*
|
|
1462
|
+
* `ApiClient.invokeFunction` hands the answer back rather than throwing for
|
|
1463
|
+
* exactly this: a 429 carries a `Retry-After` an operator needs and a 409 mode
|
|
1464
|
+
* mismatch carries the version's real mode, and re-deriving either from an
|
|
1465
|
+
* exception message would be re-deriving what the server already said.
|
|
1466
|
+
*/
|
|
1467
|
+
function renderRefusal(answer) {
|
|
1468
|
+
const lines = refusalLines(answer);
|
|
1469
|
+
error(lines.message);
|
|
1470
|
+
for (const field of lines.fields)
|
|
1471
|
+
printResult(field.label, field.value);
|
|
1472
|
+
for (const detail of lines.details)
|
|
1473
|
+
info(` ${detail}`);
|
|
1474
|
+
}
|
|
1475
|
+
/** Exit 0 only for a completed invocation; every refusal and every terminal failure is 1. */
|
|
1476
|
+
function exitCodeForInvokeAnswer(answer) {
|
|
1477
|
+
if (answer.httpStatus >= 400)
|
|
1478
|
+
return 1;
|
|
1479
|
+
return exitCodeForInvokeStatus(answer.body?.status);
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Run a call under the operator's own identity, or under a ten-minute token
|
|
1483
|
+
* minted for `--user` — D3448-009.
|
|
1484
|
+
*
|
|
1485
|
+
* The token is a REAL credential for another user, so it is revoked on every
|
|
1486
|
+
* exit path including Ctrl-C (the wrapper's `finally`), and its value never
|
|
1487
|
+
* reaches stdout, stderr or `--json`: `use` receives it and nothing else does.
|
|
1488
|
+
*/
|
|
1489
|
+
async function runAs(client, appId, identity, verb, functionKey, use) {
|
|
1490
|
+
if (identity.kind !== "user")
|
|
1491
|
+
return use(undefined);
|
|
1492
|
+
return withMintedToken({
|
|
1493
|
+
mint: async () => {
|
|
1494
|
+
const minted = await client.createToken(appId, {
|
|
1495
|
+
name: userTokenName(verb, functionKey),
|
|
1496
|
+
ttl: USER_TOKEN_TTL,
|
|
1497
|
+
userId: identity.userId,
|
|
1498
|
+
});
|
|
1499
|
+
return { token: minted.token, tokenId: minted.tokenId };
|
|
1500
|
+
},
|
|
1501
|
+
revoke: (tokenId) => client.revokeToken(appId, tokenId).then(() => undefined),
|
|
1502
|
+
warn: (message) => warn(message),
|
|
1503
|
+
}, (token) => use(token));
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Poll one run until it settles, the budget is spent, or the operator
|
|
1507
|
+
* interrupts — #3448 behavior 16.
|
|
1508
|
+
*
|
|
1509
|
+
* It polls the ADMIN single-run read, never the runs listing: that reports the
|
|
1510
|
+
* STORED status, and a durable run's row stays `running` until something asks
|
|
1511
|
+
* the engine, so a wait built on it would never settle.
|
|
1512
|
+
*
|
|
1513
|
+
* Ctrl-C exits 130 after printing the resume command, and a second one exits
|
|
1514
|
+
* at once — the failures sweep's shape, and the workflows group's convention.
|
|
1515
|
+
*/
|
|
1516
|
+
async function waitForFunctionRun(args) {
|
|
1517
|
+
const { client, appId, functionId, runId } = args;
|
|
1518
|
+
const deadline = Date.now() + args.timeoutSeconds * 1000;
|
|
1519
|
+
let interrupted = false;
|
|
1520
|
+
let interruptCount = 0;
|
|
1521
|
+
const onSigint = () => {
|
|
1522
|
+
interruptCount += 1;
|
|
1523
|
+
if (interruptCount > 1)
|
|
1524
|
+
process.exit(EXIT_INTERRUPTED);
|
|
1525
|
+
interrupted = true;
|
|
1526
|
+
};
|
|
1527
|
+
process.on("SIGINT", onSigint);
|
|
1528
|
+
try {
|
|
1529
|
+
let attempt = 0;
|
|
1530
|
+
for (;;) {
|
|
1531
|
+
const read = await client.getFunctionRun(appId, functionId, runId);
|
|
1532
|
+
const status = String(read?.status?.status ?? "");
|
|
1533
|
+
const settled = exitCodeForRunStatus(status);
|
|
1534
|
+
if (settled !== null) {
|
|
1535
|
+
if (args.json) {
|
|
1536
|
+
json({ functionId, runId, ...read });
|
|
1537
|
+
return settled;
|
|
1538
|
+
}
|
|
1539
|
+
printResult("Status", formatStatus(status));
|
|
1540
|
+
if (read?.status?.output !== undefined) {
|
|
1541
|
+
console.log("Output");
|
|
1542
|
+
console.log(JSON.stringify(read.status.output, null, 2));
|
|
1543
|
+
}
|
|
1544
|
+
const failure = read?.status?.error;
|
|
1545
|
+
if (failure) {
|
|
1546
|
+
printResult("Error", typeof failure === "string" ? failure : String(failure?.message ?? ""));
|
|
1547
|
+
}
|
|
1548
|
+
if (read?.run?.errorCode)
|
|
1549
|
+
printResult("Code", String(read.run.errorCode));
|
|
1550
|
+
return settled;
|
|
1551
|
+
}
|
|
1552
|
+
if (interrupted) {
|
|
1553
|
+
if (!args.json) {
|
|
1554
|
+
info(`Stopped waiting. The run is still going; resume with:`);
|
|
1555
|
+
info(` ${resumeCommand(functionId, runId)}`);
|
|
1556
|
+
}
|
|
1557
|
+
return EXIT_INTERRUPTED;
|
|
1558
|
+
}
|
|
1559
|
+
if (Date.now() >= deadline) {
|
|
1560
|
+
if (args.json) {
|
|
1561
|
+
json({ functionId, runId, timedOut: true, ...read });
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
warn(`Gave up after ${args.timeoutSeconds}s; the run is still ${status || "in flight"}. Resume with:`);
|
|
1565
|
+
info(` ${resumeCommand(functionId, runId)}`);
|
|
1566
|
+
}
|
|
1567
|
+
return EXIT_TIMED_OUT;
|
|
1568
|
+
}
|
|
1569
|
+
const delay = Math.min(waitDelayMs(attempt), Math.max(0, deadline - Date.now()));
|
|
1570
|
+
attempt += 1;
|
|
1571
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
finally {
|
|
1575
|
+
process.off("SIGINT", onSigint);
|
|
1576
|
+
}
|
|
764
1577
|
}
|
|
765
1578
|
//# sourceMappingURL=functions.js.map
|