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.
Files changed (57) hide show
  1. package/assets/skill/skills/primitive-platform/SKILL.md +12 -0
  2. package/dist/src/commands/documents.js +163 -21
  3. package/dist/src/commands/documents.js.map +1 -1
  4. package/dist/src/commands/functions.js +640 -9
  5. package/dist/src/commands/functions.js.map +1 -1
  6. package/dist/src/commands/prompts.js +19 -2
  7. package/dist/src/commands/prompts.js.map +1 -1
  8. package/dist/src/commands/sync.js +27 -4
  9. package/dist/src/commands/sync.js.map +1 -1
  10. package/dist/src/commands/workflows.d.ts +1 -29
  11. package/dist/src/commands/workflows.js +8 -109
  12. package/dist/src/commands/workflows.js.map +1 -1
  13. package/dist/src/lib/api-client.d.ts +116 -0
  14. package/dist/src/lib/api-client.js +163 -2
  15. package/dist/src/lib/api-client.js.map +1 -1
  16. package/dist/src/lib/config-object-descriptor.js +12 -4
  17. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  18. package/dist/src/lib/env-resolver-core.d.ts +2 -2
  19. package/dist/src/lib/env-resolver-core.js +2 -2
  20. package/dist/src/lib/function-db-types.js +15 -0
  21. package/dist/src/lib/function-db-types.js.map +1 -1
  22. package/dist/src/lib/function-document-types.js +4 -0
  23. package/dist/src/lib/function-document-types.js.map +1 -1
  24. package/dist/src/lib/function-log-tail.d.ts +51 -2
  25. package/dist/src/lib/function-log-tail.js +160 -13
  26. package/dist/src/lib/function-log-tail.js.map +1 -1
  27. package/dist/src/lib/function-run.d.ts +271 -0
  28. package/dist/src/lib/function-run.js +378 -0
  29. package/dist/src/lib/function-run.js.map +1 -0
  30. package/dist/src/lib/function-schema-codegen.d.ts +11 -0
  31. package/dist/src/lib/function-schema-codegen.js +20 -6
  32. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  33. package/dist/src/lib/function-sync.d.ts +9 -4
  34. package/dist/src/lib/function-sync.js +29 -7
  35. package/dist/src/lib/function-sync.js.map +1 -1
  36. package/dist/src/lib/generated-config-surfaces.d.ts +246 -20
  37. package/dist/src/lib/generated-config-surfaces.js +848 -83
  38. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  39. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  40. package/dist/src/lib/generated-sdk-types.js +1 -1
  41. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  42. package/dist/src/lib/prompt-schema-codegen.d.ts +94 -0
  43. package/dist/src/lib/prompt-schema-codegen.js +212 -0
  44. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  45. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  46. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  47. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  48. package/dist/src/lib/step-run-table.d.ts +43 -0
  49. package/dist/src/lib/step-run-table.js +129 -0
  50. package/dist/src/lib/step-run-table.js.map +1 -0
  51. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +7 -1
  52. package/dist/src/lib/swift-codegen/functionGenerator.js +15 -5
  53. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  54. package/dist/src/lib/workflow-toml-validator.d.ts +9 -5
  55. package/dist/src/lib/workflow-toml-validator.js +18 -10
  56. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  57. 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 `mode` serializes
172
- // only the `durable` column, which reads the same way.
173
- printResult(" Mode", activeVersionMode(active));
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
- const { items, nextCursor } = await client.listFunctionLogs(resolvedAppId, functionId, {
437
- ...(limit ? { limit } : {}),
438
- ...(options.cursor ? { cursor: options.cursor } : {}),
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("No invocation logs found for this function.");
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