okengine 0.20.0 → 0.21.1

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 (104) hide show
  1. package/package.json +3 -7
  2. package/site/content/docs/client/calling.mdx +117 -29
  3. package/site/content/docs/client/index.mdx +3 -3
  4. package/site/content/docs/elements/flow/http.mdx +18 -15
  5. package/site/content/docs/elements/flow/index.mdx +26 -18
  6. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  7. package/site/content/docs/elements/store/files.mdx +11 -5
  8. package/site/content/docs/elements/store/index.mdx +2 -3
  9. package/site/content/docs/elements/store/kv.mdx +8 -2
  10. package/site/content/docs/elements/store/sql.mdx +9 -6
  11. package/site/content/docs/elements/vault/index.mdx +11 -8
  12. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/recipes/rustfs.mdx +1 -1
  15. package/site/content/docs/reference/configuration.mdx +1 -1
  16. package/site/content/docs/reference/errors.mdx +199 -25
  17. package/site/content/docs/reference/fx.mdx +6 -1
  18. package/site/content/docs/understand/the-architecture.mdx +2 -2
  19. package/site/content/docs/understand/try-it.mdx +758 -25
  20. package/src/cli/dev-app-runner.ts +2 -1
  21. package/src/cli/dev.test.ts +105 -2
  22. package/src/cli/dev.ts +37 -2
  23. package/src/cli/start.ts +2 -1
  24. package/src/client/create.ts +14 -27
  25. package/src/client/explain.test.ts +252 -0
  26. package/src/client/explain.ts +272 -0
  27. package/src/client/live.test.ts +44 -0
  28. package/src/client/live.ts +44 -101
  29. package/src/client/notes-contract.test.ts +10 -0
  30. package/src/client/sse.ts +26 -68
  31. package/src/client/stream.ts +25 -67
  32. package/src/client/transport.test.ts +67 -0
  33. package/src/client/transport.ts +51 -112
  34. package/src/client/types.ts +17 -6
  35. package/src/client/wire.ts +119 -0
  36. package/src/client-react/live-resource.ts +6 -2
  37. package/src/compiler/aot.ts +3 -32
  38. package/src/compiler/dynamic.ts +13 -11
  39. package/src/compiler/interpret.ts +45 -0
  40. package/src/compiler/response.ts +17 -27
  41. package/src/console/server/invoke-user-flow.test.ts +8 -2
  42. package/src/console/server/invoke-user-flow.ts +12 -18
  43. package/src/console/server/security.gate.test.ts +1 -1
  44. package/src/console/ui-next/dist/assets/{access-page-DFLu0wTA.js → access-page-Bgt9bq2r.js} +1 -1
  45. package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-B43CXZZR.js} +1 -1
  46. package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-92uM5MO7.js} +1 -1
  47. package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-DtGVPtcs.js} +1 -1
  48. package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DN7l6zmC.js} +1 -1
  49. package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-BmIR9FV8.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-BMHs-IzK.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-BlAEVgNW.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BDf7OAHv.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-BKpaes3n.js} +3 -3
  54. package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-WnVLI-0j.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-B3GLNVfF.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-DMbnAe3f.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-KvFDingJ.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
  59. package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-C4NdNuxP.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-CBYl0LW_.js} +1 -1
  62. package/src/console/ui-next/dist/index.html +1 -1
  63. package/src/docker/docker.test.ts +3 -3
  64. package/src/docker/images-config.test.ts +4 -4
  65. package/src/docker/stack-id.test.ts +1 -1
  66. package/src/elements/store/files-errors.test.ts +149 -0
  67. package/src/elements/store/files-errors.ts +189 -0
  68. package/src/elements/store/kv-errors.test.ts +98 -0
  69. package/src/elements/store/kv-errors.ts +139 -0
  70. package/src/elements/store/resource.ts +11 -7
  71. package/src/elements/store/runtime.ts +18 -13
  72. package/src/elements/store/sql-errors.test.ts +197 -0
  73. package/src/elements/store/sql-errors.ts +294 -0
  74. package/src/elements/store/sql-session.test.ts +52 -0
  75. package/src/elements/store/sql-session.ts +26 -4
  76. package/src/elements/store/store-errors.ts +47 -0
  77. package/src/http.ts +9 -1
  78. package/src/i18n/catalogs/ar.ts +18 -0
  79. package/src/i18n/catalogs/en.ts +18 -0
  80. package/src/index.ts +9 -1
  81. package/src/kernel/app.ts +23 -4
  82. package/src/kernel/builtin-errors.test.ts +117 -0
  83. package/src/kernel/builtin-errors.ts +129 -0
  84. package/src/kernel/call.test.ts +182 -0
  85. package/src/kernel/client-descriptor.test.ts +78 -0
  86. package/src/kernel/client-descriptor.ts +23 -0
  87. package/src/kernel/errors-text.ts +99 -0
  88. package/src/kernel/errors-vault.ts +16 -0
  89. package/src/kernel/errors.registry.test.ts +7 -0
  90. package/src/kernel/errors.ts +184 -159
  91. package/src/kernel/fail-helpers.ts +34 -0
  92. package/src/kernel/fx-sql-handle.ts +305 -0
  93. package/src/kernel/fx.test.ts +8 -0
  94. package/src/kernel/fx.ts +49 -335
  95. package/src/kernel/index.ts +12 -1
  96. package/src/kernel/json-result.ts +59 -0
  97. package/src/kernel/project-out.ts +6 -1
  98. package/src/okid-extended.ts +175 -0
  99. package/src/okid-shared.ts +103 -0
  100. package/src/okid.ts +30 -213
  101. package/src/release/build-lib.ts +9 -0
  102. package/src/runtime/dev-request-log.ts +29 -11
  103. package/src/term.test.ts +76 -0
  104. package/src/term.ts +166 -3
package/src/term.test.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  formatOkeWordmark,
17
17
  formatServiceLine,
18
18
  formatSurfaceBox,
19
+ formatFatalError,
19
20
  formatRequestLine,
20
21
  formatStackSummary,
21
22
  countTermLines,
@@ -296,6 +297,81 @@ describe("term", () => {
296
297
  expect(out).not.toContain("↳");
297
298
  });
298
299
 
300
+ test("formatRequestLine title chip includes error.code", () => {
301
+ const out = formatRequestLine({
302
+ surface: "Backend",
303
+ method: "POST",
304
+ path: "/links",
305
+ flow: "links.create",
306
+ status: 409,
307
+ ms: 3,
308
+ detail: "That value is already in use.",
309
+ errorCode: "Conflict",
310
+ color: false,
311
+ });
312
+ expect(out).toMatch(/◇ {2}409 Conflict\n/);
313
+ });
314
+
315
+ test("formatFatalError prints VaultBootError as OKE1510 with cause and fix", () => {
316
+ const err = Object.assign(new Error("vault boot failed"), {
317
+ name: "VaultBootError",
318
+ gaps: [{ name: "STRIPE_SECRET_KEY" }, { name: "DATABASE_URL" }],
319
+ });
320
+ const out = formatFatalError(err, false);
321
+ expect(out).toContain("OKE1510");
322
+ expect(out).toContain("2 secrets have no value in any resolution layer.");
323
+ expect(out).toContain("- STRIPE_SECRET_KEY");
324
+ expect(out).toContain("oke vault set");
325
+ expect(out).toContain("https://oke.omqkhafi.dev/e/1510");
326
+ expect(out).not.toContain("Error:");
327
+ expect(out).not.toContain(" at ");
328
+ });
329
+
330
+ test("formatFatalError prints GateBootError with posture gaps", () => {
331
+ const err = Object.assign(new Error("gate boot failed"), {
332
+ name: "GateBootError",
333
+ gaps: [{ flowId: "notes.get", method: "GET", path: "/notes/:id" }],
334
+ });
335
+ const out = formatFatalError(err, false);
336
+ expect(out).toContain("Gate");
337
+ expect(out).toContain("1 trigger(s) missing auth posture.");
338
+ expect(out).toContain("- notes.get GET /notes/:id");
339
+ expect(out).toContain("Attach a gate or `.public()`.");
340
+ expect(out).not.toContain(" at ");
341
+ });
342
+
343
+ test("formatFatalError prints PluginNeedsError with unmet needs", () => {
344
+ const err = Object.assign(new Error("plugin needs failed"), {
345
+ name: "PluginNeedsError",
346
+ gaps: [{ plugin: "auth", need: "store.sql" }],
347
+ });
348
+ const out = formatFatalError(err, false);
349
+ expect(out).toContain("Plugin");
350
+ expect(out).toContain("1 unmet .needs() dependency.");
351
+ expect(out).toContain('- auth needs "store.sql"');
352
+ expect(out).toContain("Install the missing plugin");
353
+ expect(out).not.toContain(" at ");
354
+ });
355
+
356
+ test("formatFatalError hides stack unless OKE_DEBUG=1", () => {
357
+ const prev = process.env["OKE_DEBUG"];
358
+ const boom = new Error("plain boom");
359
+ try {
360
+ delete process.env["OKE_DEBUG"];
361
+ const hidden = formatFatalError(boom, false);
362
+ expect(hidden).toContain("Error");
363
+ expect(hidden).toContain("plain boom");
364
+ expect(hidden).not.toContain(" at ");
365
+
366
+ process.env["OKE_DEBUG"] = "1";
367
+ const shown = formatFatalError(boom, false);
368
+ expect(shown).toContain("Error");
369
+ } finally {
370
+ if (prev === undefined) delete process.env["OKE_DEBUG"];
371
+ else process.env["OKE_DEBUG"] = prev;
372
+ }
373
+ });
374
+
299
375
  test("formatStackSummary is scannable", () => {
300
376
  const out = formatStackSummary({
301
377
  project: "oke-dev-a3f791",
package/src/term.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  * Flip to monochrome with `NO_COLOR=1` or non-TTY stdout.
6
6
  */
7
7
 
8
+ import { docsUrl } from "./docs-origin.ts";
9
+ import { lookupOkeError } from "./kernel/errors.ts";
10
+
8
11
  /** Whether ANSI color should be applied. */
9
12
  export function termColorEnabled(stream: { readonly isTTY?: boolean } = process.stdout): boolean {
10
13
  if (process.env["NO_COLOR"] !== undefined) return false;
@@ -701,15 +704,171 @@ export function formatDevNote(options: {
701
704
  const ink = options.tone === "error" ? s.red : s.yellow;
702
705
  const bar = `${s.dim}│${s.reset}`;
703
706
  const width = Math.max(40, Math.min(72, (process.stdout.columns ?? 80) - 6));
704
- const wrapped = wrapWords(options.body.trim(), width);
707
+ const wrapped = wrapBodyLines(options.body.trimEnd(), width);
705
708
  const lines = [`${ink}◇${s.reset} ${s.bold}${ink}${options.title}${s.reset}`, bar];
706
709
  for (const line of wrapped) {
707
- lines.push(`${bar} ${ink}${line}${s.reset}`);
710
+ lines.push(line.length === 0 ? bar : `${bar} ${ink}${line}${s.reset}`);
708
711
  }
709
712
  lines.push(bar);
710
713
  return `${lines.join("\n")}\n`;
711
714
  }
712
715
 
716
+ function wrapBodyLines(body: string, width: number): string[] {
717
+ const out: string[] = [];
718
+ for (const raw of body.split("\n")) {
719
+ const line = raw.trimEnd();
720
+ if (line.length === 0) {
721
+ out.push("");
722
+ continue;
723
+ }
724
+ out.push(...wrapWords(line, width));
725
+ }
726
+ return out;
727
+ }
728
+
729
+ /**
730
+ * Boot / start fatal — cause, optional list, fix, docs URL. No stack unless
731
+ * `OKE_DEBUG=1`.
732
+ *
733
+ * @param err - Thrown boot value
734
+ * @param color - Color on/off
735
+ */
736
+ export function formatFatalError(err: unknown, color: boolean = termColorEnabled()): string {
737
+ const debug = process.env["OKE_DEBUG"] === "1";
738
+ const formatted = describeFatalError(err, debug);
739
+ return formatDevNote({
740
+ title: formatted.title,
741
+ body: formatted.body,
742
+ tone: "error",
743
+ color,
744
+ });
745
+ }
746
+
747
+ function describeFatalError(
748
+ err: unknown,
749
+ debug: boolean,
750
+ ): { readonly title: string; readonly body: string } {
751
+ const vault = vaultBootGaps(err);
752
+ if (vault) {
753
+ const def = lookupOkeError(1510);
754
+ const count = String(vault.length);
755
+ const cause = def
756
+ ? interpolateTemplate(def.cause, { count })
757
+ : `${count} secrets have no value in any resolution layer.`;
758
+ const fix = def
759
+ ? interpolateTemplate(def.fix, { count })
760
+ : "Set each name (`oke vault set <name>`, or `.env.local`).";
761
+ const url = def ? docsUrl(`/e/${def.code}`) : docsUrl("/e/1510");
762
+ const names = vault.map((g) =>
763
+ g.description ? `- ${g.name}: ${g.description}` : `- ${g.name}`,
764
+ );
765
+ return {
766
+ title: "OKE1510",
767
+ body: [cause, ...names, `→ ${fix}`, url].join("\n"),
768
+ };
769
+ }
770
+
771
+ const gate = namedGaps(err, "GateBootError");
772
+ if (gate) {
773
+ const lines = gate.map((g) => {
774
+ const rec = g as Record<string, unknown>;
775
+ const flow = typeof rec.flowId === "string" ? rec.flowId : "flow";
776
+ const method = typeof rec.method === "string" ? rec.method : "";
777
+ const path = typeof rec.path === "string" ? rec.path : "";
778
+ return `- ${flow} ${method} ${path}`.trim();
779
+ });
780
+ return {
781
+ title: "Gate",
782
+ body: [
783
+ `${gate.length} trigger(s) missing auth posture.`,
784
+ ...lines,
785
+ "→ Attach a gate or `.public()`.",
786
+ ].join("\n"),
787
+ };
788
+ }
789
+
790
+ const plugin = namedGaps(err, "PluginNeedsError");
791
+ if (plugin) {
792
+ const lines = plugin.map((g) => {
793
+ const rec = g as Record<string, unknown>;
794
+ const name = typeof rec.plugin === "string" ? rec.plugin : "plugin";
795
+ const need = typeof rec.need === "string" ? rec.need : "";
796
+ return `- ${name} needs "${need}"`;
797
+ });
798
+ return {
799
+ title: "Plugin",
800
+ body: [
801
+ `${plugin.length} unmet .needs() dependenc${plugin.length === 1 ? "y" : "ies"}.`,
802
+ ...lines,
803
+ "→ Install the missing plugin or bind the element / driver.",
804
+ ].join("\n"),
805
+ };
806
+ }
807
+
808
+ if (isOkeErrorShape(err)) {
809
+ return {
810
+ title: `OKE${err.code}`,
811
+ body: [err.causeText, `→ ${err.fix}`, err.docsUrl].join("\n"),
812
+ };
813
+ }
814
+
815
+ const message = err instanceof Error ? err.message : String(err);
816
+ const stack = debug && err instanceof Error && err.stack ? `\n${err.stack}` : "";
817
+ return { title: "Error", body: `${message}${stack}` };
818
+ }
819
+
820
+ function interpolateTemplate(template: string, params: Readonly<Record<string, string>>): string {
821
+ return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key: string) => params[key] ?? `{${key}}`);
822
+ }
823
+
824
+ function vaultBootGaps(
825
+ err: unknown,
826
+ ): readonly { readonly name: string; readonly description?: string }[] | undefined {
827
+ if (!err || typeof err !== "object") return undefined;
828
+ if ((err as { name?: unknown }).name !== "VaultBootError") return undefined;
829
+ const gaps = (err as { gaps?: unknown }).gaps;
830
+ if (!Array.isArray(gaps)) return undefined;
831
+ const out: { name: string; description?: string }[] = [];
832
+ for (const g of gaps) {
833
+ if (!g || typeof g !== "object") continue;
834
+ const name = (g as { name?: unknown }).name;
835
+ if (typeof name !== "string") continue;
836
+ const description = (g as { description?: unknown }).description;
837
+ out.push({
838
+ name,
839
+ ...(typeof description === "string" ? { description } : {}),
840
+ });
841
+ }
842
+ return out;
843
+ }
844
+
845
+ function namedGaps(err: unknown, name: string): readonly unknown[] | undefined {
846
+ if (!err || typeof err !== "object") return undefined;
847
+ if ((err as { name?: unknown }).name !== name) return undefined;
848
+ const gaps = (err as { gaps?: unknown }).gaps;
849
+ return Array.isArray(gaps) ? gaps : undefined;
850
+ }
851
+
852
+ function isOkeErrorShape(
853
+ err: unknown,
854
+ ): err is { code: number; causeText: string; fix: string; docsUrl: string } {
855
+ if (!err || typeof err !== "object") return false;
856
+ const rec = err as {
857
+ name?: unknown;
858
+ code?: unknown;
859
+ causeText?: unknown;
860
+ fix?: unknown;
861
+ docsUrl?: unknown;
862
+ };
863
+ return (
864
+ rec.name === "OkeError" &&
865
+ typeof rec.code === "number" &&
866
+ typeof rec.causeText === "string" &&
867
+ typeof rec.fix === "string" &&
868
+ typeof rec.docsUrl === "string"
869
+ );
870
+ }
871
+
713
872
  /**
714
873
  * Boot honesty notice — Clack-like column under the `oke dev` hero.
715
874
  *
@@ -808,6 +967,8 @@ export function formatRequestLine(options: {
808
967
  readonly color?: boolean;
809
968
  /** Failure detail printed on a follow-up line (4xx/5xx). */
810
969
  readonly detail?: string;
970
+ /** Failure `error.code` for the title chip (`409 Conflict`). */
971
+ readonly errorCode?: string;
811
972
  /** WideEvent / run id when the request executed a flow. */
812
973
  readonly runId?: string;
813
974
  }): string {
@@ -849,8 +1010,10 @@ export function formatRequestLine(options: {
849
1010
  const detail = options.detail?.trim();
850
1011
  if (!detail || options.status < 400) return main;
851
1012
  const tone: DevNoteTone = options.status >= 500 ? "error" : "warn";
1013
+ const code = options.errorCode?.trim();
1014
+ const title = code && code.length > 0 ? `${options.status} ${code}` : String(options.status);
852
1015
  const note = formatDevNote({
853
- title: String(options.status),
1016
+ title,
854
1017
  body: detail,
855
1018
  tone,
856
1019
  color: options.color ?? termColorEnabled(),