@nanobpm/urban 0.24.0 → 0.25.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.
|
@@ -1,14 +1,6 @@
|
|
|
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, AppUi, 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
|
-
}
|
|
12
4
|
/** The job type a worker subscribes to (schema key: `taskType`). */
|
|
13
5
|
export declare function workerJobType(w: Worker): string | undefined;
|
|
14
6
|
/**
|
|
@@ -131,9 +131,11 @@ export async function createUrbanApp(opts) {
|
|
|
131
131
|
describe.connectors = c.describe?.();
|
|
132
132
|
}
|
|
133
133
|
const routes = [];
|
|
134
|
+
let hasUiSurfaces = false;
|
|
134
135
|
if (flags.surfaces) {
|
|
135
136
|
const s = mountSurfaces(ctx, api);
|
|
136
137
|
routes.push(...s.routes);
|
|
138
|
+
hasUiSurfaces = s.routes.length > 0;
|
|
137
139
|
describe.surfaces = s.describe();
|
|
138
140
|
}
|
|
139
141
|
if (flags.triggers) {
|
|
@@ -160,6 +162,23 @@ export async function createUrbanApp(opts) {
|
|
|
160
162
|
server = await host.serveHttp(port, makeRouter(routes));
|
|
161
163
|
httpPort = server.port;
|
|
162
164
|
host.log("info", "urban app serving surfaces/triggers", { port: httpPort, routes: routes.length });
|
|
165
|
+
// ADR 0057 boot handshake: under a supervising host (the nano-bpm
|
|
166
|
+
// Studio sets NANOBPMN_APP_HANDSHAKE), announce the port we actually
|
|
167
|
+
// bound on a machine-readable stdout control line so the host can
|
|
168
|
+
// locate the app's UI even when the app picks its port at runtime.
|
|
169
|
+
// Only emitted when the app actually serves UI surfaces (a trigger- or
|
|
170
|
+
// worker-only app binds a port too, but it has no webview for the host
|
|
171
|
+
// to frame), and gated on the env var so direct terminal runs aren't
|
|
172
|
+
// cluttered. `=== "1"` so an app that sets the var to "0" opts out.
|
|
173
|
+
if (httpPort !== undefined && hasUiSurfaces && host.env("NANOBPMN_APP_HANDSHAKE") === "1") {
|
|
174
|
+
try {
|
|
175
|
+
console.log(`@@NBPM_LISTENING@@${JSON.stringify({ port: httpPort })}`);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// stdout may be closed / non-writable — the handshake is
|
|
179
|
+
// best-effort telemetry, never fatal to the app.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
163
182
|
}
|
|
164
183
|
host.log("info", `urban app "${manifest.id}" started`, {});
|
|
165
184
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"test:deno": "deno test -A --no-check src/"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@nanobpm/nano-app-schema": "^0.
|
|
65
|
+
"@nanobpm/nano-app-schema": "^0.3.0",
|
|
66
66
|
"@nanobpm/nano-sdk": "^1.2.3",
|
|
67
67
|
"@nanobpm/workflow": "^0.8.0",
|
|
68
68
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
@@ -33,20 +33,6 @@ export type {
|
|
|
33
33
|
Worker,
|
|
34
34
|
} from "@nanobpm/nano-app-schema";
|
|
35
35
|
|
|
36
|
-
// The published `@nanobpm/nano-app-schema` does not declare `models.templates`; the manifest
|
|
37
|
-
// validator only enforces `additionalProperties:false` at the top level (see validate.ts), so the
|
|
38
|
-
// nested key is accepted at runtime. Augment `Models` here so the deploy path can read it typed
|
|
39
|
-
// (no cast) until it is formalized in the schema package. (The top-level `ui` block, by contrast,
|
|
40
|
-
// now ships natively in the schema as `AppUi` — re-exported above — so it needs no shim.)
|
|
41
|
-
declare module "@nanobpm/nano-app-schema" {
|
|
42
|
-
interface Models {
|
|
43
|
-
/** `{{name}}` template sources substituted into model resources at deploy time: globs, a bare
|
|
44
|
-
* directory (scanned), or literal files. A template's name is its file stem
|
|
45
|
-
* (`prompts/review-round.md` → `{{review-round}}`). See `deployModels`. */
|
|
46
|
-
templates?: string[];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
36
|
/** The job type a worker subscribes to (schema key: `taskType`). */
|
|
51
37
|
export function workerJobType(w: Worker): string | undefined {
|
|
52
38
|
return w.taskType;
|
|
@@ -324,3 +324,81 @@ test("runFromEnv anchors the host at a non-'.' root without double-prefixing pat
|
|
|
324
324
|
await rm(dir, { recursive: true, force: true });
|
|
325
325
|
}
|
|
326
326
|
});
|
|
327
|
+
|
|
328
|
+
// Captures console.log for the duration of `fn`, restoring it after.
|
|
329
|
+
async function captureStdout(fn: () => Promise<void>): Promise<string[]> {
|
|
330
|
+
const lines: string[] = [];
|
|
331
|
+
const original = console.log;
|
|
332
|
+
console.log = (...args: unknown[]) => {
|
|
333
|
+
lines.push(args.map((a) => (typeof a === "string" ? a : String(a))).join(" "));
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
await fn();
|
|
337
|
+
} finally {
|
|
338
|
+
console.log = original;
|
|
339
|
+
}
|
|
340
|
+
return lines;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
test("emits the ADR 0057 boot handshake when NANOBPMN_APP_HANDSHAKE is set", async () => {
|
|
344
|
+
const dir = await makeFixture();
|
|
345
|
+
const host = createNodeHost({ cwd: dir, log: () => {} });
|
|
346
|
+
const engine = new FakeEngine();
|
|
347
|
+
const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
|
|
348
|
+
const prev = process.env.NANOBPMN_APP_HANDSHAKE;
|
|
349
|
+
process.env.NANOBPMN_APP_HANDSHAKE = "1";
|
|
350
|
+
try {
|
|
351
|
+
const lines = await captureStdout(() => app.start());
|
|
352
|
+
const handshake = lines.find((l) => l.startsWith("@@NBPM_LISTENING@@"));
|
|
353
|
+
assert.ok(handshake, "a @@NBPM_LISTENING@@ line was emitted");
|
|
354
|
+
const payload = JSON.parse(handshake!.slice("@@NBPM_LISTENING@@".length));
|
|
355
|
+
assert.equal(payload.port, app.httpPort, "handshake reports the actual bound port");
|
|
356
|
+
assert.ok(payload.port > 0);
|
|
357
|
+
} finally {
|
|
358
|
+
if (prev === undefined) delete process.env.NANOBPMN_APP_HANDSHAKE;
|
|
359
|
+
else process.env.NANOBPMN_APP_HANDSHAKE = prev;
|
|
360
|
+
await app.stop();
|
|
361
|
+
await rm(dir, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("does NOT emit the boot handshake without NANOBPMN_APP_HANDSHAKE", async () => {
|
|
366
|
+
const dir = await makeFixture();
|
|
367
|
+
const host = createNodeHost({ cwd: dir, log: () => {} });
|
|
368
|
+
const engine = new FakeEngine();
|
|
369
|
+
const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
|
|
370
|
+
const prev = process.env.NANOBPMN_APP_HANDSHAKE;
|
|
371
|
+
delete process.env.NANOBPMN_APP_HANDSHAKE;
|
|
372
|
+
try {
|
|
373
|
+
const lines = await captureStdout(() => app.start());
|
|
374
|
+
assert.ok(
|
|
375
|
+
!lines.some((l) => l.startsWith("@@NBPM_LISTENING@@")),
|
|
376
|
+
"no handshake line when the supervisor env is unset (clean terminal runs)",
|
|
377
|
+
);
|
|
378
|
+
} finally {
|
|
379
|
+
if (prev !== undefined) process.env.NANOBPMN_APP_HANDSHAKE = prev;
|
|
380
|
+
await app.stop();
|
|
381
|
+
await rm(dir, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("treats NANOBPMN_APP_HANDSHAKE other than \"1\" as opt-out", async () => {
|
|
386
|
+
const dir = await makeFixture();
|
|
387
|
+
const host = createNodeHost({ cwd: dir, log: () => {} });
|
|
388
|
+
const engine = new FakeEngine();
|
|
389
|
+
const app = await createUrbanApp({ host, engine, root: ".", port: 0 });
|
|
390
|
+
const prev = process.env.NANOBPMN_APP_HANDSHAKE;
|
|
391
|
+
process.env.NANOBPMN_APP_HANDSHAKE = "0";
|
|
392
|
+
try {
|
|
393
|
+
const lines = await captureStdout(() => app.start());
|
|
394
|
+
assert.ok(
|
|
395
|
+
!lines.some((l) => l.startsWith("@@NBPM_LISTENING@@")),
|
|
396
|
+
'"0" is not the opt-in sentinel ("1"), so no handshake is emitted',
|
|
397
|
+
);
|
|
398
|
+
} finally {
|
|
399
|
+
if (prev === undefined) delete process.env.NANOBPMN_APP_HANDSHAKE;
|
|
400
|
+
else process.env.NANOBPMN_APP_HANDSHAKE = prev;
|
|
401
|
+
await app.stop();
|
|
402
|
+
await rm(dir, { recursive: true, force: true });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
@@ -191,9 +191,11 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
const routes: Route[] = [];
|
|
194
|
+
let hasUiSurfaces = false;
|
|
194
195
|
if (flags.surfaces) {
|
|
195
196
|
const s = mountSurfaces(ctx, api);
|
|
196
197
|
routes.push(...s.routes);
|
|
198
|
+
hasUiSurfaces = s.routes.length > 0;
|
|
197
199
|
describe.surfaces = s.describe();
|
|
198
200
|
}
|
|
199
201
|
if (flags.triggers) {
|
|
@@ -219,6 +221,22 @@ export async function createUrbanApp(opts: CreateUrbanAppOptions): Promise<Urban
|
|
|
219
221
|
server = await host.serveHttp(port, makeRouter(routes));
|
|
220
222
|
httpPort = server.port;
|
|
221
223
|
host.log("info", "urban app serving surfaces/triggers", { port: httpPort, routes: routes.length });
|
|
224
|
+
// ADR 0057 boot handshake: under a supervising host (the nano-bpm
|
|
225
|
+
// Studio sets NANOBPMN_APP_HANDSHAKE), announce the port we actually
|
|
226
|
+
// bound on a machine-readable stdout control line so the host can
|
|
227
|
+
// locate the app's UI even when the app picks its port at runtime.
|
|
228
|
+
// Only emitted when the app actually serves UI surfaces (a trigger- or
|
|
229
|
+
// worker-only app binds a port too, but it has no webview for the host
|
|
230
|
+
// to frame), and gated on the env var so direct terminal runs aren't
|
|
231
|
+
// cluttered. `=== "1"` so an app that sets the var to "0" opts out.
|
|
232
|
+
if (httpPort !== undefined && hasUiSurfaces && host.env("NANOBPMN_APP_HANDSHAKE") === "1") {
|
|
233
|
+
try {
|
|
234
|
+
console.log(`@@NBPM_LISTENING@@${JSON.stringify({ port: httpPort })}`);
|
|
235
|
+
} catch {
|
|
236
|
+
// stdout may be closed / non-writable — the handshake is
|
|
237
|
+
// best-effort telemetry, never fatal to the app.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
222
240
|
}
|
|
223
241
|
host.log("info", `urban app "${manifest.id}" started`, {});
|
|
224
242
|
} catch (err) {
|