@nanobpm/urban 0.21.0 → 0.23.0

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 (97) hide show
  1. package/README.md +35 -0
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.js +80 -16
  4. package/dist/runtime/adapters/deno.js +19 -3
  5. package/dist/runtime/adapters/detect.js +2 -2
  6. package/dist/runtime/adapters/globals.d.ts +19 -0
  7. package/dist/runtime/adapters/globals.js +9 -0
  8. package/dist/runtime/adapters/node.js +25 -5
  9. package/dist/runtime/core/context.d.ts +4 -0
  10. package/dist/runtime/core/guards.d.ts +2 -0
  11. package/dist/runtime/core/guards.js +10 -0
  12. package/dist/runtime/core/host.js +5 -4
  13. package/dist/runtime/core/manifest.d.ts +8 -0
  14. package/dist/runtime/core/manifest.js +0 -1
  15. package/dist/runtime/core/modules/actions.js +9 -5
  16. package/dist/runtime/core/modules/connectors.js +6 -4
  17. package/dist/runtime/core/modules/dataops.d.ts +36 -0
  18. package/dist/runtime/core/modules/dataops.js +231 -0
  19. package/dist/runtime/core/modules/datasource.d.ts +51 -1
  20. package/dist/runtime/core/modules/datasource.js +98 -21
  21. package/dist/runtime/core/modules/deploy.d.ts +3 -1
  22. package/dist/runtime/core/modules/deploy.js +29 -7
  23. package/dist/runtime/core/modules/gateway.d.ts +1 -1
  24. package/dist/runtime/core/modules/gateway.js +16 -17
  25. package/dist/runtime/core/modules/llm.js +9 -7
  26. package/dist/runtime/core/modules/pages.js +118 -16
  27. package/dist/runtime/core/modules/security.js +5 -5
  28. package/dist/runtime/core/modules/templates.d.ts +18 -0
  29. package/dist/runtime/core/modules/templates.js +133 -0
  30. package/dist/runtime/core/modules/triggers.js +14 -5
  31. package/dist/runtime/core/modules/workers.js +15 -9
  32. package/dist/runtime/core/runtime.d.ts +5 -0
  33. package/dist/runtime/core/runtime.js +5 -5
  34. package/dist/runtime/core/validate.d.ts +2 -1
  35. package/dist/runtime/core/validate.js +38 -28
  36. package/dist/runtime/devserver.js +2 -1
  37. package/dist/runtime/engine/nanosdk.d.ts +1 -1
  38. package/dist/runtime/engine/nanosdk.js +15 -9
  39. package/dist/runtime/index.d.ts +5 -0
  40. package/dist/runtime/index.js +4 -0
  41. package/dist/runtime/run.js +10 -7
  42. package/dist/toolkit/addConnector.js +4 -3
  43. package/dist/toolkit/fsio.js +2 -1
  44. package/dist/toolkit/models.js +3 -5
  45. package/package.json +1 -1
  46. package/src/cli.test.ts +44 -0
  47. package/src/cli.ts +82 -16
  48. package/src/runtime/adapters/deno.ts +23 -3
  49. package/src/runtime/adapters/detect.ts +2 -2
  50. package/src/runtime/adapters/globals.ts +27 -0
  51. package/src/runtime/adapters/node.ts +27 -4
  52. package/src/runtime/connector-worker-sdk.test.ts +2 -1
  53. package/src/runtime/core/context.ts +4 -0
  54. package/src/runtime/core/guards.ts +10 -0
  55. package/src/runtime/core/host.ts +4 -4
  56. package/src/runtime/core/manifest.ts +19 -5
  57. package/src/runtime/core/modules/actions.test.ts +82 -31
  58. package/src/runtime/core/modules/actions.ts +11 -7
  59. package/src/runtime/core/modules/connectors.test.ts +29 -8
  60. package/src/runtime/core/modules/connectors.ts +6 -4
  61. package/src/runtime/core/modules/dataops.test.ts +306 -0
  62. package/src/runtime/core/modules/dataops.ts +292 -0
  63. package/src/runtime/core/modules/datasource.migrations.test.ts +12 -3
  64. package/src/runtime/core/modules/datasource.test.ts +129 -0
  65. package/src/runtime/core/modules/datasource.ts +105 -23
  66. package/src/runtime/core/modules/deploy.test.ts +258 -0
  67. package/src/runtime/core/modules/deploy.ts +29 -7
  68. package/src/runtime/core/modules/gateway.test.ts +5 -4
  69. package/src/runtime/core/modules/gateway.ts +29 -30
  70. package/src/runtime/core/modules/llm.test.ts +10 -12
  71. package/src/runtime/core/modules/llm.ts +11 -9
  72. package/src/runtime/core/modules/pages.test.ts +53 -2
  73. package/src/runtime/core/modules/pages.ts +121 -23
  74. package/src/runtime/core/modules/security.ts +5 -13
  75. package/src/runtime/core/modules/surfaces.test.ts +53 -10
  76. package/src/runtime/core/modules/templates.ts +153 -0
  77. package/src/runtime/core/modules/triggers.test.ts +51 -19
  78. package/src/runtime/core/modules/triggers.ts +14 -6
  79. package/src/runtime/core/modules/workers.test.ts +43 -20
  80. package/src/runtime/core/modules/workers.ts +18 -10
  81. package/src/runtime/core/runtime.test.ts +23 -10
  82. package/src/runtime/core/runtime.ts +15 -2
  83. package/src/runtime/core/validate.ts +43 -32
  84. package/src/runtime/devserver.test.ts +18 -5
  85. package/src/runtime/devserver.ts +2 -1
  86. package/src/runtime/engine/nanosdk.test.ts +16 -11
  87. package/src/runtime/engine/nanosdk.ts +22 -15
  88. package/src/runtime/index.ts +7 -0
  89. package/src/runtime/run.ts +10 -10
  90. package/src/toolkit/addConnector.ts +6 -5
  91. package/src/toolkit/derivers/messages.test.ts +4 -2
  92. package/src/toolkit/derivers/worker-io.test.ts +4 -2
  93. package/src/toolkit/fsio.ts +2 -1
  94. package/src/toolkit/gen.ts +4 -4
  95. package/src/toolkit/models.test.ts +1 -1
  96. package/src/toolkit/models.ts +3 -4
  97. package/src/toolkit/scaffold.ts +1 -1
package/README.md CHANGED
@@ -141,6 +141,41 @@ wiring; `WorkflowClient` deploys and starts, `Worker` hosts your `run` steps.
141
141
  inspectable in a modeller/Operate — `@nanobpm/urban` bundles `bpmn-auto-layout`
142
142
  so this works out of the box.
143
143
 
144
+ ### Deploy-time model templates (`{{name}}`)
145
+
146
+ A model file can carry `{{name}}` placeholders that are substituted at deploy
147
+ time — handy for inlining a large asset (e.g. an agent prompt authored as its own
148
+ file) into a `zeebe:header` value instead of hand-pasting it into XML or shipping
149
+ it as a bulky per-instance process variable. Declare the template sources under
150
+ `models.templates` (globs, a bare directory that is scanned, or literal files); a
151
+ template's name is its file **stem**, so `prompts/review.md` fills `{{review}}`:
152
+
153
+ ```jsonc
154
+ {
155
+ "models": {
156
+ "processes": ["processes/*.bpmn"],
157
+ "templates": ["prompts/*.md"]
158
+ }
159
+ }
160
+ ```
161
+
162
+ ```xml
163
+ <!-- processes/agent.bpmn -->
164
+ <zeebe:header key="io.nanobpm.agentTask.task.prompt" value="{{review}}" />
165
+ ```
166
+
167
+ Content is escaped for the resource's type: XML for `.bpmn`/`.dmn` (newlines/tabs
168
+ become character references so a multi-line prompt survives XML attribute-value
169
+ normalization), JSON for `.form`. Substitution is single-pass (a template's own
170
+ `{{…}}` is not re-expanded); an unknown placeholder is left verbatim and logged as
171
+ a warning. Templates can also be supplied programmatically via the `templates`
172
+ run option (globs or an explicit `name → content` map), which wins over the
173
+ manifest on a name collision:
174
+
175
+ ```ts
176
+ await runFromEnv({ templates: { review: await readPrompt("review.md") } });
177
+ ```
178
+
144
179
  ### Triggers — the inbound I/O edge
145
180
 
146
181
  Declare `triggers[]` in the app manifest to turn outside events into engine
package/dist/cli.d.ts CHANGED
@@ -1 +1,27 @@
1
+ interface Flags {
2
+ root: string;
3
+ manifest: string;
4
+ port?: number;
5
+ check: boolean;
6
+ write: boolean;
7
+ deno: boolean;
8
+ models: boolean;
9
+ stdout: boolean;
10
+ install: boolean;
11
+ style?: "model" | "code";
12
+ help: boolean;
13
+ version: boolean;
14
+ _: string[];
15
+ }
16
+ /**
17
+ * `urban data` — the DB-manager op gateway. Reads a single JSON request from stdin
18
+ * (`{ op, source?, sql?, params?, statements? }`) and writes a single JSON response to stdout
19
+ * (`{ ok: true, ...result }` or `{ ok: false, error }`). Every response — a handled op error *or*
20
+ * an unreadable request — is a parseable `{ ok: false, error }` envelope and still exits 0, so the
21
+ * caller can always parse the reply off stdout and never has to treat a non-zero exit as a lost
22
+ * error; the process only exits non-zero on an unexpected crash. This is the seam the Nano console
23
+ * re-points its Data panel at (host dry-out nano-bpm#576), replacing the vendored data-cli.ts.
24
+ */
25
+ export declare function cmdData(f: Flags, readInput?: () => Promise<string>): Promise<number>;
1
26
  export declare function main(argv: string[]): Promise<number>;
27
+ export {};
package/dist/cli.js CHANGED
@@ -7,10 +7,13 @@
7
7
  // urban run materialize + serve the app (deploy, data, workers, surfaces, triggers)
8
8
  // urban dev like run, plus watch sources and hot-reload on change
9
9
  // urban deploy deploy models only, then exit
10
+ // urban data DB-manager op gateway (stdin JSON → stdout JSON)
10
11
  //
11
12
  // Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
12
13
  // --port <n>, -h/--help, -v/--version.
13
- import { collectManifestIssues, installSignalHandlers, loadManifest, runDev, runFromEnv, selectHost, } from "./runtime/index.js";
14
+ import { collectManifestIssues, installSignalHandlers, loadManifest, runDataOp, runDev, runFromEnv, selectHost, } from "./runtime/index.js";
15
+ import { denoGlobal, processGlobal } from "./runtime/adapters/globals.js";
16
+ import { errorMessage, isRecord } from "./runtime/core/guards.js";
14
17
  import { scaffold, slugify } from "create-urban-app";
15
18
  import { addConnector, createNodeGenIO, previewModels, runGen, scaffoldWorkers } from "./toolkit/index.js";
16
19
  import { pathToFileURL } from "node:url";
@@ -21,7 +24,8 @@ import { spawnSync } from "node:child_process";
21
24
  function readVersion() {
22
25
  try {
23
26
  const url = new URL("../package.json", import.meta.url);
24
- return JSON.parse(readFileSync(url, "utf8")).version ?? "0.0.0";
27
+ const pkg = JSON.parse(readFileSync(url, "utf8"));
28
+ return isRecord(pkg) && typeof pkg.version === "string" ? pkg.version : "0.0.0";
25
29
  }
26
30
  catch {
27
31
  return "0.0.0";
@@ -102,6 +106,9 @@ Usage:
102
106
  urban run materialize + serve the app
103
107
  urban dev run + watch sources, hot-reload on change
104
108
  urban deploy deploy models only, then exit
109
+ urban data DB-manager op gateway: read a JSON request on stdin
110
+ ({op:sources|schema|query|exec|script|migrations|migrate,…}),
111
+ write a JSON result on stdout
105
112
 
106
113
  Global flags:
107
114
  --root <dir> app root (default ".")
@@ -257,6 +264,60 @@ async function cmdDeploy(f) {
257
264
  await app.stop();
258
265
  return 0;
259
266
  }
267
+ /** Read all of stdin as UTF-8 text (Node or Deno). Returns "" if stdin is empty/closed. */
268
+ async function readStdin() {
269
+ const deno = denoGlobal();
270
+ const proc = processGlobal();
271
+ const chunks = [];
272
+ const dec = new TextDecoder();
273
+ if (deno?.stdin?.readable) {
274
+ for await (const chunk of deno.stdin.readable)
275
+ chunks.push(dec.decode(chunk, { stream: true }));
276
+ chunks.push(dec.decode());
277
+ return chunks.join("");
278
+ }
279
+ if (proc?.stdin) {
280
+ for await (const chunk of proc.stdin) {
281
+ chunks.push(typeof chunk === "string" ? chunk : dec.decode(chunk, { stream: true }));
282
+ }
283
+ chunks.push(dec.decode());
284
+ return chunks.join("");
285
+ }
286
+ return "";
287
+ }
288
+ /**
289
+ * `urban data` — the DB-manager op gateway. Reads a single JSON request from stdin
290
+ * (`{ op, source?, sql?, params?, statements? }`) and writes a single JSON response to stdout
291
+ * (`{ ok: true, ...result }` or `{ ok: false, error }`). Every response — a handled op error *or*
292
+ * an unreadable request — is a parseable `{ ok: false, error }` envelope and still exits 0, so the
293
+ * caller can always parse the reply off stdout and never has to treat a non-zero exit as a lost
294
+ * error; the process only exits non-zero on an unexpected crash. This is the seam the Nano console
295
+ * re-points its Data panel at (host dry-out nano-bpm#576), replacing the vendored data-cli.ts.
296
+ */
297
+ export async function cmdData(f, readInput = readStdin) {
298
+ let req;
299
+ try {
300
+ req = JSON.parse(await readInput());
301
+ }
302
+ catch (err) {
303
+ console.log(JSON.stringify({
304
+ ok: false,
305
+ error: `bad request: ${err instanceof Error ? err.message : String(err)}`,
306
+ }));
307
+ return 0;
308
+ }
309
+ const host = selectHost({ cwd: f.root });
310
+ try {
311
+ // Mirror runFromEnv: the host is anchored at f.root, so pass root "." to avoid
312
+ // double-prefixing every manifest/db path (host.abs already resolves against f.root).
313
+ const result = await runDataOp(host, ".", f.manifest, req);
314
+ console.log(JSON.stringify({ ok: true, ...result }));
315
+ }
316
+ catch (err) {
317
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
318
+ }
319
+ return 0;
320
+ }
260
321
  async function cmdNew(f) {
261
322
  const name = f._[1];
262
323
  if (!name) {
@@ -324,7 +385,7 @@ async function cmdAdd(f) {
324
385
  return 0;
325
386
  }
326
387
  catch (err) {
327
- console.error(`✖ ${err.message}`);
388
+ console.error(`✖ ${errorMessage(err)}`);
328
389
  return 1;
329
390
  }
330
391
  }
@@ -334,7 +395,7 @@ export async function main(argv) {
334
395
  f = parse(argv);
335
396
  }
336
397
  catch (err) {
337
- console.error(String(err.message));
398
+ console.error(errorMessage(err));
338
399
  return 1;
339
400
  }
340
401
  if (f.version) {
@@ -360,6 +421,8 @@ export async function main(argv) {
360
421
  return cmdStubs(f);
361
422
  case "add":
362
423
  return cmdAdd(f);
424
+ case "data":
425
+ return cmdData(f);
363
426
  case "run":
364
427
  return cmdRun(f);
365
428
  case "dev":
@@ -372,27 +435,28 @@ export async function main(argv) {
372
435
  return 1;
373
436
  }
374
437
  }
375
- const g = globalThis;
376
- const meta = import.meta;
377
- const argv1 = g.process?.argv?.[1];
378
- const nodeMain = argv1 ? meta.url === pathToFileURL(argv1).href : false;
379
- if (meta.main === true || nodeMain) {
438
+ const proc = processGlobal();
439
+ const deno = denoGlobal();
440
+ const importMetaMain = Reflect.get(import.meta, "main");
441
+ const argv1 = proc?.argv?.[1];
442
+ const nodeMain = argv1 ? import.meta.url === pathToFileURL(argv1).href : false;
443
+ if (importMetaMain === true || nodeMain) {
380
444
  // Node passes args via process.argv (slice off exec+script); Deno (run directly)
381
445
  // exposes them on Deno.args. Prefer whichever actually carries args.
382
- const fromNode = g.process?.argv?.slice(2);
383
- const fromDeno = g.Deno?.args;
446
+ const fromNode = proc?.argv?.slice(2);
447
+ const fromDeno = deno?.args;
384
448
  const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
385
449
  const exit = (code) => {
386
- if (g.process?.exit)
387
- g.process.exit(code);
388
- else if (g.Deno?.exit)
389
- g.Deno.exit(code);
450
+ if (proc?.exit)
451
+ proc.exit(code);
452
+ else if (deno?.exit)
453
+ deno.exit(code);
390
454
  };
391
455
  main(argv).then((code) => {
392
456
  if (code >= 0)
393
457
  exit(code);
394
458
  }, (err) => {
395
- console.error(String(err?.message ?? err));
459
+ console.error(errorMessage(err));
396
460
  exit(1);
397
461
  });
398
462
  }
@@ -13,6 +13,20 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
13
13
  import { isAbsolute, resolve } from "node:path";
14
14
  import { pathToFileURL } from "node:url";
15
15
  import { DatabaseSync } from "node:sqlite";
16
+ function sqliteParams(params) {
17
+ return params.map((param) => {
18
+ if (param === null ||
19
+ typeof param === "string" ||
20
+ typeof param === "number" ||
21
+ typeof param === "bigint" ||
22
+ param instanceof Uint8Array) {
23
+ return param;
24
+ }
25
+ if (typeof param === "boolean")
26
+ return param ? 1 : 0;
27
+ throw new TypeError(`unsupported SQLite parameter type: ${typeof param}`);
28
+ });
29
+ }
16
30
  export function createDenoHost(opts = {}) {
17
31
  const cwd = opts.cwd ?? Deno.cwd();
18
32
  const abs = (p) => (isAbsolute(p) ? p : resolve(cwd, p));
@@ -53,7 +67,8 @@ export function createDenoHost(opts = {}) {
53
67
  },
54
68
  importModule: (p) => {
55
69
  const href = pathToFileURL(abs(p)).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
56
- return import(__rewriteRelativeImportExtension(href));
70
+ const mod = import(__rewriteRelativeImportExtension(href));
71
+ return mod;
57
72
  },
58
73
  async serveHttp(port, handler) {
59
74
  return startDenoServer(port, handler);
@@ -96,12 +111,13 @@ function wrapSqlite(db) {
96
111
  exec: (sql) => db.exec(sql),
97
112
  run: (sql, params = []) => {
98
113
  const stmt = db.prepare(sql);
99
- const r = stmt.run(...params);
114
+ const r = stmt.run(...sqliteParams(params));
100
115
  return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
101
116
  },
102
117
  all: (sql, params = []) => {
103
118
  const stmt = db.prepare(sql);
104
- return stmt.all(...params);
119
+ // biome-ignore lint/plugin: Deno sqlite returns untyped row objects; SqliteDb.all<T> is the host adapter boundary.
120
+ return stmt.all(...sqliteParams(params));
105
121
  },
106
122
  close: () => db.close(),
107
123
  };
@@ -2,10 +2,10 @@
2
2
  // modules is safe — neither touches its runtime's globals at module top level.
3
3
  import { createNodeHost } from "./node.js";
4
4
  import { createDenoHost } from "./deno.js";
5
+ import { denoGlobal } from "./globals.js";
5
6
  /** True when running under Deno. */
6
7
  export function isDeno() {
7
- const g = globalThis;
8
- return typeof g.Deno !== "undefined" && typeof g.Deno.version !== "undefined";
8
+ return denoGlobal()?.version !== undefined;
9
9
  }
10
10
  /** Select and construct the host for the current runtime. */
11
11
  export function selectHost(opts = {}) {
@@ -0,0 +1,19 @@
1
+ export interface DenoRuntimeGlobal {
2
+ version?: unknown;
3
+ stdin?: {
4
+ readable?: ReadableStream<Uint8Array>;
5
+ };
6
+ args?: string[];
7
+ exit?: (code: number) => void;
8
+ addSignalListener?: (sig: string, cb: () => void) => void;
9
+ }
10
+ export interface ProcessRuntimeGlobal {
11
+ argv?: string[];
12
+ exit?: (code: number) => void;
13
+ stdin?: AsyncIterable<Uint8Array | string> & {
14
+ setEncoding?(encoding: string): void;
15
+ };
16
+ on?: (sig: string, cb: () => void) => void;
17
+ }
18
+ export declare function denoGlobal(): DenoRuntimeGlobal | undefined;
19
+ export declare function processGlobal(): ProcessRuntimeGlobal | undefined;
@@ -0,0 +1,9 @@
1
+ import { isRecord } from "../core/guards.js";
2
+ export function denoGlobal() {
3
+ const value = Reflect.get(globalThis, "Deno");
4
+ return isRecord(value) ? value : undefined;
5
+ }
6
+ export function processGlobal() {
7
+ const value = Reflect.get(globalThis, "process");
8
+ return isRecord(value) ? value : undefined;
9
+ }
@@ -16,6 +16,20 @@ import { register } from "node:module";
16
16
  import { isAbsolute, resolve } from "node:path";
17
17
  import { fileURLToPath, pathToFileURL } from "node:url";
18
18
  import { DatabaseSync } from "node:sqlite";
19
+ function sqliteParams(params) {
20
+ return params.map((param) => {
21
+ if (param === null ||
22
+ typeof param === "string" ||
23
+ typeof param === "number" ||
24
+ typeof param === "bigint" ||
25
+ param instanceof Uint8Array) {
26
+ return param;
27
+ }
28
+ if (typeof param === "boolean")
29
+ return param ? 1 : 0;
30
+ throw new TypeError(`unsupported SQLite parameter type: ${typeof param}`);
31
+ });
32
+ }
19
33
  /** Resolve the in-process `@nanobpm/worker` shim URL relative to this adapter,
20
34
  * tolerating both the compiled package (`.js`, the published/`urban run` path)
21
35
  * and a from-source run (`.ts`). */
@@ -101,7 +115,8 @@ export function createNodeHost(opts = {}) {
101
115
  },
102
116
  importModule: (p) => {
103
117
  const href = pathToFileURL(abs(p)).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
104
- return import(__rewriteRelativeImportExtension(href));
118
+ const mod = import(__rewriteRelativeImportExtension(href));
119
+ return mod;
105
120
  },
106
121
  async importConnectorModule(entry) {
107
122
  ensureConnectorHooks();
@@ -147,12 +162,13 @@ function wrapNodeSqlite(db) {
147
162
  exec: (sql) => db.exec(sql),
148
163
  run: (sql, params = []) => {
149
164
  const stmt = db.prepare(sql);
150
- const r = stmt.run(...params);
165
+ const r = stmt.run(...sqliteParams(params));
151
166
  return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
152
167
  },
153
168
  all: (sql, params = []) => {
154
169
  const stmt = db.prepare(sql);
155
- return stmt.all(...params);
170
+ // biome-ignore lint/plugin: Node sqlite returns untyped row objects; SqliteDb.all<T> is the host adapter boundary.
171
+ return stmt.all(...sqliteParams(params));
156
172
  },
157
173
  close: () => db.close(),
158
174
  };
@@ -160,8 +176,12 @@ function wrapNodeSqlite(db) {
160
176
  async function startNodeServer(port, handler) {
161
177
  const server = createServer(async (nreq, nres) => {
162
178
  const chunks = [];
163
- for await (const c of nreq)
164
- chunks.push(c);
179
+ for await (const c of nreq) {
180
+ if (Buffer.isBuffer(c))
181
+ chunks.push(c);
182
+ else
183
+ chunks.push(Buffer.from(c));
184
+ }
165
185
  const bodyText = Buffer.concat(chunks).toString("utf8");
166
186
  const url = new URL(nreq.url ?? "/", "http://localhost");
167
187
  const headers = new Headers();
@@ -1,6 +1,7 @@
1
1
  import type { AppManifest } from "./manifest.ts";
2
2
  import type { EngineClient, HostContext } from "./host.ts";
3
3
  import type { EngineSdkClient } from "../engine/sdk.ts";
4
+ import type { TemplateSource } from "./modules/templates.ts";
4
5
  /** Everything a runtime module needs. Passed to each module's mount function. */
5
6
  export interface RuntimeContext {
6
7
  manifest: AppManifest;
@@ -8,6 +9,9 @@ export interface RuntimeContext {
8
9
  engine: EngineClient;
9
10
  /** App root directory (paths in the manifest are relative to this). */
10
11
  root: string;
12
+ /** Programmatic `{{name}}` template source for deploy-time substitution (globs or a
13
+ * `name → content` map), merged over the manifest's `models.templates`. See `deployModels`. */
14
+ templates?: TemplateSource;
11
15
  }
12
16
  /** A mounted module returns a disposer so the runtime can tear it down cleanly. */
13
17
  export interface Mounted {
@@ -0,0 +1,2 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function errorMessage(error: unknown): string;
@@ -0,0 +1,10 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ export function errorMessage(error) {
5
+ if (error instanceof Error)
6
+ return error.message;
7
+ if (isRecord(error) && typeof error.message === "string")
8
+ return error.message;
9
+ return String(error);
10
+ }
@@ -23,8 +23,9 @@ export class BpmnError extends Error {
23
23
  * BpmnError constructed against a different copy of this module still routes as
24
24
  * a BPMN error rather than a generic failure). */
25
25
  export function isBpmnError(err) {
26
- return (typeof err === "object" &&
27
- err !== null &&
28
- typeof err.errorCode === "string" &&
29
- err.errorCode.length > 0);
26
+ if (typeof err !== "object" || err === null)
27
+ return false;
28
+ const errorCode = "errorCode" in err ? err.errorCode : undefined;
29
+ return (typeof errorCode === "string" &&
30
+ errorCode.length > 0);
30
31
  }
@@ -1,6 +1,14 @@
1
1
  import type { HostContext } from "./host.ts";
2
2
  import type { AppManifest, Worker } from "@nanobpm/nano-app-schema";
3
3
  export type { ActionDecl, AppManifest, ChatSurface, Connection, Data, DataSource, DomainField, DomainType, LlmBinding, Models, PagesSurface, Runtime, Security, Surfaces, TaskInboxSurface, Trigger, TriggerAction, Worker, } from "@nanobpm/nano-app-schema";
4
+ declare module "@nanobpm/nano-app-schema" {
5
+ interface Models {
6
+ /** `{{name}}` template sources substituted into model resources at deploy time: globs, a bare
7
+ * directory (scanned), or literal files. A template's name is its file stem
8
+ * (`prompts/review-round.md` → `{{review-round}}`). See `deployModels`. */
9
+ templates?: string[];
10
+ }
11
+ }
4
12
  /** The job type a worker subscribes to (schema key: `taskType`). */
5
13
  export declare function workerJobType(w: Worker): string | undefined;
6
14
  /**
@@ -24,7 +24,6 @@ export function expandEnvString(input, lookup) {
24
24
  return dflt ?? "";
25
25
  });
26
26
  }
27
- /** Recursively expand env placeholders across every string in a JSON-ish value. */
28
27
  export function expandEnv(value, lookup) {
29
28
  if (typeof value === "string")
30
29
  return expandEnvString(value, lookup);
@@ -9,16 +9,20 @@
9
9
  // Precedence: action routes are mounted BEFORE the generic pages action routes (the router
10
10
  // is first-match-wins), so an override at `/app/actions/cancel` shadows the generic one,
11
11
  // while an unmatched process still falls through to the generic `/app/actions/start/` route.
12
+ import { errorMessage } from "../guards.js";
12
13
  import { json, normalizeRoutePath } from "../router.js";
14
+ function isActionHandler(value) {
15
+ return typeof value === "function";
16
+ }
13
17
  /**
14
18
  * Resolve an action handler from a loaded module, in priority order:
15
19
  * 1. `default` (when it is a function — the common one-handler-per-file case)
16
20
  * 2. a named export `handler`
17
21
  */
18
22
  export function resolveActionHandler(mod) {
19
- if (typeof mod.default === "function")
23
+ if (isActionHandler(mod.default))
20
24
  return mod.default;
21
- if (typeof mod.handler === "function")
25
+ if (isActionHandler(mod.handler))
22
26
  return mod.handler;
23
27
  return undefined;
24
28
  }
@@ -28,7 +32,7 @@ export function resolveActionHandler(mod) {
28
32
  * surfaces as a clear `500` on that route rather than failing the whole boot.
29
33
  */
30
34
  export function mountActions(ctx, app) {
31
- const decls = (ctx.manifest.actions ?? []);
35
+ const decls = ctx.manifest.actions ?? [];
32
36
  const routes = [];
33
37
  const mounted = [];
34
38
  const joinRoot = (p) => p.startsWith("/") ? p : `${ctx.root.replace(/\/+$/, "")}/${p.replace(/^\/+/, "")}`;
@@ -85,7 +89,7 @@ export function mountActions(ctx, app) {
85
89
  ctx.host.log("error", "action handler module failed to load", {
86
90
  path,
87
91
  module: decl.module,
88
- error: String(e?.message ?? e),
92
+ error: errorMessage(e),
89
93
  });
90
94
  return json({ error: `action handler ${decl.module} failed to load` }, 500);
91
95
  }
@@ -103,7 +107,7 @@ export function mountActions(ctx, app) {
103
107
  };
104
108
  }
105
109
  catch (e) {
106
- return json({ error: String(e?.message ?? e) }, 500);
110
+ return json({ error: errorMessage(e) }, 500);
107
111
  }
108
112
  },
109
113
  });
@@ -14,6 +14,7 @@
14
14
  //
15
15
  // This file is runtime-agnostic (core purity): the Node/Deno specifics of aliasing
16
16
  // the pack's `@nanobpm/worker` import live behind `host.importConnectorModule`.
17
+ import { errorMessage, isRecord } from "../guards.js";
17
18
  import { BpmnError } from "../host.js";
18
19
  import { workerJobType } from "../manifest.js";
19
20
  import { drainDefinedWorkers, } from "../../connector-worker-sdk.js";
@@ -40,11 +41,12 @@ async function readJson(ctx, relPath, opts) {
40
41
  if (!(await ctx.host.exists(path)))
41
42
  return undefined;
42
43
  try {
43
- return JSON.parse(await ctx.host.readTextFile(path));
44
+ const parsed = JSON.parse(await ctx.host.readTextFile(path));
45
+ return parsed;
44
46
  }
45
47
  catch (err) {
46
48
  if (opts?.strict) {
47
- throw new Error(`failed to parse ${relPath}: ${err.message}`);
49
+ throw new Error(`failed to parse ${relPath}: ${errorMessage(err)}`);
48
50
  }
49
51
  return undefined;
50
52
  }
@@ -107,7 +109,7 @@ export function adaptConnectorHandler(worker) {
107
109
  if (outcome?.kind === "complete")
108
110
  return outcome.vars ?? {};
109
111
  // No explicit outcome: fall back to the handle's return value (SDK-style).
110
- return ret && typeof ret === "object" ? ret : {};
112
+ return isRecord(ret) ? ret : {};
111
113
  };
112
114
  }
113
115
  /**
@@ -118,7 +120,7 @@ export function adaptConnectorHandler(worker) {
118
120
  * the host cannot host connector workers (`importConnectorModule` absent).
119
121
  */
120
122
  export async function mountConnectors(ctx, app) {
121
- const decls = (ctx.manifest.workers ?? []).filter((w) => w.connector);
123
+ const decls = (ctx.manifest.workers ?? []).filter((w) => Boolean(w.connector));
122
124
  const subs = [];
123
125
  const jobTypes = [];
124
126
  if (decls.length === 0)
@@ -0,0 +1,36 @@
1
+ import type { HostContext } from "../host.ts";
2
+ import type { AppManifest } from "../manifest.ts";
3
+ /** The DB-manager op protocol's known op set. `domaintypes` is part of the protocol but is not
4
+ * yet handled here (it errors clearly — see the file header); every other op is dispatched by
5
+ * `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers
6
+ * type-safety and catches typos at compile time. */
7
+ export type DataOp = "sources" | "schema" | "query" | "exec" | "script" | "migrations" | "migrate" | "domaintypes";
8
+ export interface DataRequest {
9
+ op: DataOp;
10
+ source?: string;
11
+ sql?: string;
12
+ params?: unknown[];
13
+ statements?: string[];
14
+ }
15
+ /** One datasource the manifest declares, resolved against the environment (env templates in
16
+ * `driver`/`url` are already expanded by `loadManifest`). Mirrors the console's `ResolvedSource`
17
+ * so the panel's datasource picker is unchanged. */
18
+ export interface ResolvedSource {
19
+ name: string;
20
+ driver: string;
21
+ url: string;
22
+ migrations?: string;
23
+ }
24
+ /** JSON-safe a value: BigInt → number (when lossless) or string, recursing objects/arrays. SQLite
25
+ * reports `lastInsertRowid` (and INTEGER columns) as BigInt, which `JSON.stringify` cannot encode. */
26
+ export declare function jsonSafe(v: unknown): unknown;
27
+ /** List every declared datasource plus the default's name (no connection opened). */
28
+ export declare function listSources(manifest: AppManifest): {
29
+ default?: string;
30
+ sources: ResolvedSource[];
31
+ };
32
+ /**
33
+ * Dispatch one DB-manager op against the app at `root`. Returns the op's result object (the CLI
34
+ * wraps it as `{ ok: true, ...result }`); throws on a handled error (wrapped as `{ ok: false }`).
35
+ */
36
+ export declare function runDataOp(host: HostContext, root: string, manifestPath: string, req: DataRequest): Promise<Record<string, unknown>>;