@nanobpm/urban 0.4.0 → 0.5.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 (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 +21 -8
  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
@@ -0,0 +1,135 @@
1
+ // Deriver: BPMN models → `worker-io.d.ts`, the worker type map (ADR 0033 §3). Scans each
2
+ // process for service tasks and extracts the Zeebe task type plus the data-envelope in/out
3
+ // contract (io.nanobpm.dataEnvelope.in / .out, carried as zeebe:property). It emits the SAME
4
+ // `worker-io.d.ts` the console generates (a faithful port of `emitWorkerBindings` in the
5
+ // server's domain_types.ts), so the toolkit is a drop-in for the IDE's codegen (ADR 0053).
6
+ import { GENERATED_DIR } from "../artifact.js";
7
+ /** Filename the console uses; kept identical so the artifact is a drop-in. */
8
+ export const WORKER_BINDINGS_DTS = "worker-io.d.ts";
9
+ /** The domain type declarations `worker-io.d.ts` imports from (console: domain-rows.d.ts). */
10
+ export const DOMAIN_DTS = "domain-rows.d.ts";
11
+ const ENVELOPE_IN = "io.nanobpm.dataEnvelope.in";
12
+ const ENVELOPE_OUT = "io.nanobpm.dataEnvelope.out";
13
+ function attr(tag, name) {
14
+ const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
15
+ return m ? m[1] : undefined;
16
+ }
17
+ function processId(xml) {
18
+ const m = xml.match(/<bpmn:process\b[^>]*>/);
19
+ return m ? attr(m[0], "id") : undefined;
20
+ }
21
+ /** Scan one BPMN document for service-task worker I/O. */
22
+ export function scanModelWorkers(xml) {
23
+ const proc = processId(xml);
24
+ const out = [];
25
+ const blockRe = /<bpmn:serviceTask\b([^>]*)>([\s\S]*?)<\/bpmn:serviceTask>/g;
26
+ let m;
27
+ while ((m = blockRe.exec(xml)) !== null) {
28
+ const openAttrs = m[1];
29
+ const body = m[2];
30
+ const elementId = attr(`<x ${openAttrs}>`, "id");
31
+ const tdMatch = body.match(/<zeebe:taskDefinition\b[^>]*>/);
32
+ if (!tdMatch)
33
+ continue;
34
+ const taskType = attr(tdMatch[0], "type");
35
+ if (!taskType)
36
+ continue;
37
+ const io = { taskType, elementId, process: proc };
38
+ const propRe = /<zeebe:property\b[^>]*>/g;
39
+ let p;
40
+ while ((p = propRe.exec(body)) !== null) {
41
+ const pname = attr(p[0], "name");
42
+ const pvalue = attr(p[0], "value");
43
+ if (pname === ENVELOPE_IN && pvalue)
44
+ io.in = pvalue;
45
+ else if (pname === ENVELOPE_OUT && pvalue)
46
+ io.out = pvalue;
47
+ }
48
+ out.push(io);
49
+ }
50
+ return out;
51
+ }
52
+ // --- Faithful port of the console's emitWorkerBindings (domain_types.ts) so the emitted
53
+ // worker-io.d.ts is byte-compatible with the IDE's own codegen. ---
54
+ function typeRefFor(id, declared) {
55
+ return id != null && declared.has(id) ? `DomainTypes[${JSON.stringify(id)}]` : undefined;
56
+ }
57
+ /** Emit `worker-io.d.ts` from worker bindings + the set of declared domain type ids. */
58
+ export function emitWorkerBindings(workers, declaredTypeIds) {
59
+ const declared = new Set(declaredTypeIds);
60
+ const propKey = (t) => JSON.stringify(t);
61
+ const taskTypes = [
62
+ ...new Set(workers
63
+ .map((w) => w?.taskType)
64
+ .filter((t) => typeof t === "string" && t.length > 0)),
65
+ ];
66
+ const taskTypeUnion = taskTypes.length > 0
67
+ ? taskTypes.map((t) => JSON.stringify(t)).join(" | ")
68
+ : "string";
69
+ const inputs = [];
70
+ const outputs = [];
71
+ for (const w of workers) {
72
+ if (typeof w?.taskType !== "string" || w.taskType.length === 0)
73
+ continue;
74
+ const inRef = typeRefFor(w.inputType, declared);
75
+ if (inRef)
76
+ inputs.push(` ${propKey(w.taskType)}: ${inRef};`);
77
+ const outRef = typeRefFor(w.outputType, declared);
78
+ if (outRef)
79
+ outputs.push(` ${propKey(w.taskType)}: ${outRef};`);
80
+ }
81
+ const header = "// AUTO-GENERATED by nanobpmn from the App manifest (ADR 0033 §3).\n" +
82
+ "// The bridge from the process model to the worker type system: each declared\n" +
83
+ "// worker's `taskType` maps to the TS type of its input payload (`job.variables`)\n" +
84
+ "// and result, so the typed `defineWorker` types a handler by its job type. Do\n" +
85
+ "// not edit — regenerated from the manifest. Erased to plain JS at compile.\n" +
86
+ "// eslint-disable\n";
87
+ const needsRegistry = inputs.length > 0 || outputs.length > 0;
88
+ const importTypes = needsRegistry
89
+ ? `import type { DomainTypes } from "./${DOMAIN_DTS}";\n`
90
+ : "";
91
+ const inputsIface = inputs.length > 0
92
+ ? `export interface WorkerInputs {\n${inputs.join("\n")}\n}\n`
93
+ : `export interface WorkerInputs {}\n`;
94
+ const outputsIface = outputs.length > 0
95
+ ? `export interface WorkerOutputs {\n${outputs.join("\n")}\n}\n`
96
+ : `export interface WorkerOutputs {}\n`;
97
+ const headersIface = `export interface WorkerHeaders {}\n`;
98
+ return `${header}\n` +
99
+ importTypes +
100
+ `\n/** Untyped fallback for a job whose worker declares no input/output type. */\n` +
101
+ `export type WorkerVars = Record<string, unknown>;\n\n` +
102
+ `/** Untyped fallback for a job whose worker declares no custom headers. */\n` +
103
+ `export type WorkerHdrs = Record<string, unknown>;\n\n` +
104
+ `/** Every declared worker \`taskType\` (ADR 0033 §3): the model-derived set the\n` +
105
+ ` * typed \`defineWorker\` accepts, so \`type\` autocompletes and rejects unknown jobs. */\n` +
106
+ `export type WorkerTaskType = ${taskTypeUnion};\n\n` +
107
+ `/** Input payload (\`job.variables\`) per declared worker, keyed by \`taskType\`. */\n` +
108
+ inputsIface +
109
+ `\n/** Output payload (worker result) per declared worker, keyed by \`taskType\`. */\n` +
110
+ outputsIface +
111
+ `\n/** Custom headers (\`job.customHeaders\`) per declared worker, keyed by \`taskType\`.\n` +
112
+ ` * Header values are strings on the wire, so each declared key maps to \`string\`;\n` +
113
+ ` * the extra index signature keeps undeclared headers accessible (non-breaking). */\n` +
114
+ headersIface;
115
+ }
116
+ /**
117
+ * Derive `nano-generated/worker-io.d.ts` from BPMN models. `declaredTypeIds` is the set of
118
+ * domain type ids declared in the manifest `types` — an envelope in/out only becomes a typed
119
+ * ref when it names a declared type (matches the console's typeRefFor).
120
+ */
121
+ export function deriveWorkerBindings(models, declaredTypeIds = []) {
122
+ const workers = [];
123
+ for (const model of [...models].sort((a, b) => (a.path < b.path ? -1 : 1))) {
124
+ for (const w of scanModelWorkers(model.xml)) {
125
+ workers.push({ taskType: w.taskType, inputType: w.in, outputType: w.out });
126
+ }
127
+ }
128
+ const content = emitWorkerBindings(workers, declaredTypeIds);
129
+ return [{ path: `${GENERATED_DIR}/${WORKER_BINDINGS_DTS}`, content }];
130
+ }
131
+ export const workerIoDeriver = {
132
+ id: "model->worker-io",
133
+ describe: "Derive worker-io.d.ts (task type + data-envelope in/out) from BPMN models.",
134
+ derive: ({ models, declaredTypeIds }) => deriveWorkerBindings(models, declaredTypeIds),
135
+ };
@@ -0,0 +1,2 @@
1
+ import type { GenIO } from "./gen.ts";
2
+ export declare function createNodeGenIO(): GenIO;
@@ -0,0 +1,35 @@
1
+ // Node/Deno filesystem implementation of the GenIO port. Uses node:fs/promises, which both
2
+ // Node and Deno provide, so a single implementation serves both runtimes.
3
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ export function createNodeGenIO() {
6
+ return {
7
+ async readText(path) {
8
+ return readFile(path, "utf8");
9
+ },
10
+ async writeText(path, content) {
11
+ await mkdir(dirname(path), { recursive: true });
12
+ await writeFile(path, content);
13
+ },
14
+ async listDir(path) {
15
+ try {
16
+ // The GenIO contract is file names only — exclude subdirectories so
17
+ // pattern expansion never hands a directory to readText()/JSON.parse.
18
+ const entries = await readdir(path, { withFileTypes: true });
19
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ },
25
+ async exists(path) {
26
+ try {
27
+ await stat(path);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,28 @@
1
+ import type { DerivedArtifact } from "./artifact.ts";
2
+ /** Minimal filesystem port. Node/Deno impls live in `fsio.ts`. */
3
+ export interface GenIO {
4
+ readText(path: string): Promise<string>;
5
+ writeText(path: string, content: string): Promise<void>;
6
+ /** File names (not paths) in a directory; empty if it does not exist. */
7
+ listDir(path: string): Promise<string[]>;
8
+ exists(path: string): Promise<boolean>;
9
+ }
10
+ export interface GenOptions {
11
+ root: string;
12
+ io: GenIO;
13
+ manifestFile?: string;
14
+ }
15
+ export interface GenResult {
16
+ artifacts: DerivedArtifact[];
17
+ /** Paths that differ from disk (only populated by `check`). */
18
+ drift: string[];
19
+ }
20
+ declare function join(root: string, rel: string): string;
21
+ declare function dirOf(p: string): string;
22
+ /** Collect all artifacts a run would produce, without touching disk beyond reads. */
23
+ export declare function collectArtifacts(opts: GenOptions): Promise<DerivedArtifact[]>;
24
+ /** Run the derivers and write artifacts (or, with `check`, report drift without writing). */
25
+ export declare function runGen(opts: GenOptions & {
26
+ check?: boolean;
27
+ }): Promise<GenResult>;
28
+ export { dirOf, join as joinPath };
@@ -0,0 +1,75 @@
1
+ // The gen orchestrator: the one impure edge of the toolkit. It reads an app's manifest and
2
+ // models, runs the pure derivers, and either writes the artifacts to disk (`urban gen`) or
3
+ // compares them against what's on disk and reports drift (`urban gen --check`). The derivers stay
4
+ // pure; all IO is confined here behind a tiny FS port so the same code runs on Node and Deno.
5
+ import { sortArtifacts } from "./artifact.js";
6
+ import { deriveMigrations } from "./derivers/migrations.js";
7
+ import { deriveWorkerBindings } from "./derivers/worker-io.js";
8
+ function join(root, rel) {
9
+ // Trim either separator so callers may pass Windows-style paths; GenIO
10
+ // implementations accept forward slashes on all platforms.
11
+ return `${root.replace(/[/\\]+$/, "")}/${rel.replace(/^[/\\]+/, "")}`;
12
+ }
13
+ function dirOf(p) {
14
+ const i = p.lastIndexOf("/");
15
+ return i <= 0 ? "." : p.slice(0, i);
16
+ }
17
+ /** Resolve a `dir/*.ext` (or literal) manifest pattern to file paths relative to root. */
18
+ async function expandPattern(root, io, pattern) {
19
+ const star = pattern.indexOf("*");
20
+ if (star === -1) {
21
+ return (await io.exists(join(root, pattern))) ? [pattern] : [];
22
+ }
23
+ const slash = pattern.lastIndexOf("/", star);
24
+ const dir = slash === -1 ? "." : pattern.slice(0, slash);
25
+ const tail = pattern.slice(slash + 1); // e.g. "*.bpmn"
26
+ const ext = tail.startsWith("*") ? tail.slice(1) : tail;
27
+ const names = await io.listDir(join(root, dir));
28
+ return names
29
+ .filter((n) => n.endsWith(ext))
30
+ .map((n) => (dir === "." ? n : `${dir}/${n}`))
31
+ .sort();
32
+ }
33
+ /** Collect all artifacts a run would produce, without touching disk beyond reads. */
34
+ export async function collectArtifacts(opts) {
35
+ const { root, io } = opts;
36
+ const manifestPath = join(root, opts.manifestFile ?? "nano.app.json");
37
+ const manifest = JSON.parse(await io.readText(manifestPath));
38
+ const artifacts = [];
39
+ // 1. types → migrations
40
+ if (manifest.types && Object.keys(manifest.types).length > 0) {
41
+ artifacts.push(...deriveMigrations(manifest));
42
+ }
43
+ // 2. models → worker I/O index
44
+ const procPatterns = manifest.models?.processes ?? [];
45
+ const models = [];
46
+ for (const pat of procPatterns) {
47
+ for (const rel of await expandPattern(root, io, pat)) {
48
+ models.push({ path: rel, xml: await io.readText(join(root, rel)) });
49
+ }
50
+ }
51
+ if (models.length > 0) {
52
+ const declaredTypeIds = Object.keys(manifest.types ?? {});
53
+ artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
54
+ }
55
+ return sortArtifacts(artifacts);
56
+ }
57
+ /** Run the derivers and write artifacts (or, with `check`, report drift without writing). */
58
+ export async function runGen(opts) {
59
+ const artifacts = await collectArtifacts(opts);
60
+ const { root, io } = opts;
61
+ const drift = [];
62
+ for (const a of artifacts) {
63
+ const abs = join(root, a.path);
64
+ if (opts.check) {
65
+ const current = (await io.exists(abs)) ? await io.readText(abs) : null;
66
+ if (current !== a.content)
67
+ drift.push(a.path);
68
+ }
69
+ else {
70
+ await io.writeText(abs, a.content);
71
+ }
72
+ }
73
+ return { artifacts, drift };
74
+ }
75
+ export { dirOf, join as joinPath };
@@ -0,0 +1,13 @@
1
+ export type { DerivedArtifact, Deriver } from "./artifact.ts";
2
+ export { GENERATED_DIR, sortArtifacts } from "./artifact.ts";
3
+ export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.ts";
4
+ export type { ToolkitManifest, ToolkitType, ToolkitField } from "./derivers/migrations.ts";
5
+ export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.ts";
6
+ export type { ModelSource, WorkerIo, WorkerBindingDecl } from "./derivers/worker-io.ts";
7
+ export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
8
+ models: import("./derivers/worker-io.ts").ModelSource[];
9
+ declaredTypeIds: Iterable<string>;
10
+ }>];
11
+ export { collectArtifacts, runGen, joinPath } from "./gen.ts";
12
+ export type { GenIO, GenOptions, GenResult } from "./gen.ts";
13
+ export { createNodeGenIO } from "./fsio.ts";
@@ -0,0 +1,15 @@
1
+ // The Urban derivation toolkit — the shared derivation library. The IDE and the
2
+ // `urban gen` CLI are peer callers of these pure derivers (ADR 0053). Code-first
3
+ // process authoring is provided by `@nanobpm/workflow` (`defineFlow`), not here
4
+ // (ADR 0054) — the toolkit derives migrations and worker I/O from the model.
5
+ export { GENERATED_DIR, sortArtifacts } from "./artifact.js";
6
+ // Derivers
7
+ export { deriveMigrations, migrationsDeriver, sqlType, createTableSql, } from "./derivers/migrations.js";
8
+ export { deriveWorkerBindings, emitWorkerBindings, workerIoDeriver, scanModelWorkers, WORKER_BINDINGS_DTS, DOMAIN_DTS, } from "./derivers/worker-io.js";
9
+ // The registry of all derivers (for discovery / IDE migration).
10
+ import { migrationsDeriver } from "./derivers/migrations.js";
11
+ import { workerIoDeriver } from "./derivers/worker-io.js";
12
+ export const DERIVERS = [migrationsDeriver, workerIoDeriver];
13
+ // Gen orchestrator + IO
14
+ export { collectArtifacts, runGen, joinPath } from "./gen.js";
15
+ export { createNodeGenIO } from "./fsio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,27 +19,40 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/jwulf/nano-ide.git",
22
+ "url": "git+https://github.com/jwulf/nano-ide.git",
23
23
  "directory": "packages/urban"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=22.6"
27
27
  },
28
28
  "bin": {
29
- "urban": "./src/bin.mjs"
29
+ "urban": "src/bin.mjs"
30
30
  },
31
- "main": "./src/index.ts",
32
- "types": "./src/index.ts",
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
33
  "exports": {
34
- ".": "./src/index.ts",
35
- "./runtime": "./src/runtime/index.ts",
36
- "./toolkit": "./src/toolkit/index.ts"
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./runtime": {
39
+ "types": "./dist/runtime/index.d.ts",
40
+ "default": "./dist/runtime/index.js"
41
+ },
42
+ "./toolkit": {
43
+ "types": "./dist/toolkit/index.d.ts",
44
+ "default": "./dist/toolkit/index.js"
45
+ },
46
+ "./source": "./src/index.ts"
37
47
  },
38
48
  "files": [
49
+ "dist",
39
50
  "src",
40
51
  "README.md"
41
52
  ],
42
53
  "scripts": {
54
+ "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs');fs.mkdirSync('dist/runtime/schema',{recursive:true});fs.copyFileSync('src/runtime/schema/nano-app.schema.json','dist/runtime/schema/nano-app.schema.json')\"",
55
+ "prepack": "npm run build",
43
56
  "typecheck": "tsc --noEmit -p tsconfig.json",
44
57
  "test": "node --test --experimental-strip-types \"src/**/*.test.ts\"",
45
58
  "test:deno": "deno test -A --no-check src/"
package/src/bin.mjs CHANGED
@@ -1,21 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // Bootstrap for the `urban` CLI. The CLI is shipped as TypeScript source (one source of
3
- // truth, no build step). Node strips types natively, but versions < 23.6 need the
4
- // `--experimental-strip-types` flag, so we re-exec node with it when required.
2
+ // Bootstrap for the `urban` CLI. Runs the compiled CLI (dist/cli.js). The package is
3
+ // published as compiled JS + .d.ts because Node cannot strip types for files under
4
+ // node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so no type-stripping flag
5
+ // is needed here.
5
6
 
6
7
  import { spawnSync } from "node:child_process";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { dirname, join } from "node:path";
9
10
 
10
- const cli = join(dirname(fileURLToPath(import.meta.url)), "cli.ts");
11
- const [maj, min] = process.versions.node.split(".").map(Number);
12
- const needsFlag = maj < 23 || (maj === 23 && min < 6);
11
+ const cli = join(dirname(fileURLToPath(import.meta.url)), "..", "dist", "cli.js");
13
12
 
14
- const args = needsFlag
15
- ? ["--disable-warning=ExperimentalWarning", "--experimental-strip-types", cli, ...process.argv.slice(2)]
16
- : [cli, ...process.argv.slice(2)];
17
-
18
- const r = spawnSync(process.execPath, args, { stdio: "inherit" });
13
+ const r = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], { stdio: "inherit" });
19
14
  if (r.error) {
20
15
  console.error(String(r.error.message ?? r.error));
21
16
  process.exit(1);
package/src/cli.ts CHANGED
@@ -19,8 +19,21 @@ import {
19
19
  import { scaffold, slugify } from "create-urban-app";
20
20
  import { createNodeGenIO, runGen } from "./toolkit/index.ts";
21
21
  import { pathToFileURL } from "node:url";
22
+ import { readFileSync } from "node:fs";
23
+
24
+ // Read the package version from package.json (one hop up from both src/cli.ts and the
25
+ // compiled dist/cli.js) so `urban --version` never drifts from the published version.
26
+ function readVersion(): string {
27
+ try {
28
+ const url = new URL("../package.json", import.meta.url);
29
+ return (JSON.parse(readFileSync(url, "utf8")) as { version?: string }).version ?? "0.0.0";
30
+ } catch {
31
+ return "0.0.0";
32
+ }
33
+ }
34
+
35
+ const VERSION = readVersion();
22
36
 
23
- const VERSION = "0.2.0";
24
37
 
25
38
  interface Flags {
26
39
  root: string;
package/src/index.ts CHANGED
@@ -15,6 +15,13 @@
15
15
  // Runtime — app materialization + hosting
16
16
  export * from "./runtime/index.ts";
17
17
 
18
+ // The nano-sdk engine client factory: build a client to talk to a Nano/Camunda
19
+ // engine directly, or type an `AppApi.sdk`. The runtime uses this same factory, so a
20
+ // code-first author reaches the full engine surface without a separate
21
+ // `@nanobpm/nano-sdk` dependency. (`EngineSdkClient`, its return type, is re-exported
22
+ // from ./runtime/index.ts above.)
23
+ export { createCamundaClient } from "@nanobpm/nano-sdk";
24
+
18
25
  // Toolkit — derivation library (`urban gen`)
19
26
  export * from "./toolkit/index.ts";
20
27
 
@@ -1,5 +1,6 @@
1
1
  import type { AppManifest } from "./manifest.ts";
2
2
  import type { EngineClient, HostContext } from "./host.ts";
3
+ import type { EngineSdkClient } from "../engine/sdk.ts";
3
4
 
4
5
  /** Everything a runtime module needs. Passed to each module's mount function. */
5
6
  export interface RuntimeContext {
@@ -30,6 +31,15 @@ export interface AppApi {
30
31
  manifest: AppManifest;
31
32
  data: DataLayer;
32
33
  engine: EngineClient;
34
+ /**
35
+ * The underlying `@nanobpm/nano-sdk` engine client, present when the app runs on
36
+ * the nano-sdk transport (the default). It exposes the full Camunda
37
+ * orchestration-cluster surface — decisions, cluster variables, incidents, user
38
+ * tasks, agents, batch operations — beyond the transport-agnostic `engine` seam,
39
+ * over the same connection. Undefined when a non-SDK engine is injected (e.g. an
40
+ * in-memory test double).
41
+ */
42
+ sdk?: EngineSdkClient;
33
43
  env(name: string): string | undefined;
34
44
  log(level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>): void;
35
45
  }
@@ -69,7 +69,7 @@ async function makeFixture(): Promise<string> {
69
69
  `export const handlers = {
70
70
  "wf.claim": async (job, app) => {
71
71
  app.data.repo("task").insert({ title: job.variables.title, status: "claimed" });
72
- return { claimed: true };
72
+ return { claimed: true, hasSdk: !!app.sdk, sdkMarker: app.sdk?.__marker };
73
73
  },
74
74
  };`,
75
75
  );
@@ -154,6 +154,51 @@ test("runtime materializes the manifest end-to-end against a fake engine", async
154
154
  }
155
155
  });
156
156
 
157
+ test("threads the engine SDK client onto app.sdk for handlers (SDK-backed engine)", async () => {
158
+ const dir = await makeFixture();
159
+ const host = createNodeHost({ cwd: dir, log: () => {} });
160
+ class SdkEngine extends FakeEngine {
161
+ sdk = { __marker: "engine-sdk" };
162
+ }
163
+ const engine = new SdkEngine();
164
+ const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
165
+ await app.start();
166
+
167
+ try {
168
+ const out = (await engine.deliver("wf.claim", {
169
+ jobKey: "j1",
170
+ jobType: "wf.claim",
171
+ variables: { title: "Fix bug" },
172
+ })) as Record<string, unknown>;
173
+ assert.equal(out.hasSdk, true, "handler saw app.sdk");
174
+ assert.equal(out.sdkMarker, "engine-sdk", "app.sdk is the engine's own client");
175
+ } finally {
176
+ await app.stop();
177
+ await rm(dir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ test("app.sdk is undefined when the engine exposes no SDK client", async () => {
182
+ const dir = await makeFixture();
183
+ const host = createNodeHost({ cwd: dir, log: () => {} });
184
+ const engine = new FakeEngine();
185
+ const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
186
+ await app.start();
187
+
188
+ try {
189
+ const out = (await engine.deliver("wf.claim", {
190
+ jobKey: "j1",
191
+ jobType: "wf.claim",
192
+ variables: { title: "Fix bug" },
193
+ })) as Record<string, unknown>;
194
+ assert.equal(out.hasSdk, false, "no app.sdk for a non-SDK engine");
195
+ assert.equal(out.sdkMarker, undefined);
196
+ } finally {
197
+ await app.stop();
198
+ await rm(dir, { recursive: true, force: true });
199
+ }
200
+ });
201
+
157
202
  test("stop() resets state so the app can be cleanly restarted", async () => {
158
203
  const dir = await makeFixture();
159
204
  const host = createNodeHost({ cwd: dir, log: () => {} });
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { AppApi, Mounted } from "./context.ts";
7
7
  import type { EngineClient, HostContext, HttpServer } from "./host.ts";
8
+ import type { EngineSdkClient } from "../engine/sdk.ts";
8
9
  import { loadManifest, type AppManifest } from "./manifest.ts";
9
10
  import { validateManifest } from "./validate.ts";
10
11
  import { makeRouter, type Route } from "./router.ts";
@@ -27,6 +28,13 @@ export function resolvePort(explicit: number | undefined, envPort: string | unde
27
28
  return n;
28
29
  }
29
30
 
31
+ /** Read the underlying nano-sdk client off an engine when it exposes one (the
32
+ * `SdkEngineClient` does). Non-SDK engines (e.g. an in-memory test double) have no
33
+ * `sdk`, so handlers get `undefined` and fall back to the transport-agnostic seam. */
34
+ function engineSdk(engine: EngineClient): EngineSdkClient | undefined {
35
+ return (engine as { sdk?: EngineSdkClient }).sdk;
36
+ }
37
+
30
38
  export interface MountFlags {
31
39
  deploy?: boolean;
32
40
  data?: boolean;
@@ -152,6 +160,7 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
152
160
  manifest,
153
161
  data,
154
162
  engine,
163
+ sdk: engineSdk(engine),
155
164
  env: (n) => host.env(n),
156
165
  log: (l, m, f) => host.log(l, m, f),
157
166
  };
@@ -254,6 +254,12 @@ test("close stops every worker and closes the SDK client", async () => {
254
254
  assert.equal(client.closed, 1);
255
255
  });
256
256
 
257
+ test("exposes the underlying nano-sdk client as .sdk", () => {
258
+ const client = fakeSdkClient();
259
+ const engine = new SdkEngineClient(client);
260
+ assert.equal(engine.sdk, client, "sdk returns the exact client the adapter was built from");
261
+ });
262
+
257
263
  test("createNanoSdkEngineClient uses an injected client", async () => {
258
264
  const client = fakeSdkClient();
259
265
  const engine = await createNanoSdkEngineClient({ restAddress: "http://x/v2", client });
@@ -17,6 +17,7 @@ import type {
17
17
  JobHandler,
18
18
  WorkerSubscription,
19
19
  } from "../core/host.ts";
20
+ import type { EngineSdkClient } from "./sdk.ts";
20
21
 
21
22
  /** Coerce an engine response's process-instance key to a non-empty string, or
22
23
  * throw — a missing key means a malformed/partial response, not a real instance. */
@@ -113,6 +114,15 @@ export class SdkEngineClient implements EngineClient {
113
114
  this.log = log;
114
115
  }
115
116
 
117
+ /**
118
+ * The underlying `@nanobpm/nano-sdk` client — the full Camunda orchestration-cluster
119
+ * surface (decisions, cluster variables, incidents, agents, batch operations, …) over
120
+ * the same connection. Surfaced to handlers as `AppApi.sdk`.
121
+ */
122
+ get sdk(): EngineSdkClient {
123
+ return this.client as unknown as EngineSdkClient;
124
+ }
125
+
116
126
  async deployResources(
117
127
  resources: { name: string; content: string; contentType: string }[],
118
128
  ): Promise<{ deployed: number }> {
@@ -0,0 +1,18 @@
1
+ // The full nano-sdk engine client, surfaced to code-first Urban authors.
2
+ //
3
+ // `AppApi.engine` is the transport-agnostic seam (deploy, create, message, user
4
+ // tasks, workers). When the app runs on the nano-sdk transport, `AppApi.sdk` also
5
+ // carries the underlying `@nanobpm/nano-sdk` client, giving handlers the whole
6
+ // Camunda orchestration-cluster surface — decisions, cluster variables, incidents,
7
+ // agents, batch operations, and more — over the same connection, without a separate
8
+ // `@nanobpm/nano-sdk` dependency.
9
+ //
10
+ // This module is type-only: the `import(...)` type query resolves nano-sdk's types at
11
+ // typecheck and is fully erased at runtime, so re-exporting it from the runtime barrel
12
+ // adds no load-time dependency on nano-sdk.
13
+
14
+ /** The full nano-sdk (Camunda orchestration-cluster) engine client type — the value
15
+ * returned by `createCamundaClient`. */
16
+ export type EngineSdkClient = ReturnType<
17
+ typeof import("@nanobpm/nano-sdk").createCamundaClient
18
+ >;
@@ -67,5 +67,6 @@ export type {
67
67
  NanoSdkJobWorker,
68
68
  NanoSdkJobWorkerConfig,
69
69
  } from "./engine/nanosdk.ts";
70
+ export type { EngineSdkClient } from "./engine/sdk.ts";
70
71
  export { runFromEnv } from "./run.ts";
71
72
  export type { RunOptions } from "./run.ts";