primitive-admin 1.1.0-alpha.80 → 1.1.0-alpha.82
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 +5 -5
- package/assets/skill/skills/primitive-platform/SKILL.md +23 -11
- package/dist/bin/primitive.js +14 -15
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/databases.js +3 -5
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +163 -21
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +736 -12
- package/dist/src/commands/functions.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-app-settings.js +2 -2
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.js +29 -27
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +29 -0
- package/dist/src/commands/webhooks.js +118 -54
- package/dist/src/commands/webhooks.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 +119 -3
- package/dist/src/lib/api-client.js +166 -5
- 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/env-resolver-core.d.ts +24 -19
- package/dist/src/lib/env-resolver-core.js +35 -54
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/function-db-types.js +15 -0
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.js +4 -0
- package/dist/src/lib/function-document-types.js.map +1 -1
- 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 +271 -0
- package/dist/src/lib/function-run.js +378 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +28 -0
- package/dist/src/lib/function-schema-codegen.js +41 -10
- package/dist/src/lib/function-schema-codegen.js.map +1 -1
- package/dist/src/lib/function-sync.d.ts +9 -4
- package/dist/src/lib/function-sync.js +29 -7
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +555 -19
- package/dist/src/lib/generated-config-surfaces.js +1826 -80
- 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/local-test-cases.js +2 -2
- package/dist/src/lib/local-test-cases.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-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/banners.d.ts +18 -0
- package/dist/src/lib/swift-codegen/banners.js +19 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +11 -7
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +125 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +423 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +6 -0
- package/dist/src/lib/swift-codegen/generator.js +21 -10
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +15 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +21 -4
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
- package/dist/src/lib/sync-paths.d.ts +19 -14
- package/dist/src/lib/sync-paths.js +26 -24
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- 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
|
@@ -17,19 +17,24 @@
|
|
|
17
17
|
* an endpoint an operator had taken down back in service.
|
|
18
18
|
*/
|
|
19
19
|
import { existsSync } from "fs";
|
|
20
|
-
import { join } from "path";
|
|
20
|
+
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
24
|
import { activeVersionMode } from "../lib/function-sync.js";
|
|
25
|
-
import { FunctionSchemaCodegenError } from "../lib/function-schema-codegen.js";
|
|
25
|
+
import { FunctionSchemaCodegenError, functionSchemaSources, } from "../lib/function-schema-codegen.js";
|
|
26
26
|
import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTypeArtifacts, } from "../lib/function-db-types.js";
|
|
27
|
+
import { generateFunctionSwiftTypes } from "../lib/swift-codegen/functionGenerator.js";
|
|
28
|
+
import { SwiftCodegenError } from "../lib/swift-codegen/schemaToSwift.js";
|
|
27
29
|
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatDate, formatStatus, json, jsonLine, divider, } from "../lib/output.js";
|
|
28
30
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
29
31
|
import { parseStatusFilter } from "../lib/object-status-filter.js";
|
|
32
|
+
import { renderStepRunTable } from "../lib/step-run-table.js";
|
|
33
|
+
import { buildWorkflowStepEnvelope } from "../lib/log-inspection.js";
|
|
30
34
|
import { followLoop } from "../lib/watch.js";
|
|
31
35
|
import { buildLogFollowSource } from "../lib/function-log-tail.js";
|
|
32
36
|
import { toFunctionLogInspectionRow } from "../lib/log-inspection.js";
|
|
37
|
+
import { decidePageContinuation, describeIdentity, exitCodeForInvokeStatus, exitCodeForRunStatus, invokeIdLines, logsFlagConflict, modeFor, precheckMode, refusalLines, renderIdLines, resolveRunFlags, resumeCommand, startIdLines, userTokenName, waitDelayMs, withMintedToken, EXIT_INTERRUPTED, EXIT_TIMED_OUT, USER_TOKEN_TTL, WAIT_DEFAULT_TIMEOUT_SECONDS, } from "../lib/function-run.js";
|
|
33
38
|
/**
|
|
34
39
|
* How far back one `--follow` tick will page to meet its high-water mark.
|
|
35
40
|
*
|
|
@@ -166,9 +171,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
166
171
|
printResult(" Config ID", active.configId);
|
|
167
172
|
printResult(" Entry", active.entry);
|
|
168
173
|
printResult(" Content Hash", active.contentHash || "-");
|
|
169
|
-
// #3281 — the mode words. A server that predates
|
|
170
|
-
// only the `durable` column, which reads the same
|
|
171
|
-
|
|
174
|
+
// #3281 — the mode words, widened by #3454. A server that predates
|
|
175
|
+
// `mode` serializes only the `durable` column, which reads the same
|
|
176
|
+
// way. `any` names no runner, so the line says which verbs it takes.
|
|
177
|
+
const activeMode = activeVersionMode(active);
|
|
178
|
+
printResult(" Mode", activeMode === "any" ? "any (invoke or start)" : activeMode);
|
|
172
179
|
printResult(" Limits", active.limits || "-");
|
|
173
180
|
// #3182 — what this version declared: since #3279 the egress
|
|
174
181
|
// allowlist, the secrets that may enter the sandbox and the
|
|
@@ -218,6 +225,10 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
218
225
|
{ header: "NAME", key: "name" },
|
|
219
226
|
{ header: "ID", key: "triggerId" },
|
|
220
227
|
{ header: "CRON", key: "cron" },
|
|
228
|
+
// #3454 — which runner this schedule's fires use. Always one of
|
|
229
|
+
// the two words: an `any` function's entry names it, and a
|
|
230
|
+
// locked one's takes its lock.
|
|
231
|
+
{ header: "RUNNER", key: "runner" },
|
|
221
232
|
{ header: "TZ", key: "timezone" },
|
|
222
233
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
223
234
|
{ header: "NEXT FIRE", key: "nextFireAt", format: formatDate },
|
|
@@ -300,7 +311,7 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
300
311
|
// no `workflowId`, so `primitive workflows runs` cannot see it. This verb is
|
|
301
312
|
// the only way to inspect what a webhook or a schedule actually did
|
|
302
313
|
// (principle 8).
|
|
303
|
-
functions
|
|
314
|
+
const runs = functions
|
|
304
315
|
.command("runs")
|
|
305
316
|
.description("List a server function's runs, newest first")
|
|
306
317
|
.argument("<function-id>", "Function ID")
|
|
@@ -324,7 +335,16 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
324
335
|
info("No runs found for this function.");
|
|
325
336
|
return;
|
|
326
337
|
}
|
|
327
|
-
console.log(formatTable(
|
|
338
|
+
console.log(formatTable(
|
|
339
|
+
// #3381 — flatten the slice's refresh count onto the row for the
|
|
340
|
+
// REFRESHES column. Blank for a run with no slice record (a run
|
|
341
|
+
// started before this child, or a request invocation).
|
|
342
|
+
items.map((run) => ({
|
|
343
|
+
...run,
|
|
344
|
+
refreshes: run.slice && Number.isFinite(run.slice.refreshCount)
|
|
345
|
+
? String(run.slice.refreshCount)
|
|
346
|
+
: "",
|
|
347
|
+
})), [
|
|
328
348
|
{ header: "RUN ID", key: "runId" },
|
|
329
349
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
330
350
|
{ header: "FIRED BY", key: "initiatorKind" },
|
|
@@ -332,6 +352,9 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
332
352
|
// makes it actionable, so a failed page leads back to the
|
|
333
353
|
// orchestrator that started it. Blank for a root.
|
|
334
354
|
{ header: "PARENT", key: "parentFunctionKey" },
|
|
355
|
+
// #3381 — how many times a task slice's token was refreshed
|
|
356
|
+
// through the gateway. Blank for a request run.
|
|
357
|
+
{ header: "REFRESHES", key: "refreshes" },
|
|
335
358
|
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
336
359
|
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
337
360
|
{ header: "CODE", key: "errorCode" },
|
|
@@ -345,6 +368,109 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
345
368
|
process.exit(1);
|
|
346
369
|
}
|
|
347
370
|
});
|
|
371
|
+
// #3348 — the step-level view of a durable run.
|
|
372
|
+
//
|
|
373
|
+
// A durable function's handler calls named steps and the engine memoizes each
|
|
374
|
+
// completed one to replay it, but nothing exposed them: `workflows runs
|
|
375
|
+
// steps|status|step-detail` resolve their first argument to a workflow
|
|
376
|
+
// DEFINITION, which a function id and a function key alike miss, and
|
|
377
|
+
// `functions runs` above reports only the run row — status, start, end, one
|
|
378
|
+
// error string. For a run that did partial work there was no way to see which
|
|
379
|
+
// steps completed, how long each took, or where the time went.
|
|
380
|
+
//
|
|
381
|
+
// A SUBCOMMAND of `runs`, which keeps its own `<function-id>` argument form.
|
|
382
|
+
// Commander dispatches to a subcommand only when the first operand names one,
|
|
383
|
+
// and a function id is a ULID, so `functions runs <function-id>` is untouched.
|
|
384
|
+
runs
|
|
385
|
+
.command("steps")
|
|
386
|
+
.description("Show step-level details for a function run")
|
|
387
|
+
.argument("<function-id>", "Function ID")
|
|
388
|
+
.argument("<run-id>", "Run ID")
|
|
389
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
390
|
+
.option("--json", "Output as JSON")
|
|
391
|
+
.action(async (functionId, runId, _options, command) => {
|
|
392
|
+
// `optsWithGlobals`, not the action's own `options`. `runs` is a command
|
|
393
|
+
// with arguments AND subcommands, so commander parses ITS options out of
|
|
394
|
+
// the argv before it dispatches here — and `runs` declares `--app` and
|
|
395
|
+
// `--json` too, so both land on the parent and the subcommand's copies
|
|
396
|
+
// read empty. The merged view is what the flags an operator typed
|
|
397
|
+
// actually are.
|
|
398
|
+
const options = command.optsWithGlobals();
|
|
399
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
400
|
+
const client = new ApiClient();
|
|
401
|
+
try {
|
|
402
|
+
const { items } = await client.getFunctionStepRuns(resolvedAppId, functionId, runId);
|
|
403
|
+
if (options.json) {
|
|
404
|
+
// The same envelope `workflows runs steps --json` emits: one run's
|
|
405
|
+
// full trace under `items`, never a bare array and never a cursor.
|
|
406
|
+
json(buildWorkflowStepEnvelope(items));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!items || items.length === 0) {
|
|
410
|
+
info("No step runs found.");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log(renderStepRunTable(items));
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
error(err.message);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// #3348 — end a run that will not settle.
|
|
421
|
+
//
|
|
422
|
+
// `client.functions.terminate` has existed in the SDK since #3186 and nothing
|
|
423
|
+
// reached it from an operator's side, so a run left `running` after its
|
|
424
|
+
// context document was deleted stayed `running` with nothing to do about it.
|
|
425
|
+
runs
|
|
426
|
+
.command("terminate")
|
|
427
|
+
.description("End a function run that will not settle")
|
|
428
|
+
.argument("<function-id>", "Function ID")
|
|
429
|
+
.argument("<run-id>", "Run ID")
|
|
430
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
431
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
432
|
+
.option("--json", "Output as JSON")
|
|
433
|
+
.action(async (functionId, runId, _options, command) => {
|
|
434
|
+
// See `runs steps` above: `runs` parses its own `--app`/`--json` out of
|
|
435
|
+
// the argv before dispatching here, so the merged view is the real one.
|
|
436
|
+
const options = command.optsWithGlobals();
|
|
437
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
438
|
+
if (!options.yes) {
|
|
439
|
+
let confirm;
|
|
440
|
+
try {
|
|
441
|
+
confirm = await confirmPrompt(`Terminate run ${runId}? Its work stops where it is.`);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
error(err.message);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
if (!confirm) {
|
|
448
|
+
info("Cancelled.");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const client = new ApiClient();
|
|
453
|
+
try {
|
|
454
|
+
const result = await client.terminateFunctionRun(resolvedAppId, functionId, runId);
|
|
455
|
+
if (options.json) {
|
|
456
|
+
json(result);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (!result.terminated) {
|
|
460
|
+
// The run finished on its own. Reporting a kill would be a lie, and
|
|
461
|
+
// the status it settled at is the answer the operator wanted.
|
|
462
|
+
info(`Run ${result.runId} had already settled (${result.status}); nothing to terminate.`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
success(`Run ${result.runId} terminated.`);
|
|
466
|
+
printResult("Status", formatStatus(result.status));
|
|
467
|
+
printResult("Ended", formatDate(result.endedAt));
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
error(err.message);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
348
474
|
// #3287 — the invocation logs. `runs` above answers what HAPPENED; this
|
|
349
475
|
// answers what the function PRINTED and what it threw, which is the thing
|
|
350
476
|
// an operator reaches for when a function is failing and the response says
|
|
@@ -358,6 +484,8 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
358
484
|
.option("--cursor <cursor>", "Continue from a previous page")
|
|
359
485
|
.option("--follow", "Append new invocations as they are recorded (tail)")
|
|
360
486
|
.option("--interval <seconds>", "Poll interval for --follow (default 2)")
|
|
487
|
+
.option("--run <run-id>", "Only this run's records (#3448)")
|
|
488
|
+
.option("--invocation <invocation-id>", "One record, by the id an invoke answered with (#3448)")
|
|
361
489
|
.option("--json", "Output as JSON")
|
|
362
490
|
.action(async (functionId, options) => {
|
|
363
491
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
@@ -369,7 +497,57 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
369
497
|
error("--follow and --cursor cannot be combined; --follow tails from now.");
|
|
370
498
|
process.exit(1);
|
|
371
499
|
}
|
|
500
|
+
// #3448 — `--invocation` names ONE record, so every flag that shapes a
|
|
501
|
+
// listing contradicts it, and `--run` is a filter a tail cannot honour.
|
|
502
|
+
const conflict = logsFlagConflict(options);
|
|
503
|
+
if (conflict) {
|
|
504
|
+
error(conflict);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
372
507
|
try {
|
|
508
|
+
if (options.invocation) {
|
|
509
|
+
// The point read. A record that never existed, one of ANOTHER
|
|
510
|
+
// function, and one the seven-day TTL has expired all answer the
|
|
511
|
+
// same 404 — which is the listing's own filter, said out loud.
|
|
512
|
+
let record;
|
|
513
|
+
try {
|
|
514
|
+
record = await client.getFunctionLog(resolvedAppId, functionId, String(options.invocation));
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
// An id nothing wrote, one of ANOTHER function, and one the
|
|
518
|
+
// seven-day TTL has expired all answer the same 404 — so the
|
|
519
|
+
// message names the retention rather than implying the platform
|
|
520
|
+
// lost something (edge 37).
|
|
521
|
+
if (err?.statusCode === 404) {
|
|
522
|
+
error(`No invocation ${options.invocation} for this function. ` +
|
|
523
|
+
`Invocation records are kept for seven days.`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
throw err;
|
|
527
|
+
}
|
|
528
|
+
if (options.json) {
|
|
529
|
+
json(toFunctionLogInspectionRow(record));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.log(formatTable([
|
|
533
|
+
{
|
|
534
|
+
startedAt: record.startedAt,
|
|
535
|
+
status: record.status,
|
|
536
|
+
trigger: record.trigger?.kind ?? "",
|
|
537
|
+
runOrInvocation: record.runId || record.invocationId,
|
|
538
|
+
errorCode: record.errorCode ?? "",
|
|
539
|
+
firstError: firstErrorLine(record),
|
|
540
|
+
},
|
|
541
|
+
], [
|
|
542
|
+
{ header: "TIME", key: "startedAt", format: formatDate },
|
|
543
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
544
|
+
{ header: "TRIGGER", key: "trigger" },
|
|
545
|
+
{ header: "RUN/INVOCATION ID", key: "runOrInvocation" },
|
|
546
|
+
{ header: "CODE", key: "errorCode" },
|
|
547
|
+
{ header: "ERROR", key: "firstError" },
|
|
548
|
+
]));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
373
551
|
if (options.follow) {
|
|
374
552
|
const intervalSeconds = options.interval ? Number(options.interval) : 2;
|
|
375
553
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
@@ -419,10 +597,34 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
419
597
|
}
|
|
420
598
|
return;
|
|
421
599
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
600
|
+
// #3448 D3448-SO-006, the CLI half. A FILTERED page holding only
|
|
601
|
+
// other runs' records comes back EMPTY WITH A CURSOR, which is an
|
|
602
|
+
// intermediate page and not the end: returning on it — as this did —
|
|
603
|
+
// reports "no logs" for a run whose records are one page further down.
|
|
604
|
+
// The continuation is followed a bounded number of times, and if it is
|
|
605
|
+
// still holding nothing the empty state is printed WITH the cursor
|
|
606
|
+
// rather than as a bare "none".
|
|
607
|
+
let cursor = options.cursor;
|
|
608
|
+
let items = [];
|
|
609
|
+
let nextCursor;
|
|
610
|
+
let exhaustedCursor = null;
|
|
611
|
+
for (let followed = 0;; followed += 1) {
|
|
612
|
+
const page = await client.listFunctionLogs(resolvedAppId, functionId, {
|
|
613
|
+
...(limit ? { limit } : {}),
|
|
614
|
+
...(cursor ? { cursor } : {}),
|
|
615
|
+
...(options.run ? { runId: String(options.run) } : {}),
|
|
616
|
+
});
|
|
617
|
+
items = page.items;
|
|
618
|
+
nextCursor = page.nextCursor;
|
|
619
|
+
const decision = decidePageContinuation(page, followed);
|
|
620
|
+
if (decision.action === "continue") {
|
|
621
|
+
cursor = decision.cursor;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (decision.action === "exhausted")
|
|
625
|
+
exhaustedCursor = decision.cursor;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
426
628
|
if (options.json) {
|
|
427
629
|
// The shared envelope, whose items are the shared inspection shape:
|
|
428
630
|
// a consumer classifies and pivots on `outcome`, `nativeStatus` and
|
|
@@ -435,7 +637,14 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
435
637
|
return;
|
|
436
638
|
}
|
|
437
639
|
if (!items || items.length === 0) {
|
|
438
|
-
info(
|
|
640
|
+
info(options.run
|
|
641
|
+
? `No invocation logs found for run ${options.run}.`
|
|
642
|
+
: "No invocation logs found for this function.");
|
|
643
|
+
// Not the end: the scan's budget was spent with matching records
|
|
644
|
+
// possibly still below. Printing the cursor is what lets an operator
|
|
645
|
+
// continue rather than concluding there is nothing there.
|
|
646
|
+
if (exhaustedCursor)
|
|
647
|
+
info(`More logs: --cursor ${exhaustedCursor}`);
|
|
439
648
|
return;
|
|
440
649
|
}
|
|
441
650
|
console.log(formatTable(items.map((row) => ({
|
|
@@ -477,8 +686,17 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
477
686
|
.description("Write the generated files into the config tree's functions/ directory: the database-type declaration, the primitive-functions declaration, the functions declaration (<Key>Input/<Key>Output from each function's schemas), the per-function client invokers under generated/, and the tsconfig that wires the declarations into your editor. `config push` does this too; this is the standalone form, with a --check mode for CI.")
|
|
478
687
|
.option("--check", "Exit non-zero if the generated files are out of date or the tsconfig no longer loads them (CI guard); does not write.")
|
|
479
688
|
.option("-o, --out <dir>", "Directory for the per-function client invokers (default: <tree>/functions/generated/)")
|
|
689
|
+
.option("--lang <lang>", "Target language for the generated per-key invokers: 'ts' (default) or 'swift'. Swift emits one <key>.generated.swift per function (<Key>Input/<Key>Output Codable types + a mode-fixed <Key>Function invoker) and writes no TypeScript artifact.", "ts")
|
|
480
690
|
.option("--json", "Output the result summary as JSON")
|
|
481
691
|
.action(async (options) => {
|
|
692
|
+
// `--lang` is validated FIRST, before a single file is read, so an
|
|
693
|
+
// unknown value fails naming the flag rather than reporting whatever the
|
|
694
|
+
// schema parser happened to meet on the way (#3344 behavior 8).
|
|
695
|
+
const lang = String(options.lang ?? "ts").toLowerCase();
|
|
696
|
+
if (lang !== "ts" && lang !== "swift") {
|
|
697
|
+
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
482
700
|
// A tree command: what it writes is the environment's own directory,
|
|
483
701
|
// which is the only directory it can be asked to write (#3154).
|
|
484
702
|
const configDir = resolveSyncDir();
|
|
@@ -499,6 +717,76 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
499
717
|
// #3281 — the per-key client invokers go beside the declarations unless
|
|
500
718
|
// `-o` names another directory, the `workflows codegen` shape.
|
|
501
719
|
const artifactOptions = options.out ? { invokerDir: String(options.out) } : {};
|
|
720
|
+
// #3344 — the Swift half. It shares the default output directory with
|
|
721
|
+
// the TypeScript invokers (suffixes and ownership banners keep the two
|
|
722
|
+
// sweeps apart) and writes NO TypeScript artifact: a Swift app has no
|
|
723
|
+
// tsconfig to wire, so `problems` is always empty here.
|
|
724
|
+
if (lang === "swift") {
|
|
725
|
+
const outputDir = options.out
|
|
726
|
+
? String(options.out)
|
|
727
|
+
: join(configDir, "functions", "generated");
|
|
728
|
+
// Tree-relative, so the paths a CI job prints read the way the author's
|
|
729
|
+
// repository does.
|
|
730
|
+
const relative = (filePath) => relativePath(configDir, filePath);
|
|
731
|
+
let outcome;
|
|
732
|
+
try {
|
|
733
|
+
outcome = await generateFunctionSwiftTypes({
|
|
734
|
+
inputs: functionSchemaSources(configDir),
|
|
735
|
+
outputDir,
|
|
736
|
+
check: Boolean(options.check),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
// A malformed schema, a duplicate key, an un-typable schema or a
|
|
741
|
+
// collision with another generator's file: nothing is written, and
|
|
742
|
+
// the message names the file the author edits.
|
|
743
|
+
if (!(err instanceof FunctionSchemaCodegenError) &&
|
|
744
|
+
!(err instanceof SwiftCodegenError)) {
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
if (options.json)
|
|
748
|
+
json({ ok: false, stale: [], problems: [err.message] });
|
|
749
|
+
else
|
|
750
|
+
error(err.message);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
if (options.check) {
|
|
754
|
+
const stale = outcome.mismatches.map((m) => relative(m.filePath)).sort();
|
|
755
|
+
if (stale.length === 0) {
|
|
756
|
+
if (options.json)
|
|
757
|
+
json({ ok: true, stale: [], problems: [] });
|
|
758
|
+
else
|
|
759
|
+
success("Check passed: the generated Swift invokers are up to date.");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (options.json) {
|
|
763
|
+
json({ ok: false, stale, problems: [] });
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
error(`Check failed: ${stale.length} file(s) out of date.`);
|
|
767
|
+
for (const mismatch of outcome.mismatches) {
|
|
768
|
+
error(` ${mismatch.reason}: ${relative(mismatch.filePath)}`);
|
|
769
|
+
}
|
|
770
|
+
const regenerate = ["primitive functions codegen --lang swift"];
|
|
771
|
+
if (options.out)
|
|
772
|
+
regenerate.push(`-o ${options.out}`);
|
|
773
|
+
info(`Run \`${regenerate.join(" ")}\` to regenerate.`);
|
|
774
|
+
}
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
const writtenRelative = outcome.writtenFiles.map(relative).sort();
|
|
778
|
+
const deletedRelative = outcome.deletedFiles.map(relative).sort();
|
|
779
|
+
if (options.json) {
|
|
780
|
+
json({ written: writtenRelative, deleted: deletedRelative, problems: [] });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
success(`Wrote ${writtenRelative.length} Swift file(s) into ${outputDir}.`);
|
|
784
|
+
for (const file of writtenRelative)
|
|
785
|
+
keyValue(" wrote", file);
|
|
786
|
+
for (const file of deletedRelative)
|
|
787
|
+
keyValue(" swept", file);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
502
790
|
// Wiring push cannot repair is checked alongside freshness: a tsconfig
|
|
503
791
|
// that carries comments is never rewritten, so it can be perfectly
|
|
504
792
|
// current and still load none of the declarations (CR3182-003).
|
|
@@ -668,5 +956,441 @@ Reclaiming the key means hard-deleting the row:
|
|
|
668
956
|
process.exit(1);
|
|
669
957
|
}
|
|
670
958
|
});
|
|
959
|
+
// ── #3448: running a function ───────────────────────────────────────
|
|
960
|
+
//
|
|
961
|
+
// `functions list|get|runs|logs` could INSPECT a function and nothing could
|
|
962
|
+
// run one: invoking a request function, starting a task and waiting for a
|
|
963
|
+
// run all needed a signed-in app client or a test harness, and stopping an
|
|
964
|
+
// experimental run needed SDK code (Compound developer feedback SF3).
|
|
965
|
+
//
|
|
966
|
+
// Two identities were missing with it. Every HTTP invocation ran as a
|
|
967
|
+
// caller, so a function fired by cron, a webhook or a database change —
|
|
968
|
+
// `ctx.user` null, the system principal — could not be exercised from
|
|
969
|
+
// outside without faking its trigger.
|
|
970
|
+
//
|
|
971
|
+
// Every decision these verbs make is in `cli/src/lib/function-run.ts`, where
|
|
972
|
+
// it is pinned without a server; what is here is the I/O and the rendering.
|
|
973
|
+
/**
|
|
974
|
+
* Key → the function's id and the mode its ACTIVE version runs in.
|
|
975
|
+
*
|
|
976
|
+
* `invoke` and `start` take the KEY, because that is the public route's
|
|
977
|
+
* argument and what a developer wrote in their TOML — while `runs`, `logs`
|
|
978
|
+
* and `steps` keep the function id they have always taken. So the key has to
|
|
979
|
+
* be resolved, and the drained admin listing is what `functions list` reads.
|
|
980
|
+
*/
|
|
981
|
+
async function resolveFunctionByKey(client, appId, functionKey) {
|
|
982
|
+
const { items } = await client.listFunctions(appId);
|
|
983
|
+
const wanted = functionKey.trim().toLowerCase();
|
|
984
|
+
const match = (items || []).find((item) => String(item.functionKey ?? "").toLowerCase() === wanted);
|
|
985
|
+
if (!match) {
|
|
986
|
+
throw new Error(`No function with key '${functionKey}' in this app. ` +
|
|
987
|
+
`List them with 'primitive functions list'.`);
|
|
988
|
+
}
|
|
989
|
+
const functionId = String(match.functionId);
|
|
990
|
+
// A function with no pushed version has no mode to disagree with, and the
|
|
991
|
+
// server's `FUNCTION_NOT_PUSHED` is the honest answer (edge 30).
|
|
992
|
+
let mode = null;
|
|
993
|
+
try {
|
|
994
|
+
const { items: configs } = await client.listFunctionConfigs(appId, functionId);
|
|
995
|
+
const active = (configs || []).find((config) => String(config.configId) === String(match.activeConfigId));
|
|
996
|
+
if (active)
|
|
997
|
+
mode = activeVersionMode(active);
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// A version list this operator cannot read is not a reason to refuse the
|
|
1001
|
+
// call: the pre-check is a courtesy and the server is the authority.
|
|
1002
|
+
mode = null;
|
|
1003
|
+
}
|
|
1004
|
+
return { functionId, mode };
|
|
1005
|
+
}
|
|
1006
|
+
/** The identity a call runs under, and the bearer it presents. */
|
|
1007
|
+
async function planIdentity(client, appId, identity) {
|
|
1008
|
+
if (identity.kind === "system") {
|
|
1009
|
+
const me = await client.getAppProfile(appId);
|
|
1010
|
+
return {
|
|
1011
|
+
describe: describeIdentity({ kind: "system", byUserId: me.userId }),
|
|
1012
|
+
actingUserId: null,
|
|
1013
|
+
asSystem: true,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
if (identity.kind === "user") {
|
|
1017
|
+
return {
|
|
1018
|
+
describe: describeIdentity({ kind: "user", userId: identity.userId }),
|
|
1019
|
+
actingUserId: identity.userId,
|
|
1020
|
+
asSystem: false,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const me = await client.getAppProfile(appId);
|
|
1024
|
+
return {
|
|
1025
|
+
describe: describeIdentity({
|
|
1026
|
+
kind: "self",
|
|
1027
|
+
userId: me.userId,
|
|
1028
|
+
appRole: me.appRole,
|
|
1029
|
+
}),
|
|
1030
|
+
actingUserId: me.userId,
|
|
1031
|
+
asSystem: false,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/** Print an envelope's ids, each followed by the command that takes it. */
|
|
1035
|
+
function printIdLines(lines) {
|
|
1036
|
+
for (const line of renderIdLines(lines))
|
|
1037
|
+
console.log(line);
|
|
1038
|
+
}
|
|
1039
|
+
functions
|
|
1040
|
+
.command("invoke")
|
|
1041
|
+
.allowExcessArguments(false)
|
|
1042
|
+
.description("Run a REQUEST function over the public route and print its result — the same call `client.functions.invoke` makes")
|
|
1043
|
+
.argument("<key>", "Function key (as written in functions/<key>.toml)")
|
|
1044
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1045
|
+
.option("--input <json>", "The handler's input, as JSON (default {})")
|
|
1046
|
+
.option("--context-doc-id <id>", "Context document for the invocation")
|
|
1047
|
+
.option("--timeout <seconds>", "Wall-clock budget; the platform clamps at 30s")
|
|
1048
|
+
.option("--user <user-id>", "Run as this app user (admin/owner only)")
|
|
1049
|
+
.option("--as <principal>", "Run with no caller: --as system (admin/owner only)")
|
|
1050
|
+
.option("--json", "Output as JSON")
|
|
1051
|
+
.action(async (functionKey, options) => {
|
|
1052
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1053
|
+
const flags = resolveRunFlags(options);
|
|
1054
|
+
if (flags.ok === false) {
|
|
1055
|
+
error(flags.error);
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
const client = new ApiClient();
|
|
1059
|
+
try {
|
|
1060
|
+
const target = await resolveFunctionByKey(client, resolvedAppId, functionKey);
|
|
1061
|
+
const precheck = precheckMode("invoke", target.mode, functionKey);
|
|
1062
|
+
if (precheck.ok === false) {
|
|
1063
|
+
error(precheck.error);
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
const identity = await planIdentity(client, resolvedAppId, flags.identity);
|
|
1067
|
+
const body = {
|
|
1068
|
+
rootInput: flags.input,
|
|
1069
|
+
mode: modeFor("invoke"),
|
|
1070
|
+
...(options.contextDocId ? { contextDocId: options.contextDocId } : {}),
|
|
1071
|
+
...(flags.timeoutSeconds
|
|
1072
|
+
? { timeoutMs: Math.round(flags.timeoutSeconds * 1000) }
|
|
1073
|
+
: {}),
|
|
1074
|
+
};
|
|
1075
|
+
const answer = await runAs(client, resolvedAppId, flags.identity, "invoke", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
|
|
1076
|
+
...(bearer ? { bearer } : {}),
|
|
1077
|
+
asSystem: identity.asSystem,
|
|
1078
|
+
}));
|
|
1079
|
+
const envelope = answer.body ?? {};
|
|
1080
|
+
if (options.json) {
|
|
1081
|
+
json({
|
|
1082
|
+
functionId: target.functionId,
|
|
1083
|
+
functionKey,
|
|
1084
|
+
identity: identity.describe,
|
|
1085
|
+
httpStatus: answer.httpStatus,
|
|
1086
|
+
...(answer.retryAfterSeconds !== null
|
|
1087
|
+
? { retryAfterSeconds: answer.retryAfterSeconds }
|
|
1088
|
+
: {}),
|
|
1089
|
+
...envelope,
|
|
1090
|
+
});
|
|
1091
|
+
process.exit(exitCodeForInvokeAnswer(answer));
|
|
1092
|
+
}
|
|
1093
|
+
if (answer.httpStatus >= 400) {
|
|
1094
|
+
renderRefusal(answer);
|
|
1095
|
+
// The ids, each with the command that takes it — the same rendering
|
|
1096
|
+
// a success gets, because a refusal is when an operator most needs
|
|
1097
|
+
// the next command (CR3448-004). A REFUSAL CAN CARRY AN INVOCATION
|
|
1098
|
+
// ID: the two 500s the platform records answer one (edge 35), and
|
|
1099
|
+
// printing the function id bare beside it left the record findable
|
|
1100
|
+
// only by someone who already knew the flag.
|
|
1101
|
+
printIdLines(invokeIdLines({
|
|
1102
|
+
functionId: target.functionId,
|
|
1103
|
+
invocationId: envelope.invocationId,
|
|
1104
|
+
}));
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
printResult("Status", formatStatus(String(envelope.status ?? "")));
|
|
1108
|
+
if (envelope.output !== undefined) {
|
|
1109
|
+
console.log("Output");
|
|
1110
|
+
console.log(JSON.stringify(envelope.output, null, 2));
|
|
1111
|
+
}
|
|
1112
|
+
if (envelope.error)
|
|
1113
|
+
printResult("Error", String(envelope.error));
|
|
1114
|
+
if (envelope.errorCode)
|
|
1115
|
+
printResult("Code", String(envelope.errorCode));
|
|
1116
|
+
if (envelope.limits) {
|
|
1117
|
+
printResult("Limits", `cpuMs ${envelope.limits.cpuMs}, subRequests ${envelope.limits.subRequests}, ratePerMinute ${envelope.limits.ratePerMinute}`);
|
|
1118
|
+
}
|
|
1119
|
+
console.log(identity.describe);
|
|
1120
|
+
printIdLines(invokeIdLines({
|
|
1121
|
+
functionId: target.functionId,
|
|
1122
|
+
invocationId: envelope.invocationId,
|
|
1123
|
+
}));
|
|
1124
|
+
process.exit(exitCodeForInvokeAnswer(answer));
|
|
1125
|
+
}
|
|
1126
|
+
catch (err) {
|
|
1127
|
+
error(err.message);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
functions
|
|
1132
|
+
.command("start")
|
|
1133
|
+
.allowExcessArguments(false)
|
|
1134
|
+
.description("Start a TASK function and print its run id")
|
|
1135
|
+
.argument("<key>", "Function key (as written in functions/<key>.toml)")
|
|
1136
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1137
|
+
.option("--input <json>", "The handler's input, as JSON (default {})")
|
|
1138
|
+
.option("--context-doc-id <id>", "Context document the run is keyed under")
|
|
1139
|
+
.option("--run-key <key>", "Idempotency key; a repeat replays the existing run")
|
|
1140
|
+
.option("--user <user-id>", "Run as this app user (admin/owner only)")
|
|
1141
|
+
.option("--as <principal>", "Run with no caller: --as system (admin/owner only)")
|
|
1142
|
+
.option("--wait", "Wait for the run to settle, then print its outcome")
|
|
1143
|
+
.option("--timeout <seconds>", "Budget for --wait (default 900)")
|
|
1144
|
+
.option("--json", "Output as JSON")
|
|
1145
|
+
.action(async (functionKey, options) => {
|
|
1146
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1147
|
+
const flags = resolveRunFlags(options);
|
|
1148
|
+
if (flags.ok === false) {
|
|
1149
|
+
error(flags.error);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
const client = new ApiClient();
|
|
1153
|
+
try {
|
|
1154
|
+
const target = await resolveFunctionByKey(client, resolvedAppId, functionKey);
|
|
1155
|
+
const precheck = precheckMode("start", target.mode, functionKey);
|
|
1156
|
+
if (precheck.ok === false) {
|
|
1157
|
+
error(precheck.error);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
const identity = await planIdentity(client, resolvedAppId, flags.identity);
|
|
1161
|
+
// D3448-014 — the task path defaults the context to the acting user's
|
|
1162
|
+
// `AppUser.rootDocId`, and the app user provisioned for an admin has
|
|
1163
|
+
// none, so the advertised `functions start <key> --wait` would refuse
|
|
1164
|
+
// `CONTEXT_DOC_REQUIRED` on a fresh app. The route that mints one is
|
|
1165
|
+
// idempotent, so asking for it is free on the second call.
|
|
1166
|
+
//
|
|
1167
|
+
// `--as system` keeps the synthetic `fn:<functionId>` context and does
|
|
1168
|
+
// NOT call the route: there is no app user to mint a document for.
|
|
1169
|
+
let contextDocId = options.contextDocId;
|
|
1170
|
+
if (!contextDocId && !identity.asSystem && identity.actingUserId) {
|
|
1171
|
+
const root = await client.ensureUserRootDocument(resolvedAppId, identity.actingUserId);
|
|
1172
|
+
contextDocId = root.rootDocId;
|
|
1173
|
+
}
|
|
1174
|
+
const body = {
|
|
1175
|
+
rootInput: flags.input,
|
|
1176
|
+
mode: modeFor("start"),
|
|
1177
|
+
...(contextDocId ? { contextDocId } : {}),
|
|
1178
|
+
...(options.runKey ? { runKey: options.runKey } : {}),
|
|
1179
|
+
};
|
|
1180
|
+
const answer = await runAs(client, resolvedAppId, flags.identity, "start", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
|
|
1181
|
+
...(bearer ? { bearer } : {}),
|
|
1182
|
+
asSystem: identity.asSystem,
|
|
1183
|
+
}));
|
|
1184
|
+
const envelope = answer.body ?? {};
|
|
1185
|
+
if (answer.httpStatus >= 400) {
|
|
1186
|
+
if (options.json) {
|
|
1187
|
+
json({
|
|
1188
|
+
functionId: target.functionId,
|
|
1189
|
+
functionKey,
|
|
1190
|
+
identity: identity.describe,
|
|
1191
|
+
httpStatus: answer.httpStatus,
|
|
1192
|
+
...(answer.retryAfterSeconds !== null
|
|
1193
|
+
? { retryAfterSeconds: answer.retryAfterSeconds }
|
|
1194
|
+
: {}),
|
|
1195
|
+
...envelope,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
renderRefusal(answer);
|
|
1200
|
+
}
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
const runId = String(envelope.runId ?? "");
|
|
1204
|
+
if (!options.json) {
|
|
1205
|
+
if (envelope.existing) {
|
|
1206
|
+
info(`Replayed the existing run for this run key (existing).`);
|
|
1207
|
+
}
|
|
1208
|
+
console.log(identity.describe);
|
|
1209
|
+
printIdLines(startIdLines({ functionId: target.functionId, runId }));
|
|
1210
|
+
}
|
|
1211
|
+
if (!options.wait) {
|
|
1212
|
+
if (options.json) {
|
|
1213
|
+
json({
|
|
1214
|
+
functionId: target.functionId,
|
|
1215
|
+
functionKey,
|
|
1216
|
+
identity: identity.describe,
|
|
1217
|
+
httpStatus: answer.httpStatus,
|
|
1218
|
+
...envelope,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
process.exit(0);
|
|
1222
|
+
}
|
|
1223
|
+
// The run id is printed BEFORE the wait begins, so an operator who
|
|
1224
|
+
// interrupts still holds the thing they need to resume.
|
|
1225
|
+
const outcome = await waitForFunctionRun({
|
|
1226
|
+
client,
|
|
1227
|
+
appId: resolvedAppId,
|
|
1228
|
+
functionId: target.functionId,
|
|
1229
|
+
runId,
|
|
1230
|
+
timeoutSeconds: flags.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_SECONDS,
|
|
1231
|
+
json: !!options.json,
|
|
1232
|
+
});
|
|
1233
|
+
process.exit(outcome);
|
|
1234
|
+
}
|
|
1235
|
+
catch (err) {
|
|
1236
|
+
error(err.message);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
runs
|
|
1241
|
+
.command("wait")
|
|
1242
|
+
.allowExcessArguments(false)
|
|
1243
|
+
.description("Poll a function run until it settles")
|
|
1244
|
+
.argument("<function-id>", "Function ID")
|
|
1245
|
+
.argument("<run-id>", "Run ID")
|
|
1246
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1247
|
+
.option("--timeout <seconds>", "Give up after this many seconds (default 900)")
|
|
1248
|
+
.option("--json", "Output as JSON")
|
|
1249
|
+
.action(async (functionId, runId, _options, command) => {
|
|
1250
|
+
// `optsWithGlobals`, for the reason `runs steps` records: `runs` declares
|
|
1251
|
+
// `--app` and `--json` of its own, so both land on the parent.
|
|
1252
|
+
const options = command.optsWithGlobals();
|
|
1253
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1254
|
+
const flags = resolveRunFlags({ timeout: options.timeout });
|
|
1255
|
+
if (flags.ok === false) {
|
|
1256
|
+
error(flags.error);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
const client = new ApiClient();
|
|
1260
|
+
try {
|
|
1261
|
+
const outcome = await waitForFunctionRun({
|
|
1262
|
+
client,
|
|
1263
|
+
appId: resolvedAppId,
|
|
1264
|
+
functionId,
|
|
1265
|
+
runId,
|
|
1266
|
+
timeoutSeconds: flags.timeoutSeconds ?? WAIT_DEFAULT_TIMEOUT_SECONDS,
|
|
1267
|
+
json: !!options.json,
|
|
1268
|
+
});
|
|
1269
|
+
process.exit(outcome);
|
|
1270
|
+
}
|
|
1271
|
+
catch (err) {
|
|
1272
|
+
error(err.message);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* A refusal, rendered from the envelope the server sent — #3448.
|
|
1279
|
+
*
|
|
1280
|
+
* `ApiClient.invokeFunction` hands the answer back rather than throwing for
|
|
1281
|
+
* exactly this: a 429 carries a `Retry-After` an operator needs and a 409 mode
|
|
1282
|
+
* mismatch carries the version's real mode, and re-deriving either from an
|
|
1283
|
+
* exception message would be re-deriving what the server already said.
|
|
1284
|
+
*/
|
|
1285
|
+
function renderRefusal(answer) {
|
|
1286
|
+
const lines = refusalLines(answer);
|
|
1287
|
+
error(lines.message);
|
|
1288
|
+
for (const field of lines.fields)
|
|
1289
|
+
printResult(field.label, field.value);
|
|
1290
|
+
for (const detail of lines.details)
|
|
1291
|
+
info(` ${detail}`);
|
|
1292
|
+
}
|
|
1293
|
+
/** Exit 0 only for a completed invocation; every refusal and every terminal failure is 1. */
|
|
1294
|
+
function exitCodeForInvokeAnswer(answer) {
|
|
1295
|
+
if (answer.httpStatus >= 400)
|
|
1296
|
+
return 1;
|
|
1297
|
+
return exitCodeForInvokeStatus(answer.body?.status);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Run a call under the operator's own identity, or under a ten-minute token
|
|
1301
|
+
* minted for `--user` — D3448-009.
|
|
1302
|
+
*
|
|
1303
|
+
* The token is a REAL credential for another user, so it is revoked on every
|
|
1304
|
+
* exit path including Ctrl-C (the wrapper's `finally`), and its value never
|
|
1305
|
+
* reaches stdout, stderr or `--json`: `use` receives it and nothing else does.
|
|
1306
|
+
*/
|
|
1307
|
+
async function runAs(client, appId, identity, verb, functionKey, use) {
|
|
1308
|
+
if (identity.kind !== "user")
|
|
1309
|
+
return use(undefined);
|
|
1310
|
+
return withMintedToken({
|
|
1311
|
+
mint: async () => {
|
|
1312
|
+
const minted = await client.createToken(appId, {
|
|
1313
|
+
name: userTokenName(verb, functionKey),
|
|
1314
|
+
ttl: USER_TOKEN_TTL,
|
|
1315
|
+
userId: identity.userId,
|
|
1316
|
+
});
|
|
1317
|
+
return { token: minted.token, tokenId: minted.tokenId };
|
|
1318
|
+
},
|
|
1319
|
+
revoke: (tokenId) => client.revokeToken(appId, tokenId).then(() => undefined),
|
|
1320
|
+
warn: (message) => warn(message),
|
|
1321
|
+
}, (token) => use(token));
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Poll one run until it settles, the budget is spent, or the operator
|
|
1325
|
+
* interrupts — #3448 behavior 16.
|
|
1326
|
+
*
|
|
1327
|
+
* It polls the ADMIN single-run read, never the runs listing: that reports the
|
|
1328
|
+
* STORED status, and a durable run's row stays `running` until something asks
|
|
1329
|
+
* the engine, so a wait built on it would never settle.
|
|
1330
|
+
*
|
|
1331
|
+
* Ctrl-C exits 130 after printing the resume command, and a second one exits
|
|
1332
|
+
* at once — the failures sweep's shape, and the workflows group's convention.
|
|
1333
|
+
*/
|
|
1334
|
+
async function waitForFunctionRun(args) {
|
|
1335
|
+
const { client, appId, functionId, runId } = args;
|
|
1336
|
+
const deadline = Date.now() + args.timeoutSeconds * 1000;
|
|
1337
|
+
let interrupted = false;
|
|
1338
|
+
let interruptCount = 0;
|
|
1339
|
+
const onSigint = () => {
|
|
1340
|
+
interruptCount += 1;
|
|
1341
|
+
if (interruptCount > 1)
|
|
1342
|
+
process.exit(EXIT_INTERRUPTED);
|
|
1343
|
+
interrupted = true;
|
|
1344
|
+
};
|
|
1345
|
+
process.on("SIGINT", onSigint);
|
|
1346
|
+
try {
|
|
1347
|
+
let attempt = 0;
|
|
1348
|
+
for (;;) {
|
|
1349
|
+
const read = await client.getFunctionRun(appId, functionId, runId);
|
|
1350
|
+
const status = String(read?.status?.status ?? "");
|
|
1351
|
+
const settled = exitCodeForRunStatus(status);
|
|
1352
|
+
if (settled !== null) {
|
|
1353
|
+
if (args.json) {
|
|
1354
|
+
json({ functionId, runId, ...read });
|
|
1355
|
+
return settled;
|
|
1356
|
+
}
|
|
1357
|
+
printResult("Status", formatStatus(status));
|
|
1358
|
+
if (read?.status?.output !== undefined) {
|
|
1359
|
+
console.log("Output");
|
|
1360
|
+
console.log(JSON.stringify(read.status.output, null, 2));
|
|
1361
|
+
}
|
|
1362
|
+
const failure = read?.status?.error;
|
|
1363
|
+
if (failure) {
|
|
1364
|
+
printResult("Error", typeof failure === "string" ? failure : String(failure?.message ?? ""));
|
|
1365
|
+
}
|
|
1366
|
+
if (read?.run?.errorCode)
|
|
1367
|
+
printResult("Code", String(read.run.errorCode));
|
|
1368
|
+
return settled;
|
|
1369
|
+
}
|
|
1370
|
+
if (interrupted) {
|
|
1371
|
+
if (!args.json) {
|
|
1372
|
+
info(`Stopped waiting. The run is still going; resume with:`);
|
|
1373
|
+
info(` ${resumeCommand(functionId, runId)}`);
|
|
1374
|
+
}
|
|
1375
|
+
return EXIT_INTERRUPTED;
|
|
1376
|
+
}
|
|
1377
|
+
if (Date.now() >= deadline) {
|
|
1378
|
+
if (args.json) {
|
|
1379
|
+
json({ functionId, runId, timedOut: true, ...read });
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
warn(`Gave up after ${args.timeoutSeconds}s; the run is still ${status || "in flight"}. Resume with:`);
|
|
1383
|
+
info(` ${resumeCommand(functionId, runId)}`);
|
|
1384
|
+
}
|
|
1385
|
+
return EXIT_TIMED_OUT;
|
|
1386
|
+
}
|
|
1387
|
+
const delay = Math.min(waitDelayMs(attempt), Math.max(0, deadline - Date.now()));
|
|
1388
|
+
attempt += 1;
|
|
1389
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
finally {
|
|
1393
|
+
process.off("SIGINT", onSigint);
|
|
1394
|
+
}
|
|
671
1395
|
}
|
|
672
1396
|
//# sourceMappingURL=functions.js.map
|