@nanobpm/urban 0.4.0 → 0.5.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 (69) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +212 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +25 -0
  6. package/dist/runtime/adapters/deno.d.ts +6 -0
  7. package/dist/runtime/adapters/deno.js +98 -0
  8. package/dist/runtime/adapters/detect.d.ts +9 -0
  9. package/dist/runtime/adapters/detect.js +13 -0
  10. package/dist/runtime/adapters/node.d.ts +7 -0
  11. package/dist/runtime/adapters/node.js +113 -0
  12. package/dist/runtime/core/context.d.ts +40 -0
  13. package/dist/runtime/core/context.js +1 -0
  14. package/dist/runtime/core/glob.d.ts +5 -0
  15. package/dist/runtime/core/glob.js +51 -0
  16. package/dist/runtime/core/host.d.ts +128 -0
  17. package/dist/runtime/core/host.js +5 -0
  18. package/dist/runtime/core/manifest.d.ts +83 -0
  19. package/dist/runtime/core/manifest.js +45 -0
  20. package/dist/runtime/core/modules/datasource.d.ts +38 -0
  21. package/dist/runtime/core/modules/datasource.js +172 -0
  22. package/dist/runtime/core/modules/deploy.d.ts +6 -0
  23. package/dist/runtime/core/modules/deploy.js +37 -0
  24. package/dist/runtime/core/modules/security.d.ts +11 -0
  25. package/dist/runtime/core/modules/security.js +32 -0
  26. package/dist/runtime/core/modules/surfaces.d.ts +9 -0
  27. package/dist/runtime/core/modules/surfaces.js +88 -0
  28. package/dist/runtime/core/modules/triggers.d.ts +15 -0
  29. package/dist/runtime/core/modules/triggers.js +119 -0
  30. package/dist/runtime/core/modules/workers.d.ts +16 -0
  31. package/dist/runtime/core/modules/workers.js +79 -0
  32. package/dist/runtime/core/router.d.ts +22 -0
  33. package/dist/runtime/core/router.js +39 -0
  34. package/dist/runtime/core/runtime.d.ts +46 -0
  35. package/dist/runtime/core/runtime.js +173 -0
  36. package/dist/runtime/core/validate.d.ts +13 -0
  37. package/dist/runtime/core/validate.js +108 -0
  38. package/dist/runtime/engine/nanosdk.d.ts +151 -0
  39. package/dist/runtime/engine/nanosdk.js +209 -0
  40. package/dist/runtime/engine/sdk.d.ts +3 -0
  41. package/dist/runtime/engine/sdk.js +13 -0
  42. package/dist/runtime/index.d.ts +21 -0
  43. package/dist/runtime/index.js +17 -0
  44. package/dist/runtime/run.d.ts +17 -0
  45. package/dist/runtime/run.js +69 -0
  46. package/dist/runtime/schema/nano-app.schema.json +471 -0
  47. package/dist/toolkit/artifact.d.ts +19 -0
  48. package/dist/toolkit/artifact.js +11 -0
  49. package/dist/toolkit/derivers/migrations.d.ts +26 -0
  50. package/dist/toolkit/derivers/migrations.js +73 -0
  51. package/dist/toolkit/derivers/worker-io.d.ts +38 -0
  52. package/dist/toolkit/derivers/worker-io.js +135 -0
  53. package/dist/toolkit/fsio.d.ts +2 -0
  54. package/dist/toolkit/fsio.js +35 -0
  55. package/dist/toolkit/gen.d.ts +28 -0
  56. package/dist/toolkit/gen.js +75 -0
  57. package/dist/toolkit/index.d.ts +13 -0
  58. package/dist/toolkit/index.js +15 -0
  59. package/package.json +19 -6
  60. package/src/bin.mjs +6 -11
  61. package/src/cli.ts +14 -1
  62. package/src/index.ts +7 -0
  63. package/src/runtime/core/context.ts +10 -0
  64. package/src/runtime/core/runtime.test.ts +46 -1
  65. package/src/runtime/core/runtime.ts +9 -0
  66. package/src/runtime/engine/nanosdk.test.ts +6 -0
  67. package/src/runtime/engine/nanosdk.ts +10 -0
  68. package/src/runtime/engine/sdk.ts +18 -0
  69. package/src/runtime/index.ts +1 -0
package/README.md CHANGED
@@ -19,8 +19,10 @@ npx @nanobpm/urban new my-app
19
19
  deno run -A npm:@nanobpm/urban run
20
20
  ```
21
21
 
22
- Requires Node ≥ 22.6 or Deno. It ships as TypeScript source with no build step;
23
- on Node < 23.6 the `urban` command re-executes with `--experimental-strip-types`.
22
+ Requires Node ≥ 22.6 or Deno. It ships as compiled JavaScript with `.d.ts` type
23
+ declarations: Node can't strip types under `node_modules`, so the published package
24
+ carries `dist/` and needs no build step or `--experimental-strip-types` flag to run.
25
+ Deno users can still import the TypeScript source directly via the `./source` export.
24
26
 
25
27
  ## The `urban` CLI
26
28
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,212 @@
1
+ // The `urban` CLI — a thin command surface over the Urban runtime + toolkit.
2
+ //
3
+ // urban new <name> scaffold a new app (delegates to create-urban-app)
4
+ // urban check load + validate the manifest, report issues
5
+ // urban gen derive generated artifacts (migrations, worker I/O)
6
+ // urban run materialize + serve the app (deploy, data, workers, surfaces, triggers)
7
+ // urban dev like run (hot-reload is not yet implemented; documented below)
8
+ // urban deploy deploy models only, then exit
9
+ //
10
+ // Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
11
+ // --port <n>, -h/--help, -v/--version.
12
+ import { collectManifestIssues, loadManifest, runFromEnv, selectHost, } from "./runtime/index.js";
13
+ import { scaffold, slugify } from "create-urban-app";
14
+ import { createNodeGenIO, runGen } from "./toolkit/index.js";
15
+ import { pathToFileURL } from "node:url";
16
+ import { readFileSync } from "node:fs";
17
+ // Read the package version from package.json (one hop up from both src/cli.ts and the
18
+ // compiled dist/cli.js) so `urban --version` never drifts from the published version.
19
+ function readVersion() {
20
+ try {
21
+ const url = new URL("../package.json", import.meta.url);
22
+ return JSON.parse(readFileSync(url, "utf8")).version ?? "0.0.0";
23
+ }
24
+ catch {
25
+ return "0.0.0";
26
+ }
27
+ }
28
+ const VERSION = readVersion();
29
+ function parse(argv) {
30
+ const f = { root: ".", manifest: "nano.app.json", check: false, help: false, version: false, _: [] };
31
+ const need = (i, flag) => {
32
+ const v = argv[i];
33
+ if (v === undefined || v.startsWith("-"))
34
+ throw new Error(`flag ${flag} requires a value`);
35
+ return v;
36
+ };
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === "-h" || a === "--help")
40
+ f.help = true;
41
+ else if (a === "-v" || a === "--version")
42
+ f.version = true;
43
+ else if (a === "--check")
44
+ f.check = true;
45
+ else if (a === "--root")
46
+ f.root = need(++i, a);
47
+ else if (a === "--manifest")
48
+ f.manifest = need(++i, a);
49
+ else if (a === "--port") {
50
+ const raw = need(++i, a);
51
+ const n = Number(raw);
52
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
53
+ throw new Error(`flag --port requires an integer in 0..65535 (got "${raw}")`);
54
+ }
55
+ f.port = n;
56
+ }
57
+ else if (a.startsWith("-"))
58
+ throw new Error(`unknown flag: ${a}`);
59
+ else
60
+ f._.push(a);
61
+ }
62
+ return f;
63
+ }
64
+ const USAGE = `urban — build and run Urban apps (nano.app.json)
65
+
66
+ Usage:
67
+ urban new <name> [--root <path>] scaffold a new Urban app
68
+ urban check validate the manifest
69
+ urban gen [--check] derive artifacts (migrations, worker-io)
70
+ urban run materialize + serve the app
71
+ urban dev run (hot-reload not yet implemented)
72
+ urban deploy deploy models only, then exit
73
+
74
+ Global flags:
75
+ --root <dir> app root (default ".")
76
+ --manifest <file> manifest filename (default "nano.app.json")
77
+ --port <n> HTTP port for surfaces/triggers (default $PORT or 8090)
78
+ -h, --help show this help
79
+ -v, --version print version
80
+
81
+ Engine address: $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2).
82
+ `;
83
+ async function cmdCheck(f) {
84
+ // The host is anchored at f.root, so manifest paths are already root-relative —
85
+ // pass the bare manifest filename (prefixing root again would double it).
86
+ const host = selectHost({ cwd: f.root });
87
+ const manifest = await loadManifest(host, f.manifest);
88
+ const issues = collectManifestIssues(manifest);
89
+ if (issues.length === 0) {
90
+ console.log(`✔ ${manifest.name ?? manifest.id} — manifest is valid`);
91
+ return 0;
92
+ }
93
+ console.error(`✖ ${issues.length} issue(s) in ${f.manifest}:`);
94
+ for (const it of issues)
95
+ console.error(` • ${it.path}: ${it.message}`);
96
+ return 1;
97
+ }
98
+ async function cmdGen(f) {
99
+ const io = createNodeGenIO();
100
+ const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check });
101
+ if (f.check) {
102
+ if (res.drift.length === 0) {
103
+ console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
104
+ return 0;
105
+ }
106
+ console.error(`✖ ${res.drift.length} generated artifact(s) are out of date — run \`urban gen\`:`);
107
+ for (const p of res.drift)
108
+ console.error(` • ${p}`);
109
+ return 1;
110
+ }
111
+ console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
112
+ for (const a of res.artifacts)
113
+ console.log(` • ${a.path}`);
114
+ return 0;
115
+ }
116
+ async function cmdRun(f, mount) {
117
+ const app = await runFromEnv({
118
+ root: f.root,
119
+ manifestPath: f.manifest,
120
+ port: f.port,
121
+ mount,
122
+ });
123
+ const info = app.inspect();
124
+ console.log(`▲ ${info.name} — surfaces on :${info.httpPort ?? "n/a"} (Ctrl-C to stop)`);
125
+ return -1; // keep the process alive; signal handlers stop it
126
+ }
127
+ async function cmdDeploy(f) {
128
+ const app = await runFromEnv({
129
+ root: f.root,
130
+ manifestPath: f.manifest,
131
+ mount: { deploy: true, data: false, workers: false, surfaces: false, triggers: false, security: false },
132
+ handleSignals: false,
133
+ });
134
+ console.log(`✔ deployed models for ${app.manifest.name ?? app.manifest.id}`);
135
+ await app.stop();
136
+ return 0;
137
+ }
138
+ async function cmdNew(f) {
139
+ const name = f._[1];
140
+ if (!name) {
141
+ console.error("usage: urban new <name> [--root <path>]");
142
+ return 1;
143
+ }
144
+ const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
145
+ const res = await scaffold({ name, dir });
146
+ console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
147
+ console.log(` cd ${res.dir} && (npm install && npm start) || deno task start`);
148
+ return 0;
149
+ }
150
+ export async function main(argv) {
151
+ let f;
152
+ try {
153
+ f = parse(argv);
154
+ }
155
+ catch (err) {
156
+ console.error(String(err.message));
157
+ return 1;
158
+ }
159
+ if (f.version) {
160
+ console.log(VERSION);
161
+ return 0;
162
+ }
163
+ const cmd = f._[0];
164
+ if (f.help || !cmd) {
165
+ console.log(USAGE);
166
+ return cmd ? 0 : f.help ? 0 : 1;
167
+ }
168
+ switch (cmd) {
169
+ case "new":
170
+ case "scaffold":
171
+ return cmdNew(f);
172
+ case "check":
173
+ return cmdCheck(f);
174
+ case "gen":
175
+ return cmdGen(f);
176
+ case "run":
177
+ return cmdRun(f);
178
+ case "dev":
179
+ console.log("urban dev: hot-reload is not yet implemented; running once.");
180
+ return cmdRun(f);
181
+ case "deploy":
182
+ return cmdDeploy(f);
183
+ default:
184
+ console.error(`unknown command: ${cmd}\n`);
185
+ console.log(USAGE);
186
+ return 1;
187
+ }
188
+ }
189
+ const g = globalThis;
190
+ const meta = import.meta;
191
+ const argv1 = g.process?.argv?.[1];
192
+ const nodeMain = argv1 ? meta.url === pathToFileURL(argv1).href : false;
193
+ if (meta.main === true || nodeMain) {
194
+ // Node passes args via process.argv (slice off exec+script); Deno (run directly)
195
+ // exposes them on Deno.args. Prefer whichever actually carries args.
196
+ const fromNode = g.process?.argv?.slice(2);
197
+ const fromDeno = g.Deno?.args;
198
+ const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
199
+ const exit = (code) => {
200
+ if (g.process?.exit)
201
+ g.process.exit(code);
202
+ else if (g.Deno?.exit)
203
+ g.Deno.exit(code);
204
+ };
205
+ main(argv).then((code) => {
206
+ if (code >= 0)
207
+ exit(code);
208
+ }, (err) => {
209
+ console.error(String(err?.message ?? err));
210
+ exit(1);
211
+ });
212
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./runtime/index.ts";
2
+ export { createCamundaClient } from "@nanobpm/nano-sdk";
3
+ export * from "./toolkit/index.ts";
4
+ export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
5
+ export type { FlowBuilder, Envelope, EnvelopeType, EnvelopeField, FieldSpec, ScalarType, WorkflowClientOptions, NanoSdkClient, NanoJobWorker, JobWorkerConfig, ActivatedJob, WorkerOptions, ActivityEvent, } from "@nanobpm/workflow";
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // @nanobpm/urban — the code-first Urban stack in one package (ADR 0054):
2
+ //
3
+ // • runtime — materialize and serve an app from a nano.app.json manifest
4
+ // (datasource, workers, surfaces, triggers) on Node or Deno.
5
+ // • toolkit — pure derivers (`urban gen`) that emit generated artifacts
6
+ // (SQL migrations, worker I/O bindings) from the model.
7
+ // • workflow — the code-first process surface, re-exported from
8
+ // `@nanobpm/workflow`: author durable flows with `defineFlow` and the SDK
9
+ // derives the executable BPMN, job types, and correlation wiring.
10
+ //
11
+ // The CLI (`urban`) is a separate entrypoint (bin) over this same API.
12
+ // Subpath imports are also available: `@nanobpm/urban/runtime` and
13
+ // `@nanobpm/urban/toolkit`.
14
+ // Runtime — app materialization + hosting
15
+ export * from "./runtime/index.js";
16
+ // The nano-sdk engine client factory: build a client to talk to a Nano/Camunda
17
+ // engine directly, or type an `AppApi.sdk`. The runtime uses this same factory, so a
18
+ // code-first author reaches the full engine surface without a separate
19
+ // `@nanobpm/nano-sdk` dependency. (`EngineSdkClient`, its return type, is re-exported
20
+ // from ./runtime/index.ts above.)
21
+ export { createCamundaClient } from "@nanobpm/nano-sdk";
22
+ // Toolkit — derivation library (`urban gen`)
23
+ export * from "./toolkit/index.js";
24
+ // Code-first process surface (re-exported from @nanobpm/workflow)
25
+ export { defineFlow, declarativeToBpmn, declarativeToLayoutedBpmn, externalJobTypes, envelope, layoutBpmn, toBpmn, toDeployableBpmn, Worker, WorkflowClient, WorkflowError, } from "@nanobpm/workflow";
@@ -0,0 +1,6 @@
1
+ import type { HostContext } from "../core/host.ts";
2
+ export interface DenoHostOptions {
3
+ cwd?: string;
4
+ log?: HostContext["log"];
5
+ }
6
+ export declare function createDenoHost(opts?: DenoHostOptions): HostContext;
@@ -0,0 +1,98 @@
1
+ // Deno adapter — implements HostContext against the `Deno` global (plus `node:*` compat
2
+ // modules Deno supports). This and node.ts are the only files that touch a concrete runtime.
3
+ // A minimal ambient `Deno` declaration lets this compile under Node's tsc; at runtime the
4
+ // adapter is only ever selected when the real `Deno` global is present (see detect.ts).
5
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
6
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
7
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
8
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
9
+ });
10
+ }
11
+ return path;
12
+ };
13
+ import { isAbsolute, resolve } from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ import { DatabaseSync } from "node:sqlite";
16
+ export function createDenoHost(opts = {}) {
17
+ const cwd = opts.cwd ?? Deno.cwd();
18
+ const abs = (p) => (isAbsolute(p) ? p : resolve(cwd, p));
19
+ const log = opts.log ??
20
+ ((level, msg, fields) => {
21
+ const line = fields ? `${msg} ${JSON.stringify(fields)}` : msg;
22
+ (level === "error" ? console.error : level === "warn" ? console.warn : console.log)(`[urban] ${line}`);
23
+ });
24
+ return {
25
+ runtime: "deno",
26
+ env: (name) => Deno.env.get(name),
27
+ readTextFile: (p) => Deno.readTextFile(abs(p)),
28
+ async listDir(dir) {
29
+ try {
30
+ const names = [];
31
+ for await (const e of Deno.readDir(abs(dir))) {
32
+ if (e.isFile)
33
+ names.push(e.name);
34
+ }
35
+ return names;
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ },
41
+ async exists(p) {
42
+ try {
43
+ await Deno.stat(abs(p));
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ },
50
+ openSqlite(path) {
51
+ const db = new DatabaseSync(abs(path));
52
+ return wrapSqlite(db);
53
+ },
54
+ importModule: (p) => import(__rewriteRelativeImportExtension(pathToFileURL(abs(p)).href)),
55
+ async serveHttp(port, handler) {
56
+ return startDenoServer(port, handler);
57
+ },
58
+ now: () => Date.now(),
59
+ log,
60
+ };
61
+ }
62
+ function wrapSqlite(db) {
63
+ return {
64
+ exec: (sql) => db.exec(sql),
65
+ run: (sql, params = []) => {
66
+ const stmt = db.prepare(sql);
67
+ const r = stmt.run(...params);
68
+ return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
69
+ },
70
+ all: (sql, params = []) => {
71
+ const stmt = db.prepare(sql);
72
+ return stmt.all(...params);
73
+ },
74
+ close: () => db.close(),
75
+ };
76
+ }
77
+ function startDenoServer(port, handler) {
78
+ return new Promise((resolveServer) => {
79
+ const server = Deno.serve({ port, onListen: ({ port: p }) => {
80
+ resolveServer({
81
+ port: p,
82
+ stop: () => server.shutdown(),
83
+ });
84
+ } }, async (request) => {
85
+ const url = new URL(request.url);
86
+ const bodyText = request.body ? await request.text() : "";
87
+ const req = {
88
+ method: request.method,
89
+ path: url.pathname,
90
+ query: url.searchParams,
91
+ headers: request.headers,
92
+ text: () => Promise.resolve(bodyText),
93
+ };
94
+ const res = await handler(req);
95
+ return new Response(res.body ?? "", { status: res.status ?? 200, headers: res.headers });
96
+ });
97
+ });
98
+ }
@@ -0,0 +1,9 @@
1
+ import type { HostContext } from "../core/host.ts";
2
+ /** True when running under Deno. */
3
+ export declare function isDeno(): boolean;
4
+ export interface SelectHostOptions {
5
+ cwd?: string;
6
+ log?: HostContext["log"];
7
+ }
8
+ /** Select and construct the host for the current runtime. */
9
+ export declare function selectHost(opts?: SelectHostOptions): HostContext;
@@ -0,0 +1,13 @@
1
+ // Runtime detection: pick the Node or Deno host at load time. Importing both adapter
2
+ // modules is safe — neither touches its runtime's globals at module top level.
3
+ import { createNodeHost } from "./node.js";
4
+ import { createDenoHost } from "./deno.js";
5
+ /** True when running under Deno. */
6
+ export function isDeno() {
7
+ const g = globalThis;
8
+ return typeof g.Deno !== "undefined" && typeof g.Deno.version !== "undefined";
9
+ }
10
+ /** Select and construct the host for the current runtime. */
11
+ export function selectHost(opts = {}) {
12
+ return isDeno() ? createDenoHost(opts) : createNodeHost(opts);
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { HostContext } from "../core/host.ts";
2
+ export interface NodeHostOptions {
3
+ /** Base directory relative paths resolve against. Default process.cwd(). */
4
+ cwd?: string;
5
+ log?: HostContext["log"];
6
+ }
7
+ export declare function createNodeHost(opts?: NodeHostOptions): HostContext;
@@ -0,0 +1,113 @@
1
+ // Node adapter — implements HostContext against `node:*`. This is one of only two files
2
+ // (with deno.ts) allowed to touch a concrete runtime.
3
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
4
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
5
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
6
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
7
+ });
8
+ }
9
+ return path;
10
+ };
11
+ import { readFile, readdir, stat } from "node:fs/promises";
12
+ import { createServer } from "node:http";
13
+ import { isAbsolute, resolve } from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ import { DatabaseSync } from "node:sqlite";
16
+ export function createNodeHost(opts = {}) {
17
+ const cwd = opts.cwd ?? process.cwd();
18
+ const abs = (p) => (isAbsolute(p) ? p : resolve(cwd, p));
19
+ const log = opts.log ??
20
+ ((level, msg, fields) => {
21
+ const line = fields ? `${msg} ${JSON.stringify(fields)}` : msg;
22
+ (level === "error" ? console.error : level === "warn" ? console.warn : console.log)(`[urban] ${line}`);
23
+ });
24
+ return {
25
+ runtime: "node",
26
+ env: (name) => process.env[name],
27
+ readTextFile: (p) => readFile(abs(p), "utf8"),
28
+ async listDir(dir) {
29
+ try {
30
+ const entries = await readdir(abs(dir), { withFileTypes: true });
31
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ },
37
+ async exists(p) {
38
+ try {
39
+ await stat(abs(p));
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ },
46
+ openSqlite(path) {
47
+ const db = new DatabaseSync(abs(path));
48
+ return wrapNodeSqlite(db);
49
+ },
50
+ importModule: (p) => import(__rewriteRelativeImportExtension(pathToFileURL(abs(p)).href)),
51
+ async serveHttp(port, handler) {
52
+ return await startNodeServer(port, handler);
53
+ },
54
+ now: () => Date.now(),
55
+ log,
56
+ };
57
+ }
58
+ function wrapNodeSqlite(db) {
59
+ return {
60
+ exec: (sql) => db.exec(sql),
61
+ run: (sql, params = []) => {
62
+ const stmt = db.prepare(sql);
63
+ const r = stmt.run(...params);
64
+ return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
65
+ },
66
+ all: (sql, params = []) => {
67
+ const stmt = db.prepare(sql);
68
+ return stmt.all(...params);
69
+ },
70
+ close: () => db.close(),
71
+ };
72
+ }
73
+ async function startNodeServer(port, handler) {
74
+ const server = createServer(async (nreq, nres) => {
75
+ const chunks = [];
76
+ for await (const c of nreq)
77
+ chunks.push(c);
78
+ const bodyText = Buffer.concat(chunks).toString("utf8");
79
+ const url = new URL(nreq.url ?? "/", "http://localhost");
80
+ const headers = new Headers();
81
+ for (const [k, v] of Object.entries(nreq.headers)) {
82
+ if (typeof v === "string")
83
+ headers.set(k, v);
84
+ else if (Array.isArray(v))
85
+ headers.set(k, v.join(", "));
86
+ }
87
+ const req = {
88
+ method: nreq.method ?? "GET",
89
+ path: url.pathname,
90
+ query: url.searchParams,
91
+ headers,
92
+ text: () => Promise.resolve(bodyText),
93
+ };
94
+ try {
95
+ const res = await handler(req);
96
+ nres.statusCode = res.status ?? 200;
97
+ for (const [k, v] of Object.entries(res.headers ?? {}))
98
+ nres.setHeader(k, v);
99
+ nres.end(res.body ?? "");
100
+ }
101
+ catch (err) {
102
+ nres.statusCode = 500;
103
+ nres.end(String(err));
104
+ }
105
+ });
106
+ await new Promise((res) => server.listen(port, res));
107
+ const addr = server.address();
108
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
109
+ return {
110
+ port: actualPort,
111
+ stop: () => new Promise((res) => server.close(() => res())),
112
+ };
113
+ }
@@ -0,0 +1,40 @@
1
+ import type { AppManifest } from "./manifest.ts";
2
+ import type { EngineClient, HostContext } from "./host.ts";
3
+ import type { EngineSdkClient } from "../engine/sdk.ts";
4
+ /** Everything a runtime module needs. Passed to each module's mount function. */
5
+ export interface RuntimeContext {
6
+ manifest: AppManifest;
7
+ host: HostContext;
8
+ engine: EngineClient;
9
+ /** App root directory (paths in the manifest are relative to this). */
10
+ root: string;
11
+ }
12
+ /** A mounted module returns a disposer so the runtime can tear it down cleanly. */
13
+ export interface Mounted {
14
+ readonly name: string;
15
+ stop(): Promise<void>;
16
+ /** Optional human-readable summary for `inspect()`. */
17
+ describe?(): Record<string, unknown>;
18
+ }
19
+ import type { DataLayer } from "./modules/datasource.ts";
20
+ /**
21
+ * The surface injected into worker/trigger/surface handlers — the app's runtime API.
22
+ * This is how a handler reaches the datasource (typed accessors), the engine, and host
23
+ * utilities without hard-coding any of them.
24
+ */
25
+ export interface AppApi {
26
+ manifest: AppManifest;
27
+ data: DataLayer;
28
+ engine: EngineClient;
29
+ /**
30
+ * The underlying `@nanobpm/nano-sdk` engine client, present when the app runs on
31
+ * the nano-sdk transport (the default). It exposes the full Camunda
32
+ * orchestration-cluster surface — decisions, cluster variables, incidents, user
33
+ * tasks, agents, batch operations — beyond the transport-agnostic `engine` seam,
34
+ * over the same connection. Undefined when a non-SDK engine is injected (e.g. an
35
+ * in-memory test double).
36
+ */
37
+ sdk?: EngineSdkClient;
38
+ env(name: string): string | undefined;
39
+ log(level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>): void;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { HostContext } from "./host.ts";
2
+ /** Expand a single pattern (relative to `root`) to a sorted list of matching file paths. */
3
+ export declare function expandPattern(host: HostContext, root: string, pattern: string): Promise<string[]>;
4
+ /** Expand many patterns, de-duplicating while preserving first-seen order. */
5
+ export declare function expandPatterns(host: HostContext, root: string, patterns: string[] | undefined): Promise<string[]>;
@@ -0,0 +1,51 @@
1
+ // A deliberately tiny glob: resolves patterns of the form "dir/*.ext" (a single directory
2
+ // plus a `*.ext` or `*` leaf) against the host filesystem. This covers the manifest
3
+ // `models` patterns (e.g. "processes/*.bpmn") without pulling in a glob dependency, keeping
4
+ // core runtime-agnostic. A literal path (no `*`) resolves to itself if it exists.
5
+ function joinPath(a, b) {
6
+ if (!a)
7
+ return b;
8
+ return `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
9
+ }
10
+ /** Expand a single pattern (relative to `root`) to a sorted list of matching file paths. */
11
+ export async function expandPattern(host, root, pattern) {
12
+ if (!pattern.includes("*")) {
13
+ const p = joinPath(root, pattern);
14
+ return (await host.exists(p)) ? [p] : [];
15
+ }
16
+ const slash = pattern.lastIndexOf("/");
17
+ const dirPart = slash >= 0 ? pattern.slice(0, slash) : "";
18
+ const leaf = slash >= 0 ? pattern.slice(slash + 1) : pattern;
19
+ const dir = joinPath(root, dirPart);
20
+ let re;
21
+ if (leaf === "*") {
22
+ re = /.*/;
23
+ }
24
+ else if (leaf.startsWith("*.")) {
25
+ const ext = leaf.slice(1); // ".bpmn"
26
+ re = new RegExp(`${ext.replace(/[.]/g, "\\.")}$`);
27
+ }
28
+ else {
29
+ // Fall back to translating a simple leaf glob (`*` → `.*`).
30
+ re = new RegExp(`^${leaf.replace(/[.]/g, "\\.").replace(/\*/g, ".*")}$`);
31
+ }
32
+ const names = await host.listDir(dir);
33
+ return names
34
+ .filter((n) => re.test(n))
35
+ .sort()
36
+ .map((n) => joinPath(dir, n));
37
+ }
38
+ /** Expand many patterns, de-duplicating while preserving first-seen order. */
39
+ export async function expandPatterns(host, root, patterns) {
40
+ const seen = new Set();
41
+ const out = [];
42
+ for (const pat of patterns ?? []) {
43
+ for (const p of await expandPattern(host, root, pat)) {
44
+ if (!seen.has(p)) {
45
+ seen.add(p);
46
+ out.push(p);
47
+ }
48
+ }
49
+ }
50
+ return out;
51
+ }