primitive-admin 1.1.0-alpha.82 → 1.1.0-alpha.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/src/commands/documents.js +333 -42
  2. package/dist/src/commands/documents.js.map +1 -1
  3. package/dist/src/commands/functions.js +302 -120
  4. package/dist/src/commands/functions.js.map +1 -1
  5. package/dist/src/commands/locks.js +15 -1
  6. package/dist/src/commands/locks.js.map +1 -1
  7. package/dist/src/commands/sync.d.ts +50 -0
  8. package/dist/src/commands/sync.js +560 -312
  9. package/dist/src/commands/sync.js.map +1 -1
  10. package/dist/src/lib/api-client.d.ts +75 -2
  11. package/dist/src/lib/api-client.js +122 -7
  12. package/dist/src/lib/api-client.js.map +1 -1
  13. package/dist/src/lib/config-object-descriptor.js +5 -5
  14. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  15. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  16. package/dist/src/lib/document-ingest-artifact.js +505 -0
  17. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  18. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  19. package/dist/src/lib/document-ingest-input.js +132 -0
  20. package/dist/src/lib/document-ingest-input.js.map +1 -0
  21. package/dist/src/lib/document-ingest.d.ts +78 -0
  22. package/dist/src/lib/document-ingest.js +231 -0
  23. package/dist/src/lib/document-ingest.js.map +1 -0
  24. package/dist/src/lib/function-bundle.d.ts +6 -0
  25. package/dist/src/lib/function-bundle.js +8 -0
  26. package/dist/src/lib/function-bundle.js.map +1 -1
  27. package/dist/src/lib/function-collect.d.ts +1 -1
  28. package/dist/src/lib/function-collect.js +16 -0
  29. package/dist/src/lib/function-collect.js.map +1 -1
  30. package/dist/src/lib/function-db-types.d.ts +4 -0
  31. package/dist/src/lib/function-db-types.js +31 -2
  32. package/dist/src/lib/function-db-types.js.map +1 -1
  33. package/dist/src/lib/function-document-types.d.ts +83 -0
  34. package/dist/src/lib/function-document-types.js +133 -0
  35. package/dist/src/lib/function-document-types.js.map +1 -1
  36. package/dist/src/lib/function-grants-preflight.js +5 -0
  37. package/dist/src/lib/function-grants-preflight.js.map +1 -1
  38. package/dist/src/lib/function-log-lines.d.ts +76 -0
  39. package/dist/src/lib/function-log-lines.js +160 -0
  40. package/dist/src/lib/function-log-lines.js.map +1 -0
  41. package/dist/src/lib/function-log-row.d.ts +29 -0
  42. package/dist/src/lib/function-log-row.js +73 -0
  43. package/dist/src/lib/function-log-row.js.map +1 -0
  44. package/dist/src/lib/function-run.d.ts +53 -35
  45. package/dist/src/lib/function-run.js +55 -44
  46. package/dist/src/lib/function-run.js.map +1 -1
  47. package/dist/src/lib/function-schema-codegen.d.ts +6 -9
  48. package/dist/src/lib/function-schema-codegen.js +22 -35
  49. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  50. package/dist/src/lib/function-sync.d.ts +226 -35
  51. package/dist/src/lib/function-sync.js +731 -99
  52. package/dist/src/lib/function-sync.js.map +1 -1
  53. package/dist/src/lib/function-versions.d.ts +122 -0
  54. package/dist/src/lib/function-versions.js +182 -0
  55. package/dist/src/lib/function-versions.js.map +1 -0
  56. package/dist/src/lib/generated-config-surfaces.d.ts +390 -146
  57. package/dist/src/lib/generated-config-surfaces.js +1207 -401
  58. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  59. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  60. package/dist/src/lib/generated-sdk-types.js +1 -1
  61. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  62. package/dist/src/lib/log-inspection.d.ts +2 -0
  63. package/dist/src/lib/log-inspection.js +4 -0
  64. package/dist/src/lib/log-inspection.js.map +1 -1
  65. package/dist/src/lib/output.d.ts +15 -0
  66. package/dist/src/lib/output.js +28 -0
  67. package/dist/src/lib/output.js.map +1 -1
  68. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  69. package/dist/src/lib/snapshot-audit-source.js +58 -0
  70. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  71. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  72. package/dist/src/lib/snapshot-audit-store.js +196 -0
  73. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  74. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  75. package/dist/src/lib/snapshot-audit.js +431 -0
  76. package/dist/src/lib/snapshot-audit.js.map +1 -0
  77. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  78. package/dist/src/lib/snapshot-build-rows.js +87 -0
  79. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  80. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +23 -16
  81. package/dist/src/lib/swift-codegen/functionGenerator.js +34 -33
  82. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  83. package/dist/src/lib/sync-selectors.d.ts +45 -2
  84. package/dist/src/lib/sync-selectors.js +66 -5
  85. package/dist/src/lib/sync-selectors.js.map +1 -1
  86. package/package.json +2 -2
@@ -21,20 +21,22 @@ import { join, relative as relativePath } from "path";
21
21
  import { ApiClient } from "../lib/api-client.js";
22
22
  import { resolveAppId } from "../lib/config.js";
23
23
  import { resolveSyncDir } from "../lib/sync-paths.js";
24
- import { activeVersionMode } from "../lib/function-sync.js";
25
24
  import { FunctionSchemaCodegenError, functionSchemaSources, } from "../lib/function-schema-codegen.js";
26
25
  import { functionTypeWiringProblems, staleFunctionTypeArtifacts, writeFunctionTypeArtifacts, } from "../lib/function-db-types.js";
27
26
  import { generateFunctionSwiftTypes } from "../lib/swift-codegen/functionGenerator.js";
28
27
  import { SwiftCodegenError } from "../lib/swift-codegen/schemaToSwift.js";
29
28
  import { success, error, info, warn, keyValue, result as printResult, formatTable, formatDate, formatStatus, json, jsonLine, divider, } from "../lib/output.js";
30
29
  import { confirmPrompt } from "../lib/confirm-prompt.js";
30
+ import { FUNCTION_LOG_COLUMNS, functionLogCells, functionLogLine, } from "../lib/function-log-row.js";
31
+ import { activatedLine, activationOutput, activationPrompt, activationRefusalLine, functionVersionRows, newerVersionLine, versionLine, } from "../lib/function-versions.js";
31
32
  import { parseStatusFilter } from "../lib/object-status-filter.js";
32
33
  import { renderStepRunTable } from "../lib/step-run-table.js";
34
+ import { renderRunLogs } from "../lib/function-log-lines.js";
33
35
  import { buildWorkflowStepEnvelope } from "../lib/log-inspection.js";
34
36
  import { followLoop } from "../lib/watch.js";
35
37
  import { buildLogFollowSource } from "../lib/function-log-tail.js";
36
38
  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";
39
+ import { decidePageContinuation, describeIdentity, exitCodeForInvokeStatus, exitCodeForRunStatus, invokeIdLines, logsFlagConflict, refusalLines, renderIdLines, resolveRunFlags, runRuntimeCell, runSliceCells, runtimeForExecutionMode, resumeCommand, startIdLines, userTokenName, waitDelayMs, withMintedToken, EXIT_INTERRUPTED, EXIT_TIMED_OUT, USER_TOKEN_TTL, WAIT_DEFAULT_TIMEOUT_SECONDS, } from "../lib/function-run.js";
38
40
  /**
39
41
  * How far back one `--follow` tick will page to meet its high-water mark.
40
42
  *
@@ -48,25 +50,26 @@ import { decidePageContinuation, describeIdentity, exitCodeForInvokeStatus, exit
48
50
  */
49
51
  const FOLLOW_MAX_PAGES_PER_TICK = 10;
50
52
  /**
51
- * One invocation, as a human reads it in the table — #3287.
53
+ * Which cell each shared invocation column reads, and how it is rendered.
52
54
  *
53
- * The FIRST error line only: a stack is eight kilobytes and a table row is
54
- * one line, so the row says what happened and `--json` carries the rest.
55
+ * The ORDER lives in `FUNCTION_LOG_COLUMNS` (#3462); this is only the mapping
56
+ * from a header to the cell `functionLogCells` builds for it, so adding a
57
+ * column is one edit in each place rather than three copies of the order.
55
58
  */
56
- function firstErrorLine(row) {
57
- const message = String(row?.errorMessage ?? "").trim();
58
- if (message)
59
- return message.split("\n")[0];
60
- const stderr = Array.isArray(row?.stderr) ? row.stderr : [];
61
- return stderr.length > 0 ? String(stderr[0].line ?? "") : "";
62
- }
63
- /** The same row, for the follow tail's one-line-per-invocation output. */
64
- function logRow(row) {
65
- const when = row?.startedAt ? String(row.startedAt) : "";
66
- const code = row?.errorCode ? ` ${row.errorCode}` : "";
67
- const detail = firstErrorLine(row);
68
- return `${when} ${row?.status ?? ""} ${row?.trigger?.kind ?? ""} ${row?.runId || row?.invocationId || ""}${code}${detail ? ` ${detail}` : ""}`;
69
- }
59
+ const LOG_COLUMN_KEYS = {
60
+ TIME: "startedAt",
61
+ STATUS: "status",
62
+ TRIGGER: "trigger",
63
+ RUNTIME: "runtime",
64
+ VERSION: "configId",
65
+ "RUN/INVOCATION ID": "runOrInvocation",
66
+ CODE: "errorCode",
67
+ ERROR: "firstError",
68
+ };
69
+ const LOG_COLUMN_FORMATS = {
70
+ TIME: formatDate,
71
+ STATUS: formatStatus,
72
+ };
70
73
  export function registerFunctionsCommands(program) {
71
74
  const functions = program
72
75
  .command("functions")
@@ -145,18 +148,56 @@ Configuration lives in functions/<key>.toml, and the code beside it:
145
148
  // The versions are append-only and nameless; `activeConfigId` is the
146
149
  // only thing that says which one runs, so a listing without it would
147
150
  // report the newest rather than the live one.
151
+ //
152
+ // #3462 — a POINT READ, not a drained listing scanned for the id. Past
153
+ // 25 versions the listing is a page and the active version may not be
154
+ // on it at all; one read by id is both cheaper and correct.
148
155
  let active = null;
149
156
  if (record?.activeConfigId) {
150
- const { items } = await client.listFunctionConfigs(resolvedAppId, functionId);
151
- active =
152
- (items || []).find((c) => String(c.configId) === String(record.activeConfigId)) ?? null;
157
+ // #3455 — this single read is also what carries
158
+ // `documentSchemaModels`: the models this version can seed are
159
+ // derived from the schema INSIDE the envelope rather than stored on
160
+ // the row, so the LISTING (one row per version, no R2 read) cannot
161
+ // carry them and only the point read can. Best-effort — an older
162
+ // server sends no such field, and the line then reads exactly as it
163
+ // does for a version pushed without a schema.
164
+ try {
165
+ active = await client.getFunctionConfig(resolvedAppId, functionId, String(record.activeConfigId));
166
+ }
167
+ catch {
168
+ active = null;
169
+ }
170
+ }
171
+ // #3462 — one row of the listing answers "is the active version the
172
+ // newest", which is what tells an operator this function is rolled back.
173
+ let newest = null;
174
+ try {
175
+ const page = await client.listFunctionConfigs(resolvedAppId, functionId, {
176
+ limit: 1,
177
+ });
178
+ newest = (page.items || [])[0] ?? null;
153
179
  }
180
+ catch {
181
+ newest = null;
182
+ }
183
+ const newer = newerVersionLine(newest, record?.activeConfigId, formatDate);
154
184
  if (options.json) {
155
- json({ ...record, activeConfig: active });
185
+ json({
186
+ ...record,
187
+ activeConfig: active,
188
+ ...(newer ? { newerVersion: newest } : {}),
189
+ });
156
190
  return;
157
191
  }
158
192
  printResult("Function ID", record.functionId);
159
193
  printResult("Key", record.functionKey);
194
+ // #3462 — which code this function is running, beside its identity. The
195
+ // version was only reachable by reading the section below, or by asking
196
+ // the API; an operator answering "what is deployed" reads this line.
197
+ printResult("Version", versionLine({
198
+ activeConfigId: record.activeConfigId,
199
+ contentHash: active?.contentHash,
200
+ }));
160
201
  printResult("Description", record.description || "-");
161
202
  printResult("Status", formatStatus(record.status));
162
203
  printResult("Access", record.access || "-");
@@ -171,11 +212,9 @@ Configuration lives in functions/<key>.toml, and the code beside it:
171
212
  printResult(" Config ID", active.configId);
172
213
  printResult(" Entry", active.entry);
173
214
  printResult(" Content Hash", active.contentHash || "-");
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);
215
+ // #3482 — no Mode line. The config says nothing about how a
216
+ // function runs: `invoke` runs it inside the request, `start` runs
217
+ // it as a task, and every function takes both.
179
218
  printResult(" Limits", active.limits || "-");
180
219
  // #3182 — what this version declared: since #3279 the egress
181
220
  // allowlist, the secrets that may enter the sandbox and the
@@ -185,6 +224,28 @@ Configuration lives in functions/<key>.toml, and the code beside it:
185
224
  ? active.capabilities
186
225
  : [];
187
226
  printResult(" Capabilities", capabilities.length > 0 ? capabilities.join(", ") : "-");
227
+ // #3462 — who put this version live and when, from either door (a
228
+ // push or `functions activate`). A header written before that change
229
+ // carries neither field and reads `unknown` rather than naming an
230
+ // actor nobody recorded.
231
+ printResult(" Activated", activatedLine(record, formatDate));
232
+ if (newer) {
233
+ printResult(" Newer version", newer);
234
+ }
235
+ // #3455 — the document models this version can SEED on a first
236
+ // write. An operator asking "why did the first write not seed
237
+ // `institution`" gets the answer here: either the model is in this
238
+ // list, or the version was pushed without a document schema at all.
239
+ // A server that predates the field sends nothing, which reads the
240
+ // same way as a version pushed without one.
241
+ const documentSchemaModels = Array.isArray(active.documentSchemaModels)
242
+ ? active.documentSchemaModels
243
+ : null;
244
+ printResult(" Document schema", documentSchemaModels === null
245
+ ? "none — this version was pushed without one"
246
+ : documentSchemaModels.length > 0
247
+ ? documentSchemaModels.join(", ")
248
+ : "declares no models");
188
249
  }
189
250
  // #3181 — the ONE surface a function's triggers are visible on. The
190
251
  // hidden `AppWebhook` and `CronTrigger` rows are omitted from
@@ -197,17 +258,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
197
258
  const cronEntries = Array.isArray(triggers.cron)
198
259
  ? triggers.cron
199
260
  : [];
200
- // #3184 — the database types this function watches. A change trigger
201
- // has no standalone surface at all (there is no `database-triggers
202
- // list`), so this is the only place an operator can see that a write
203
- // to a type runs this function. A server that predates #3184 sends no
204
- // `databases` key, which reads as none.
205
- const databaseEntries = Array.isArray(triggers.databases)
206
- ? triggers.databases
207
- : [];
208
- if (!triggers.webhook &&
209
- cronEntries.length === 0 &&
210
- databaseEntries.length === 0) {
261
+ // #3483 — an OLDER server still answers a `databases` key, and this
262
+ // command ignores it rather than branching on it: the trigger it
263
+ // described was removed, so a section about it would tell an operator
264
+ // that a write runs this function when nothing does.
265
+ if (!triggers.webhook && cronEntries.length === 0) {
211
266
  printResult(" Triggers", "none — this function is invoked over HTTP only");
212
267
  }
213
268
  if (triggers.webhook) {
@@ -228,7 +283,6 @@ Configuration lives in functions/<key>.toml, and the code beside it:
228
283
  // #3454 — which runner this schedule's fires use. Always one of
229
284
  // the two words: an `any` function's entry names it, and a
230
285
  // locked one's takes its lock.
231
- { header: "RUNNER", key: "runner" },
232
286
  { header: "TZ", key: "timezone" },
233
287
  { header: "STATUS", key: "status", format: formatStatus },
234
288
  { header: "NEXT FIRE", key: "nextFireAt", format: formatDate },
@@ -236,15 +290,6 @@ Configuration lives in functions/<key>.toml, and the code beside it:
236
290
  { header: "LAST RUN", key: "lastTriggeredRunId" },
237
291
  ]));
238
292
  }
239
- if (databaseEntries.length > 0) {
240
- printResult(" Database changes", "a committed write to one of these types runs this function");
241
- // A fire carries the rows of every change of the type (the intent's
242
- // 2026-09-09 decision), so the row says which type and no more —
243
- // the READ GRANTS column retired with the grant grammar (#3279).
244
- console.log(formatTable(databaseEntries, [
245
- { header: "DATABASE TYPE", key: "databaseType" },
246
- ]));
247
- }
248
293
  // #3187 / #3279 — the ACTIVE version's manifest: what it registered
249
294
  // and what it TOUCHES, from the push-time collection. Documentation,
250
295
  // never authorization — function code acts as the system, and this
@@ -307,6 +352,124 @@ Configuration lives in functions/<key>.toml, and the code beside it:
307
352
  process.exit(1);
308
353
  }
309
354
  });
355
+ // ── #3462: a function's versions, and pointing it back at one ─────────
356
+ //
357
+ // Every changing push mints an immutable version and repoints the function at
358
+ // it. That was already true and already stored — and entirely invisible: an
359
+ // operator could not list a function's versions, and a bad push was undone by
360
+ // editing the tree and pushing again, with `disable` as the only immediate
361
+ // stop.
362
+ //
363
+ // Both are OPERATIONAL verbs on a configuration group in `docs/cli-design.md`'s
364
+ // sense: they author nothing. A function's versions are nameless, immutable
365
+ // and not in TOML — unlike a prompt's `[[configs]]` blocks, whose `active`
366
+ // marker is why `prompts configs activate` was retired — so there is no TOML
367
+ // spelling of activation to prefer to a verb.
368
+ functions
369
+ .command("configs")
370
+ .description("List a server function's versions, newest first")
371
+ .argument("<function-id>", "Function ID")
372
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
373
+ .option("--limit <n>", "Rows per page (default 25, max 100)")
374
+ .option("--cursor <cursor>", "Continue from a previous page")
375
+ .option("--json", "Output as JSON")
376
+ .action(async (functionId, options) => {
377
+ const resolvedAppId = resolveAppId(undefined, options);
378
+ const client = new ApiClient();
379
+ try {
380
+ // Always a `limit`, so this pages. The server's no-parameter form
381
+ // drains every version and exists for a CLI installed before paging
382
+ // did — this one is not it.
383
+ const { items, nextCursor } = await client.listFunctionConfigs(resolvedAppId, functionId, {
384
+ limit: options.limit ? Number(options.limit) : 25,
385
+ ...(options.cursor ? { cursor: options.cursor } : {}),
386
+ });
387
+ if (options.json) {
388
+ // Full hashes, not the table's twelve characters: a consumer
389
+ // comparing versions needs the value the server stored.
390
+ json({ items, hasMore: !!nextCursor, ...(nextCursor ? { nextCursor } : {}) });
391
+ return;
392
+ }
393
+ if (!items || items.length === 0) {
394
+ info("No versions pushed yet.");
395
+ return;
396
+ }
397
+ console.log(formatTable(functionVersionRows(items), [
398
+ { header: "CONFIG ID", key: "configId" },
399
+ { header: "PUSHED", key: "createdAt", format: formatDate },
400
+ { header: "ACTIVE", key: "active" },
401
+ { header: "CONTENT", key: "content" },
402
+ { header: "ENVELOPE", key: "envelope" },
403
+ { header: "TRIGGERS", key: "triggers" },
404
+ { header: "BY", key: "by" },
405
+ ]));
406
+ if (nextCursor) {
407
+ info(`More versions: --cursor ${nextCursor}`);
408
+ }
409
+ }
410
+ catch (err) {
411
+ error(err.message);
412
+ process.exit(1);
413
+ }
414
+ });
415
+ functions
416
+ .command("activate")
417
+ .description("Point a server function at one of its own existing versions (roll back or forward)")
418
+ .argument("<function-id>", "Function ID")
419
+ .argument("<config-id>", "The version to activate (`functions configs`)")
420
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
421
+ .option("-y, --yes", "Skip confirmation prompt")
422
+ .option("--json", "Output as JSON")
423
+ .addHelpText("after", `
424
+ What activating a version does, and does not:
425
+ Request calls and trigger fires use it from the NEXT call, under that
426
+ version's own capabilities, limits and triggers.
427
+ A task run already in flight finishes on the version it started on — the
428
+ content hash pins its code.
429
+ No version is created. \`config push\` of the tree is still what makes the
430
+ tree's code the newest version.
431
+ `)
432
+ .action(async (functionId, configId, options) => {
433
+ const resolvedAppId = resolveAppId(undefined, options);
434
+ if (!options.yes) {
435
+ let confirmed;
436
+ try {
437
+ confirmed = await confirmPrompt(activationPrompt(String(functionId), String(configId)));
438
+ }
439
+ catch (err) {
440
+ error(err.message);
441
+ process.exit(1);
442
+ }
443
+ if (!confirmed) {
444
+ info("Cancelled.");
445
+ return;
446
+ }
447
+ }
448
+ const client = new ApiClient();
449
+ try {
450
+ const answer = await client.activateFunctionConfig(resolvedAppId, functionId, configId);
451
+ if (options.json) {
452
+ json(answer);
453
+ return;
454
+ }
455
+ const rendered = activationOutput(answer, formatDate);
456
+ if (!rendered.changed) {
457
+ info(rendered.headline);
458
+ return;
459
+ }
460
+ success(rendered.headline);
461
+ for (const [label, value] of rendered.lines)
462
+ printResult(label, value);
463
+ }
464
+ catch (err) {
465
+ // The server's own sentence and code: an archived function, an archived
466
+ // version, a held delete lease and a pointer another writer moved each
467
+ // owe the operator a different next move, and only the code says which
468
+ // one without reading English.
469
+ error(activationRefusalLine(err));
470
+ process.exit(1);
471
+ }
472
+ });
310
473
  // #3181 — a trigger-fired invocation writes a run row, and a function run has
311
474
  // no `workflowId`, so `primitive workflows runs` cannot see it. This verb is
312
475
  // the only way to inspect what a webhook or a schedule actually did
@@ -327,8 +490,17 @@ Configuration lives in functions/<key>.toml, and the code beside it:
327
490
  ...(options.limit ? { limit: Number(options.limit) } : {}),
328
491
  ...(options.cursor ? { cursor: options.cursor } : {}),
329
492
  });
493
+ // #3482, C-6 — the runtime this run used, derived BEFORE the output is
494
+ // chosen so the table and `--json` say the same thing about the same
495
+ // row. `--json` takes the word or `null`, never the table's blank: a
496
+ // reader parsing this needs "no runtime" to be a value rather than an
497
+ // empty string that sorts beside real ones.
498
+ const withRuntime = (items ?? []).map((run) => ({
499
+ ...run,
500
+ runtime: runtimeForExecutionMode(run?.executionMode),
501
+ }));
330
502
  if (options.json) {
331
- json({ items, nextCursor });
503
+ json({ items: withRuntime, nextCursor });
332
504
  return;
333
505
  }
334
506
  if (!items || items.length === 0) {
@@ -336,18 +508,29 @@ Configuration lives in functions/<key>.toml, and the code beside it:
336
508
  return;
337
509
  }
338
510
  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).
511
+ // #3381, #3566 — flatten the slice record's two counts onto the
512
+ // row for the REFRESHES and RESETS columns. One helper decides
513
+ // both, so a row with no record gets a blank cell in each and a
514
+ // record with no count gets `0`.
342
515
  items.map((run) => ({
343
516
  ...run,
344
- refreshes: run.slice && Number.isFinite(run.slice.refreshCount)
345
- ? String(run.slice.refreshCount)
346
- : "",
517
+ // #3482 — which RUNTIME this run used, from the row's own
518
+ // `executionMode`. One pure mapping, so the word an operator
519
+ // reads here is the word `ctx.runtime` gave the code.
520
+ runtime: runRuntimeCell(run),
521
+ ...runSliceCells(run),
347
522
  })), [
348
523
  { header: "RUN ID", key: "runId" },
349
524
  { header: "STATUS", key: "status", format: formatStatus },
350
525
  { header: "FIRED BY", key: "initiatorKind" },
526
+ // #3482 — the runtime this run used: `request` for a webhook
527
+ // delivery or a `workflow.call` leaf, `task` for everything the
528
+ // engine ran. Blank for a DSL row, which is not a function run.
529
+ { header: "RUNTIME", key: "runtime" },
530
+ // #3462 — WHICH version this run executed. The row has carried
531
+ // `configId` since #3181; nothing showed it, so an operator
532
+ // reading a failed run could not tell which code failed.
533
+ { header: "VERSION", key: "configId" },
351
534
  // #3280 — FIRED BY already said `function`; this is the half that
352
535
  // makes it actionable, so a failed page leads back to the
353
536
  // orchestrator that started it. Blank for a root.
@@ -355,6 +538,11 @@ Configuration lives in functions/<key>.toml, and the code beside it:
355
538
  // #3381 — how many times a task slice's token was refreshed
356
539
  // through the gateway. Blank for a request run.
357
540
  { header: "REFRESHES", key: "refreshes" },
541
+ // #3566 — how many of this run's slices a platform deploy (or an
542
+ // isolate eviction) tore down. The engine re-runs the interrupted
543
+ // step, so a run that reset and then COMPLETED carries no failure
544
+ // and no error code: this cell is the only place it shows.
545
+ { header: "RESETS", key: "resets" },
358
546
  { header: "STARTED", key: "startedAt", format: formatDate },
359
547
  { header: "ENDED", key: "endedAt", format: formatDate },
360
548
  { header: "CODE", key: "errorCode" },
@@ -529,23 +717,13 @@ Configuration lives in functions/<key>.toml, and the code beside it:
529
717
  json(toFunctionLogInspectionRow(record));
530
718
  return;
531
719
  }
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
- ]));
720
+ console.log(formatTable([functionLogCells(record)], FUNCTION_LOG_COLUMNS.map((header) => ({
721
+ header,
722
+ key: LOG_COLUMN_KEYS[header],
723
+ ...(LOG_COLUMN_FORMATS[header]
724
+ ? { format: LOG_COLUMN_FORMATS[header] }
725
+ : {}),
726
+ }))));
549
727
  return;
550
728
  }
551
729
  if (options.follow) {
@@ -588,7 +766,7 @@ Configuration lives in functions/<key>.toml, and the code beside it:
588
766
  return;
589
767
  }
590
768
  for (const row of items)
591
- console.log(logRow(row));
769
+ console.log(functionLogLine(row));
592
770
  },
593
771
  });
594
772
  }
@@ -647,23 +825,39 @@ Configuration lives in functions/<key>.toml, and the code beside it:
647
825
  info(`More logs: --cursor ${exhaustedCursor}`);
648
826
  return;
649
827
  }
650
- console.log(formatTable(items.map((row) => ({
651
- startedAt: row.startedAt,
652
- status: row.status,
653
- trigger: row.trigger?.kind ?? "",
654
- // A trigger fire and a task run name their run; a request
655
- // invocation's key is its own id.
656
- runOrInvocation: row.runId || row.invocationId,
657
- errorCode: row.errorCode ?? "",
658
- firstError: firstErrorLine(row),
659
- })), [
660
- { header: "TIME", key: "startedAt", format: formatDate },
661
- { header: "STATUS", key: "status", format: formatStatus },
662
- { header: "TRIGGER", key: "trigger" },
663
- { header: "RUN/INVOCATION ID", key: "runOrInvocation" },
664
- { header: "CODE", key: "errorCode" },
665
- { header: "ERROR", key: "firstError" },
666
- ]));
828
+ if (options.run) {
829
+ // #3453 — reading ONE run, which is a different question from
830
+ // listing a function's invocations. A table of one row per record
831
+ // answers "which invocations were there"; what an operator opened
832
+ // `--run` for is what the run PRINTED, and until now that was only
833
+ // reachable through `--json`. So: the step trace first, then each
834
+ // record oldest first with its own lines under it.
835
+ let trace = null;
836
+ let traceError = null;
837
+ try {
838
+ const read = await client.getFunctionStepRuns(resolvedAppId, functionId, String(options.run));
839
+ trace = read?.items ?? [];
840
+ }
841
+ catch (traceErr) {
842
+ traceError = traceErr?.message ?? String(traceErr);
843
+ }
844
+ const rendered = renderRunLogs({ records: items, trace, traceError });
845
+ if (rendered.warning)
846
+ warn(rendered.warning);
847
+ for (const line of rendered.lines)
848
+ console.log(line);
849
+ if (nextCursor) {
850
+ info(`More logs: --cursor ${nextCursor}`);
851
+ }
852
+ return;
853
+ }
854
+ console.log(formatTable(items.map((row) => functionLogCells(row)), FUNCTION_LOG_COLUMNS.map((header) => ({
855
+ header,
856
+ key: LOG_COLUMN_KEYS[header],
857
+ ...(LOG_COLUMN_FORMATS[header]
858
+ ? { format: LOG_COLUMN_FORMATS[header] }
859
+ : {}),
860
+ }))));
667
861
  if (nextCursor) {
668
862
  info(`More logs: --cursor ${nextCursor}`);
669
863
  }
@@ -971,12 +1165,17 @@ Reclaiming the key means hard-deleting the row:
971
1165
  // Every decision these verbs make is in `cli/src/lib/function-run.ts`, where
972
1166
  // it is pinned without a server; what is here is the I/O and the rendering.
973
1167
  /**
974
- * Key → the function's id and the mode its ACTIVE version runs in.
1168
+ * Key → the function's id.
975
1169
  *
976
1170
  * `invoke` and `start` take the KEY, because that is the public route's
977
1171
  * argument and what a developer wrote in their TOML — while `runs`, `logs`
978
1172
  * and `steps` keep the function id they have always taken. So the key has to
979
1173
  * be resolved, and the drained admin listing is what `functions list` reads.
1174
+ *
1175
+ * #3482 — the ACTIVE VERSION is no longer read. It was point-read for one
1176
+ * reason: the mode pre-check, which refused the wrong verb before a round
1177
+ * trip. There is no wrong verb any more — the caller picks the runtime and
1178
+ * every function takes both — so the read and the pre-check go together.
980
1179
  */
981
1180
  async function resolveFunctionByKey(client, appId, functionKey) {
982
1181
  const { items } = await client.listFunctions(appId);
@@ -986,22 +1185,7 @@ Reclaiming the key means hard-deleting the row:
986
1185
  throw new Error(`No function with key '${functionKey}' in this app. ` +
987
1186
  `List them with 'primitive functions list'.`);
988
1187
  }
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 };
1188
+ return { functionId: String(match.functionId) };
1005
1189
  }
1006
1190
  /** The identity a call runs under, and the bearer it presents. */
1007
1191
  async function planIdentity(client, appId, identity) {
@@ -1058,15 +1242,9 @@ Reclaiming the key means hard-deleting the row:
1058
1242
  const client = new ApiClient();
1059
1243
  try {
1060
1244
  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
1245
  const identity = await planIdentity(client, resolvedAppId, flags.identity);
1067
1246
  const body = {
1068
1247
  rootInput: flags.input,
1069
- mode: modeFor("invoke"),
1070
1248
  ...(options.contextDocId ? { contextDocId: options.contextDocId } : {}),
1071
1249
  ...(flags.timeoutSeconds
1072
1250
  ? { timeoutMs: Math.round(flags.timeoutSeconds * 1000) }
@@ -1075,6 +1253,11 @@ Reclaiming the key means hard-deleting the row:
1075
1253
  const answer = await runAs(client, resolvedAppId, flags.identity, "invoke", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
1076
1254
  ...(bearer ? { bearer } : {}),
1077
1255
  asSystem: identity.asSystem,
1256
+ // #3482 — the ROUTE says which runtime this call means. The
1257
+ // admin-only system door is one route for both, so the body still
1258
+ // names it there and only there.
1259
+ runtime: "request",
1260
+ ...(identity.asSystem ? {} : {}),
1078
1261
  }));
1079
1262
  const envelope = answer.body ?? {};
1080
1263
  if (options.json) {
@@ -1152,11 +1335,6 @@ Reclaiming the key means hard-deleting the row:
1152
1335
  const client = new ApiClient();
1153
1336
  try {
1154
1337
  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
1338
  const identity = await planIdentity(client, resolvedAppId, flags.identity);
1161
1339
  // D3448-014 — the task path defaults the context to the acting user's
1162
1340
  // `AppUser.rootDocId`, and the app user provisioned for an admin has
@@ -1173,13 +1351,17 @@ Reclaiming the key means hard-deleting the row:
1173
1351
  }
1174
1352
  const body = {
1175
1353
  rootInput: flags.input,
1176
- mode: modeFor("start"),
1354
+ // #3482 — `--as system` is the ONE door that is a single route for
1355
+ // both runtimes, so it keeps the deprecated body selector; every
1356
+ // other call says it with the route.
1357
+ ...(identity.asSystem ? { mode: "task" } : {}),
1177
1358
  ...(contextDocId ? { contextDocId } : {}),
1178
1359
  ...(options.runKey ? { runKey: options.runKey } : {}),
1179
1360
  };
1180
1361
  const answer = await runAs(client, resolvedAppId, flags.identity, "start", functionKey, (bearer) => client.invokeFunction(resolvedAppId, functionKey, body, {
1181
1362
  ...(bearer ? { bearer } : {}),
1182
1363
  asSystem: identity.asSystem,
1364
+ runtime: "task",
1183
1365
  }));
1184
1366
  const envelope = answer.body ?? {};
1185
1367
  if (answer.httpStatus >= 400) {