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.
Files changed (87) hide show
  1. package/README.md +5 -5
  2. package/assets/skill/skills/primitive-platform/SKILL.md +23 -11
  3. package/dist/bin/primitive.js +14 -15
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/databases.js +3 -5
  6. package/dist/src/commands/databases.js.map +1 -1
  7. package/dist/src/commands/documents.js +163 -21
  8. package/dist/src/commands/documents.js.map +1 -1
  9. package/dist/src/commands/functions.js +736 -12
  10. package/dist/src/commands/functions.js.map +1 -1
  11. package/dist/src/commands/prompts.js +19 -2
  12. package/dist/src/commands/prompts.js.map +1 -1
  13. package/dist/src/commands/sync-app-settings.js +2 -2
  14. package/dist/src/commands/sync-app-settings.js.map +1 -1
  15. package/dist/src/commands/sync.js +29 -27
  16. package/dist/src/commands/sync.js.map +1 -1
  17. package/dist/src/commands/webhooks.d.ts +29 -0
  18. package/dist/src/commands/webhooks.js +118 -54
  19. package/dist/src/commands/webhooks.js.map +1 -1
  20. package/dist/src/commands/workflows.d.ts +1 -29
  21. package/dist/src/commands/workflows.js +8 -109
  22. package/dist/src/commands/workflows.js.map +1 -1
  23. package/dist/src/lib/api-client.d.ts +119 -3
  24. package/dist/src/lib/api-client.js +166 -5
  25. package/dist/src/lib/api-client.js.map +1 -1
  26. package/dist/src/lib/config-object-descriptor.js +12 -4
  27. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  28. package/dist/src/lib/env-resolver-core.d.ts +24 -19
  29. package/dist/src/lib/env-resolver-core.js +35 -54
  30. package/dist/src/lib/env-resolver-core.js.map +1 -1
  31. package/dist/src/lib/function-db-types.js +15 -0
  32. package/dist/src/lib/function-db-types.js.map +1 -1
  33. package/dist/src/lib/function-document-types.js +4 -0
  34. package/dist/src/lib/function-document-types.js.map +1 -1
  35. package/dist/src/lib/function-log-tail.d.ts +51 -2
  36. package/dist/src/lib/function-log-tail.js +160 -13
  37. package/dist/src/lib/function-log-tail.js.map +1 -1
  38. package/dist/src/lib/function-run.d.ts +271 -0
  39. package/dist/src/lib/function-run.js +378 -0
  40. package/dist/src/lib/function-run.js.map +1 -0
  41. package/dist/src/lib/function-schema-codegen.d.ts +28 -0
  42. package/dist/src/lib/function-schema-codegen.js +41 -10
  43. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  44. package/dist/src/lib/function-sync.d.ts +9 -4
  45. package/dist/src/lib/function-sync.js +29 -7
  46. package/dist/src/lib/function-sync.js.map +1 -1
  47. package/dist/src/lib/generated-config-surfaces.d.ts +555 -19
  48. package/dist/src/lib/generated-config-surfaces.js +1826 -80
  49. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  50. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  51. package/dist/src/lib/generated-sdk-types.js +1 -1
  52. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  53. package/dist/src/lib/local-test-cases.js +2 -2
  54. package/dist/src/lib/local-test-cases.js.map +1 -1
  55. package/dist/src/lib/prompt-schema-codegen.d.ts +94 -0
  56. package/dist/src/lib/prompt-schema-codegen.js +212 -0
  57. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  58. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  59. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  60. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  61. package/dist/src/lib/step-run-table.d.ts +43 -0
  62. package/dist/src/lib/step-run-table.js +129 -0
  63. package/dist/src/lib/step-run-table.js.map +1 -0
  64. package/dist/src/lib/swift-codegen/banners.d.ts +18 -0
  65. package/dist/src/lib/swift-codegen/banners.js +19 -0
  66. package/dist/src/lib/swift-codegen/banners.js.map +1 -0
  67. package/dist/src/lib/swift-codegen/dbGenerator.js +11 -7
  68. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  69. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +125 -0
  70. package/dist/src/lib/swift-codegen/functionGenerator.js +423 -0
  71. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
  72. package/dist/src/lib/swift-codegen/generator.d.ts +6 -0
  73. package/dist/src/lib/swift-codegen/generator.js +21 -10
  74. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  75. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +15 -0
  76. package/dist/src/lib/swift-codegen/schemaToSwift.js +21 -4
  77. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
  78. package/dist/src/lib/sync-paths.d.ts +19 -14
  79. package/dist/src/lib/sync-paths.js +26 -24
  80. package/dist/src/lib/sync-paths.js.map +1 -1
  81. package/dist/src/lib/webhook-deliver.d.ts +209 -0
  82. package/dist/src/lib/webhook-deliver.js +519 -0
  83. package/dist/src/lib/webhook-deliver.js.map +1 -0
  84. package/dist/src/lib/workflow-toml-validator.d.ts +9 -5
  85. package/dist/src/lib/workflow-toml-validator.js +18 -10
  86. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  87. 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 `mode` serializes
170
- // only the `durable` column, which reads the same way.
171
- 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);
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(items, [
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
- const { items, nextCursor } = await client.listFunctionLogs(resolvedAppId, functionId, {
423
- ...(limit ? { limit } : {}),
424
- ...(options.cursor ? { cursor: options.cursor } : {}),
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("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}`);
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