primitive-admin 1.1.0-alpha.81 → 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/assets/skill/skills/primitive-platform/SKILL.md +12 -0
- package/dist/src/commands/documents.js +163 -21
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +640 -9
- 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.js +27 -4
- 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 +116 -0
- package/dist/src/lib/api-client.js +163 -2
- 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 +2 -2
- package/dist/src/lib/env-resolver-core.js +2 -2
- 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 +11 -0
- package/dist/src/lib/function-schema-codegen.js +20 -6
- 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 +246 -20
- package/dist/src/lib/generated-config-surfaces.js +848 -83
- 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/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/functionGenerator.d.ts +7 -1
- package/dist/src/lib/swift-codegen/functionGenerator.js +15 -5
- package/dist/src/lib/swift-codegen/functionGenerator.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
|
@@ -29,9 +29,12 @@ import { SwiftCodegenError } from "../lib/swift-codegen/schemaToSwift.js";
|
|
|
29
29
|
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatDate, formatStatus, json, jsonLine, divider, } from "../lib/output.js";
|
|
30
30
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
31
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";
|
|
32
34
|
import { followLoop } from "../lib/watch.js";
|
|
33
35
|
import { buildLogFollowSource } from "../lib/function-log-tail.js";
|
|
34
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";
|
|
35
38
|
/**
|
|
36
39
|
* How far back one `--follow` tick will page to meet its high-water mark.
|
|
37
40
|
*
|
|
@@ -168,9 +171,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
168
171
|
printResult(" Config ID", active.configId);
|
|
169
172
|
printResult(" Entry", active.entry);
|
|
170
173
|
printResult(" Content Hash", active.contentHash || "-");
|
|
171
|
-
// #3281 — the mode words. A server that predates
|
|
172
|
-
// only the `durable` column, which reads the same
|
|
173
|
-
|
|
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);
|
|
174
179
|
printResult(" Limits", active.limits || "-");
|
|
175
180
|
// #3182 — what this version declared: since #3279 the egress
|
|
176
181
|
// allowlist, the secrets that may enter the sandbox and the
|
|
@@ -220,6 +225,10 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
220
225
|
{ header: "NAME", key: "name" },
|
|
221
226
|
{ header: "ID", key: "triggerId" },
|
|
222
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" },
|
|
223
232
|
{ header: "TZ", key: "timezone" },
|
|
224
233
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
225
234
|
{ header: "NEXT FIRE", key: "nextFireAt", format: formatDate },
|
|
@@ -302,7 +311,7 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
302
311
|
// no `workflowId`, so `primitive workflows runs` cannot see it. This verb is
|
|
303
312
|
// the only way to inspect what a webhook or a schedule actually did
|
|
304
313
|
// (principle 8).
|
|
305
|
-
functions
|
|
314
|
+
const runs = functions
|
|
306
315
|
.command("runs")
|
|
307
316
|
.description("List a server function's runs, newest first")
|
|
308
317
|
.argument("<function-id>", "Function ID")
|
|
@@ -359,6 +368,109 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
359
368
|
process.exit(1);
|
|
360
369
|
}
|
|
361
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
|
+
});
|
|
362
474
|
// #3287 — the invocation logs. `runs` above answers what HAPPENED; this
|
|
363
475
|
// answers what the function PRINTED and what it threw, which is the thing
|
|
364
476
|
// an operator reaches for when a function is failing and the response says
|
|
@@ -372,6 +484,8 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
372
484
|
.option("--cursor <cursor>", "Continue from a previous page")
|
|
373
485
|
.option("--follow", "Append new invocations as they are recorded (tail)")
|
|
374
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)")
|
|
375
489
|
.option("--json", "Output as JSON")
|
|
376
490
|
.action(async (functionId, options) => {
|
|
377
491
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
@@ -383,7 +497,57 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
383
497
|
error("--follow and --cursor cannot be combined; --follow tails from now.");
|
|
384
498
|
process.exit(1);
|
|
385
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
|
+
}
|
|
386
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
|
+
}
|
|
387
551
|
if (options.follow) {
|
|
388
552
|
const intervalSeconds = options.interval ? Number(options.interval) : 2;
|
|
389
553
|
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
@@ -433,10 +597,34 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
433
597
|
}
|
|
434
598
|
return;
|
|
435
599
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
}
|
|
440
628
|
if (options.json) {
|
|
441
629
|
// The shared envelope, whose items are the shared inspection shape:
|
|
442
630
|
// a consumer classifies and pivots on `outcome`, `nativeStatus` and
|
|
@@ -449,7 +637,14 @@ Configuration lives in functions/<key>.toml, and the code beside it:
|
|
|
449
637
|
return;
|
|
450
638
|
}
|
|
451
639
|
if (!items || items.length === 0) {
|
|
452
|
-
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}`);
|
|
453
648
|
return;
|
|
454
649
|
}
|
|
455
650
|
console.log(formatTable(items.map((row) => ({
|
|
@@ -761,5 +956,441 @@ Reclaiming the key means hard-deleting the row:
|
|
|
761
956
|
process.exit(1);
|
|
762
957
|
}
|
|
763
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
|
+
}
|
|
764
1395
|
}
|
|
765
1396
|
//# sourceMappingURL=functions.js.map
|