@nanobpm/urban 0.1.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.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # `@nanobpm/urban`
2
+
3
+ The `urban` command-line tool: scaffold, validate, generate and run an Urban app
4
+ from your terminal — on **Node or Deno**.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm i -g @nanobpm/urban # install the `urban` command
10
+ # or run without installing:
11
+ npx @nanobpm/urban new my-app
12
+ ```
13
+
14
+ Requires Node ≥ 22.6 or Deno. On Node < 23.6 the `urban` command re-executes with
15
+ `--experimental-strip-types` so its TypeScript source runs directly.
16
+
17
+ Under Deno you can run it without installing:
18
+
19
+ ```bash
20
+ deno run -A npm:@nanobpm/urban run
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ | Command | What it does |
26
+ |---|---|
27
+ | `urban new <name>` | scaffold a new app in a new directory |
28
+ | `urban check` | validate the app's `nano.app.json` manifest |
29
+ | `urban gen` | generate the `nano-generated/` artifacts (migrations, worker I/O, models) |
30
+ | `urban gen --check` | fail if the generated artifacts are out of date (a CI drift gate) |
31
+ | `urban run` | generate, then run the app — starts its workers and serves its surfaces |
32
+ | `urban dev` | run the app (hot-reload is not yet implemented) |
33
+ | `urban deploy` | deploy the app's models to the engine, then exit |
34
+
35
+ ## Options
36
+
37
+ | Flag | Purpose | Default |
38
+ |---|---|---|
39
+ | `--root <dir>` | app directory | `.` |
40
+ | `--manifest <file>` | manifest filename | `nano.app.json` |
41
+ | `--port <n>` | HTTP port for surfaces and triggers (integer 0–65535) | `$PORT` or `8090` |
42
+ | `-h`, `--help` | show help | |
43
+ | `-v`, `--version` | print the version | |
44
+
45
+ The engine address comes from `$CAMUNDA_REST_ADDRESS` (default
46
+ `http://localhost:8080/v2`). Set `$CAMUNDA_TRANSPORT` to a non-`rest` value to use
47
+ the `@nanobpm/nano-sdk` transport when it is installed.
48
+
49
+ ## A typical session
50
+
51
+ ```bash
52
+ urban new invoices && cd invoices
53
+ urban gen # generate nano-generated/
54
+ urban check # validate the manifest
55
+ urban run # start workers and serve surfaces
56
+ ```
57
+
58
+ ## Related packages
59
+
60
+ - [`create-urban-app`](../create-urban-app) — the scaffolder behind `urban new`.
61
+ - [`@nanobpm/urban-toolkit`](../urban-toolkit) — the generation behind `urban gen`.
62
+ - [`@nanobpm/urban-runtime`](../urban-runtime) — the runtime behind `urban run`.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nanobpm/urban",
3
+ "version": "0.1.0",
4
+ "description": "The Urban CLI: scaffold, check, run, dev, and deploy Urban apps (nano.app.json) on Node or Deno. Thin command surface over @nanobpm/urban-runtime (ADR 0052).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "keywords": [
8
+ "nanobpm",
9
+ "urban",
10
+ "urban-cli",
11
+ "cli",
12
+ "nano-app",
13
+ "rad"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/jwulf/nano-ide.git",
21
+ "directory": "packages/urban-cli"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.6"
25
+ },
26
+ "bin": {
27
+ "urban": "./src/bin.mjs"
28
+ },
29
+ "main": "./src/cli.ts",
30
+ "types": "./src/cli.ts",
31
+ "exports": {
32
+ ".": "./src/cli.ts"
33
+ },
34
+ "files": [
35
+ "src",
36
+ "README.md"
37
+ ],
38
+ "scripts": {
39
+ "typecheck": "tsc --noEmit -p tsconfig.json",
40
+ "test": "node --test --experimental-strip-types \"src/**/*.test.ts\""
41
+ },
42
+ "dependencies": {
43
+ "@nanobpm/urban-runtime": "^0.1.0",
44
+ "@nanobpm/urban-toolkit": "^0.1.0",
45
+ "create-urban-app": "^0.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.20.1",
49
+ "typescript": "^5.9.3"
50
+ }
51
+ }
package/src/bin.mjs ADDED
@@ -0,0 +1,23 @@
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.
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+
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);
13
+
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" });
19
+ if (r.error) {
20
+ console.error(String(r.error.message ?? r.error));
21
+ process.exit(1);
22
+ }
23
+ process.exit(r.status ?? 1);
package/src/cli.ts ADDED
@@ -0,0 +1,215 @@
1
+ // The `urban` CLI — a thin command surface over @nanobpm/urban-runtime.
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 run materialize + serve the app (deploy, data, workers, surfaces, triggers)
6
+ // urban dev like run (hot-reload is not yet implemented; documented below)
7
+ // urban deploy deploy models only, then exit
8
+ //
9
+ // Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
10
+ // --port <n>, -h/--help, -v/--version.
11
+
12
+ import {
13
+ collectManifestIssues,
14
+ loadManifest,
15
+ runFromEnv,
16
+ selectHost,
17
+ } from "@nanobpm/urban-runtime";
18
+ import { scaffold, slugify } from "create-urban-app";
19
+ import { createNodeGenIO, runGen } from "@nanobpm/urban-toolkit";
20
+ import { pathToFileURL } from "node:url";
21
+
22
+ const VERSION = "0.1.0";
23
+
24
+ interface Flags {
25
+ root: string;
26
+ manifest: string;
27
+ port?: number;
28
+ check: boolean;
29
+ help: boolean;
30
+ version: boolean;
31
+ _: string[];
32
+ }
33
+
34
+ function parse(argv: string[]): Flags {
35
+ const f: Flags = { root: ".", manifest: "nano.app.json", check: false, help: false, version: false, _: [] };
36
+ const need = (i: number, flag: string): string => {
37
+ const v = argv[i];
38
+ if (v === undefined || v.startsWith("-")) throw new Error(`flag ${flag} requires a value`);
39
+ return v;
40
+ };
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === "-h" || a === "--help") f.help = true;
44
+ else if (a === "-v" || a === "--version") f.version = true;
45
+ else if (a === "--check") f.check = true;
46
+ else if (a === "--root") f.root = need(++i, a);
47
+ else if (a === "--manifest") f.manifest = need(++i, a);
48
+ else if (a === "--port") {
49
+ const raw = need(++i, a);
50
+ const n = Number(raw);
51
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
52
+ throw new Error(`flag --port requires an integer in 0..65535 (got "${raw}")`);
53
+ }
54
+ f.port = n;
55
+ } else if (a.startsWith("-")) throw new Error(`unknown flag: ${a}`);
56
+ else f._.push(a);
57
+ }
58
+ return f;
59
+ }
60
+
61
+ const USAGE = `urban — build and run Urban apps (nano.app.json)
62
+
63
+ Usage:
64
+ urban new <name> [--root <path>] scaffold a new Urban app
65
+ urban check validate the manifest
66
+ urban gen [--check] derive artifacts (migrations, worker-io, models)
67
+ urban run materialize + serve the app
68
+ urban dev run (hot-reload not yet implemented)
69
+ urban deploy deploy models only, then exit
70
+
71
+ Global flags:
72
+ --root <dir> app root (default ".")
73
+ --manifest <file> manifest filename (default "nano.app.json")
74
+ --port <n> HTTP port for surfaces/triggers (default $PORT or 8090)
75
+ -h, --help show this help
76
+ -v, --version print version
77
+
78
+ Engine address: $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2).
79
+ `;
80
+
81
+ async function cmdCheck(f: Flags): Promise<number> {
82
+ // The host is anchored at f.root, so manifest paths are already root-relative —
83
+ // pass the bare manifest filename (prefixing root again would double it).
84
+ const host = selectHost({ cwd: f.root });
85
+ const manifest = await loadManifest(host, f.manifest);
86
+ const issues = collectManifestIssues(manifest);
87
+ if (issues.length === 0) {
88
+ console.log(`✔ ${manifest.name ?? manifest.id} — manifest is valid`);
89
+ return 0;
90
+ }
91
+ console.error(`✖ ${issues.length} issue(s) in ${f.manifest}:`);
92
+ for (const it of issues) console.error(` • ${it.path}: ${it.message}`);
93
+ return 1;
94
+ }
95
+
96
+ async function cmdGen(f: Flags): Promise<number> {
97
+ const io = createNodeGenIO();
98
+ const res = await runGen({ root: f.root, io, manifestFile: f.manifest, check: f.check });
99
+ if (f.check) {
100
+ if (res.drift.length === 0) {
101
+ console.log(`✔ generated artifacts are up to date (${res.artifacts.length} checked)`);
102
+ return 0;
103
+ }
104
+ console.error(`✖ ${res.drift.length} generated artifact(s) are out of date — run \`urban gen\`:`);
105
+ for (const p of res.drift) console.error(` • ${p}`);
106
+ return 1;
107
+ }
108
+ console.log(`✔ generated ${res.artifacts.length} artifact(s):`);
109
+ for (const a of res.artifacts) console.log(` • ${a.path}`);
110
+ return 0;
111
+ }
112
+
113
+ async function cmdRun(f: Flags, mount?: Record<string, boolean>): Promise<number> {
114
+ const app = await runFromEnv({
115
+ root: f.root,
116
+ manifestPath: f.manifest,
117
+ port: f.port,
118
+ mount,
119
+ });
120
+ const info = app.inspect();
121
+ console.log(`▲ ${info.name} — surfaces on :${info.httpPort ?? "n/a"} (Ctrl-C to stop)`);
122
+ return -1; // keep the process alive; signal handlers stop it
123
+ }
124
+
125
+ async function cmdDeploy(f: Flags): Promise<number> {
126
+ const app = await runFromEnv({
127
+ root: f.root,
128
+ manifestPath: f.manifest,
129
+ mount: { deploy: true, data: false, workers: false, surfaces: false, triggers: false, security: false },
130
+ handleSignals: false,
131
+ });
132
+ console.log(`✔ deployed models for ${app.manifest.name ?? app.manifest.id}`);
133
+ await app.stop();
134
+ return 0;
135
+ }
136
+
137
+ async function cmdNew(f: Flags): Promise<number> {
138
+ const name = f._[1];
139
+ if (!name) {
140
+ console.error("usage: urban new <name> [--root <path>]");
141
+ return 1;
142
+ }
143
+ const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
144
+ const res = await scaffold({ name, dir });
145
+ console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
146
+ console.log(` cd ${res.dir} && (npm install && npm start) || deno task start`);
147
+ return 0;
148
+ }
149
+
150
+ export async function main(argv: string[]): Promise<number> {
151
+ let f: Flags;
152
+ try {
153
+ f = parse(argv);
154
+ } catch (err) {
155
+ console.error(String((err as Error).message));
156
+ return 1;
157
+ }
158
+ if (f.version) {
159
+ console.log(VERSION);
160
+ return 0;
161
+ }
162
+ const cmd = f._[0];
163
+ if (f.help || !cmd) {
164
+ console.log(USAGE);
165
+ return cmd ? 0 : f.help ? 0 : 1;
166
+ }
167
+ switch (cmd) {
168
+ case "new":
169
+ case "scaffold":
170
+ return cmdNew(f);
171
+ case "check":
172
+ return cmdCheck(f);
173
+ case "gen":
174
+ return cmdGen(f);
175
+ case "run":
176
+ return cmdRun(f);
177
+ case "dev":
178
+ console.log("urban dev: hot-reload is not yet implemented; running once.");
179
+ return cmdRun(f);
180
+ case "deploy":
181
+ return cmdDeploy(f);
182
+ default:
183
+ console.error(`unknown command: ${cmd}\n`);
184
+ console.log(USAGE);
185
+ return 1;
186
+ }
187
+ }
188
+
189
+ const g = globalThis as {
190
+ process?: { argv: string[]; exit(code: number): void };
191
+ Deno?: { args?: string[]; exit?: (code: number) => void };
192
+ };
193
+ const meta = import.meta as unknown as { main?: boolean; url: string };
194
+ const argv1 = g.process?.argv?.[1];
195
+ const nodeMain = argv1 ? meta.url === pathToFileURL(argv1).href : false;
196
+ if (meta.main === true || nodeMain) {
197
+ // Node passes args via process.argv (slice off exec+script); Deno (run directly)
198
+ // exposes them on Deno.args. Prefer whichever actually carries args.
199
+ const fromNode = g.process?.argv?.slice(2);
200
+ const fromDeno = g.Deno?.args;
201
+ const argv = (fromNode && fromNode.length ? fromNode : fromDeno) ?? fromNode ?? [];
202
+ const exit = (code: number) => {
203
+ if (g.process?.exit) g.process.exit(code);
204
+ else if (g.Deno?.exit) g.Deno.exit(code);
205
+ };
206
+ main(argv).then(
207
+ (code) => {
208
+ if (code >= 0) exit(code);
209
+ },
210
+ (err) => {
211
+ console.error(String((err as Error)?.message ?? err));
212
+ exit(1);
213
+ },
214
+ );
215
+ }