@nbt-dev/nbt 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nbt-dev/nbt
2
2
 
3
- The `nbt` CLI and `console` daemon for the nbt-dev console — a fixed-target platform for web development.
3
+ The `nbt` CLI and runtime for nbt-dev — a schema-first backend platform for web development.
4
4
 
5
5
  ## Install
6
6
 
@@ -16,12 +16,13 @@ npx nbt list # list available standard modules
16
16
  npx nbt add crm email # vendor standard cartridges into nbt/
17
17
  npx nbt update crm email # refresh vendored modules without deleting migrations
18
18
  # ...edit nbt/<cart>/schema.nbt (entities, indexes, @route decls)…
19
- npx nbt dev # boot the console + live-reload local carts on save
20
- npx nbt migrate create billing --baseline # first migration from the schema
19
+ npx nbt dev # boot the runtime + live-reload local carts on save
20
+ npx nbt migrate create billing -m "initial" --baseline # first migration from the schema
21
+ npx nbt status # what persistent changes are unrecorded?
21
22
  npx nbt generate # typed TS clients for the local cartridge set
22
23
 
23
- npx nbt migrate deploy # upload local source + apply migrations
24
- npx nbt migrate deploy --env prod # target another configured environment
24
+ npx nbt deploy # build + ship the whole project as one release
25
+ npx nbt deploy --env prod # target another configured environment
25
26
  ```
26
27
 
27
28
  Commands read `nbt.json` for paths and the target instance; `--host`/`--port`/`--root`/`--out`
@@ -29,7 +30,7 @@ flags override it. Run from anywhere inside the project — `nbt` walks up to fi
29
30
 
30
31
  Standard cartridges use a shadcn-style ownership model: `nbt add <name>` copies their source into
31
32
  your configured carts directory. The local copy is visible and editable, `nbt generate` follows
32
- that local cartridge set, and `nbt migrate deploy` uploads it to the configured console.
33
+ that local cartridge set, and `nbt deploy` ships it to the configured runtime as one release.
33
34
 
34
35
  ```bash
35
36
  npx nbt up # boot using nbt.json dev port
package/dist/nbt.js CHANGED
@@ -3134,9 +3134,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3134
3134
  * @param {string} [path]
3135
3135
  * @return {(string|null|Command)}
3136
3136
  */
3137
- executableDir(path4) {
3138
- if (path4 === void 0) return this._executableDir;
3139
- this._executableDir = path4;
3137
+ executableDir(path3) {
3138
+ if (path3 === void 0) return this._executableDir;
3139
+ this._executableDir = path3;
3140
3140
  return this;
3141
3141
  }
3142
3142
  /**
@@ -3393,48 +3393,33 @@ var program = new Command();
3393
3393
 
3394
3394
  // src/nbt.ts
3395
3395
  var import_node_child_process2 = require("node:child_process");
3396
- var net = __toESM(require("node:net"));
3397
- var path3 = __toESM(require("node:path"));
3398
- var fs2 = __toESM(require("node:fs"));
3399
- var readline = __toESM(require("node:readline/promises"));
3400
-
3401
- // src/bundler.ts
3402
- var import_esbuild = __toESM(require("esbuild"));
3403
3396
  var path2 = __toESM(require("node:path"));
3404
- var import_node_url = require("node:url");
3405
- async function bundleWorkflows(options) {
3406
- import_esbuild.default.buildSync({
3407
- entryPoints: options.entryPoints,
3408
- outfile: options.outfile,
3409
- bundle: true,
3410
- format: "cjs",
3411
- platform: "node"
3412
- });
3413
- const g = globalThis;
3414
- delete g.__nbt_bundler__;
3415
- await import((0, import_node_url.pathToFileURL)(path2.resolve(options.outfile)).href);
3416
- const bundler = g.__nbt_bundler__;
3417
- console.log(bundler.workflows, bundler.steps, bundler.actions);
3418
- }
3419
-
3420
- // src/nbt.ts
3421
- if (process.platform !== "linux" || process.arch !== "x64") {
3397
+ var fs2 = __toESM(require("node:fs"));
3398
+ var os = __toESM(require("node:os"));
3399
+ var VENDOR_DIRS = {
3400
+ "linux-x64": "linux-x64",
3401
+ "darwin-arm64": "darwin-arm64",
3402
+ "darwin-x64": "darwin-x64"
3403
+ };
3404
+ var PLATFORM_KEY = `${process.platform}-${process.arch}`;
3405
+ var VENDOR_SUBDIR = VENDOR_DIRS[PLATFORM_KEY];
3406
+ if (!VENDOR_SUBDIR) {
3422
3407
  console.error(
3423
- `@nbt-dev/nbt ships linux/x64 prebuilt binaries only; this is ${process.platform}/${process.arch}.`
3408
+ `@nbt-dev/nbt has no prebuilt binaries for ${PLATFORM_KEY} (supported: ${Object.keys(VENDOR_DIRS).join(", ")}).`
3424
3409
  );
3425
3410
  process.exit(1);
3426
3411
  }
3427
- var VENDOR = path3.join(__dirname, "..", "vendor", "linux-x64");
3428
- var STDLIB = path3.join(__dirname, "..", "stdlib");
3429
- var NBT_BIN = path3.join(VENDOR, "nbt");
3430
- var CONSOLE_BIN = path3.join(VENDOR, "console");
3412
+ var IS_MAC = process.platform === "darwin";
3413
+ var VENDOR = path2.join(__dirname, "..", "vendor", VENDOR_SUBDIR);
3414
+ var NBT_BIN = path2.join(VENDOR, "nbt");
3415
+ var CONSOLE_BIN = path2.join(VENDOR, "console");
3431
3416
  function nativeEnv() {
3432
3417
  return { ...process.env };
3433
3418
  }
3434
3419
  function corsOriginsFromNbtJson() {
3435
3420
  let dir = process.cwd();
3436
3421
  for (let i = 0; i < 32; i++) {
3437
- const candidate = path3.join(dir, "nbt.json");
3422
+ const candidate = path2.join(dir, "nbt.json");
3438
3423
  if (fs2.existsSync(candidate)) {
3439
3424
  try {
3440
3425
  const cfg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
@@ -3446,7 +3431,7 @@ function corsOriginsFromNbtJson() {
3446
3431
  }
3447
3432
  return [];
3448
3433
  }
3449
- const parent = path3.dirname(dir);
3434
+ const parent = path2.dirname(dir);
3450
3435
  if (parent === dir) break;
3451
3436
  dir = parent;
3452
3437
  }
@@ -3460,15 +3445,49 @@ function consoleEnv() {
3460
3445
  }
3461
3446
  return env;
3462
3447
  }
3448
+ function projectUpPort() {
3449
+ let dir = process.cwd();
3450
+ for (let i = 0; i < 32; i++) {
3451
+ const candidate = path2.join(dir, "nbt.json");
3452
+ if (fs2.existsSync(candidate)) {
3453
+ try {
3454
+ const cfg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
3455
+ return Number(cfg?.environments?.dev?.port) || 8080;
3456
+ } catch {
3457
+ break;
3458
+ }
3459
+ }
3460
+ const parent = path2.dirname(dir);
3461
+ if (parent === dir) break;
3462
+ dir = parent;
3463
+ }
3464
+ return 8080;
3465
+ }
3466
+ function withProjectUpDefaults(argv2) {
3467
+ const hasPort = argv2.some(
3468
+ (arg) => arg === "--port" || arg === "-p" || arg.startsWith("--port=")
3469
+ );
3470
+ if (hasPort) return argv2;
3471
+ return [...argv2, "--port", String(projectUpPort())];
3472
+ }
3463
3473
  function forwardToNative(argv2) {
3464
- const isConsole = consoleCommands.has(argv2[0]);
3474
+ const cmd = argv2[0];
3475
+ const isConsole = consoleCommands.has(cmd);
3465
3476
  const bin = isConsole ? CONSOLE_BIN : NBT_BIN;
3466
- const env = isConsole ? consoleEnv() : nativeEnv();
3467
- const nativeArgv = argv2[0] === "up" ? withProjectUpDefaults(argv2) : argv2;
3477
+ const bootsDaemon = isConsole || cmd === "dev";
3478
+ const env = bootsDaemon ? consoleEnv() : nativeEnv();
3479
+ const nativeArgv = cmd === "up" ? withProjectUpDefaults(argv2) : argv2;
3468
3480
  if (!fs2.existsSync(bin)) {
3469
- console.error(
3470
- `@nbt-dev/nbt: bundled binary not found at ${bin} (broken install?)`
3471
- );
3481
+ if (isConsole && IS_MAC) {
3482
+ console.error(
3483
+ `@nbt-dev/nbt: \`${cmd}\` runs the console daemon, which is Linux-only on macOS.
3484
+ Run it in Docker, then point the CLI at it with NBT_CONSOLE_URL.`
3485
+ );
3486
+ } else {
3487
+ console.error(
3488
+ `@nbt-dev/nbt: bundled binary not found at ${bin} (broken install?)`
3489
+ );
3490
+ }
3472
3491
  process.exit(1);
3473
3492
  }
3474
3493
  try {
@@ -3485,417 +3504,167 @@ function forwardToNative(argv2) {
3485
3504
  }
3486
3505
  process.exit(r.status === null ? 1 : r.status);
3487
3506
  }
3488
- function packageVersion() {
3507
+ function resolveAgentEntry() {
3508
+ const rel = path2.join(
3509
+ "packages",
3510
+ "coding-agent",
3511
+ "dist",
3512
+ "cli.js"
3513
+ );
3514
+ const candidates = [
3515
+ path2.join(__dirname, "..", "agent", rel),
3516
+ // published: agent vendored under this package
3517
+ path2.join(__dirname, "..", "..", "agent", rel)
3518
+ // dev monorepo: npm/agent
3519
+ ];
3520
+ for (const c of candidates) if (fs2.existsSync(c)) return c;
3489
3521
  try {
3490
- const pkg = JSON.parse(
3491
- fs2.readFileSync(path3.join(__dirname, "..", "package.json"), "utf8")
3492
- );
3493
- return typeof pkg?.version === "string" ? pkg.version : "unknown";
3522
+ return require.resolve("@nbt-dev/agent/dist/cli.js");
3494
3523
  } catch {
3495
- return "unknown";
3496
- }
3497
- }
3498
- var program2 = new Command();
3499
- program2.name("nbt").description("nbt.dev CLI").version(packageVersion(), "-v, --version", "Print the @nbt-dev/nbt version");
3500
- var NBT = [
3501
- ["install", "Install a cartridge on a live instance"],
3502
- ["generate", "Generate a typed client from contracts"],
3503
- ["migrate", "Create migrations and deploy local cartridges"],
3504
- ["lsp", "Run the NBT language server over stdio (editors)"],
3505
- ["editor", "Install the NBT VS Code extension (editor install)"]
3506
- ];
3507
- var CONSOLE = [
3508
- ["up", "Boot the console daemon"],
3509
- ["gen-cluster-key", "Generate a cluster signing key (copy to every node)"]
3510
- ];
3511
- var consoleCommands = new Set(CONSOLE.map(([n]) => n));
3512
- for (const [name, summary] of [...NBT, ...CONSOLE]) {
3513
- program2.command(name).description(`${summary} (native)`);
3514
- }
3515
- var customCommands = /* @__PURE__ */ new Set();
3516
- customCommands.add("init");
3517
- program2.command("init").description("Scaffold an nbt project (nbt.json, nbt/, generated/)").argument(
3518
- "[dir]",
3519
- "directory to create the project in (default: current directory)"
3520
- ).option("-y, --yes", "skip prompts; include the hello-world example").option("--no-example", "skip the hello-world example cartridge").action(
3521
- (dir, opts) => {
3522
- runInit(dir, opts).catch((e) => {
3523
- console.error(`nbt init: ${e?.message ?? e}`);
3524
- process.exit(1);
3525
- });
3526
- }
3527
- );
3528
- var NBT_JSON_TEMPLATE = `{
3529
- "carts": "nbt",
3530
- "generated": "generated",
3531
- "environments": {
3532
- "dev": { "host": "127.0.0.1", "port": 8080 }
3533
- }
3534
- }
3535
- `;
3536
- var HELLO_SCHEMA = `# A minimal entity. CRUD routes are generated automatically:
3537
- # GET /api/hello/note list
3538
- # POST /api/hello/note create
3539
- # GET /api/hello/note/:id get
3540
- # PUT /api/hello/note/:id update
3541
- # DELETE /api/hello/note/:id delete
3542
- entity Note {
3543
- id: ulid
3544
- createdAt: DateTime @default(now())
3545
- updatedAt: DateTime @updatedAt
3546
- title: string
3547
- body: string
3548
-
3549
- @@index([title])
3550
- }
3551
- `;
3552
- async function runInit(dir, opts) {
3553
- const root = dir ? path3.resolve(process.cwd(), dir) : process.cwd();
3554
- if (dir) fs2.mkdirSync(root, { recursive: true });
3555
- if (fs2.existsSync(path3.join(root, "nbt.json"))) {
3556
- throw new Error(
3557
- `nbt.json already exists ${dir ? `in ${dir}` : "here"} \u2014 this is already an nbt project`
3558
- );
3559
- }
3560
- if (!fs2.existsSync(path3.join(STDLIB, "auth", "schema.nbt"))) {
3561
- throw new Error("bundled auth cartridge is missing (broken install)");
3562
- }
3563
- if (fs2.existsSync(path3.join(root, "nbt", "auth"))) {
3564
- throw new Error(
3565
- "nbt/auth already exists; refusing to overwrite it during init"
3566
- );
3567
- }
3568
- fs2.writeFileSync(path3.join(root, "nbt.json"), NBT_JSON_TEMPLATE);
3569
- fs2.mkdirSync(path3.join(root, "nbt"), { recursive: true });
3570
- fs2.mkdirSync(path3.join(root, "generated"), { recursive: true });
3571
- addStandardCartridge("auth", path3.join(root, "nbt"), false);
3572
- console.log("Created project:");
3573
- console.log(" nbt.json (dev environment configured)");
3574
- console.log(" nbt/auth/ (local core auth cartridge)");
3575
- console.log(" generated/ (typed client output \u2014 nbt generate)");
3576
- const example = opts.example !== false && (opts.yes === true || await wantExample());
3577
- if (example) {
3578
- scaffoldHelloCart(path3.join(root, "nbt", "hello"));
3579
- console.log("\nCreated example cartridge nbt/hello/ (entity Note).");
3580
- console.log("\nNext:");
3581
- if (dir) console.log(` cd ${dir}`);
3582
- console.log(
3583
- " nbt dev # boot the console + live-reload on save"
3584
- );
3585
- } else {
3586
- if (dir)
3587
- console.log(
3588
- `
3589
- Next: cd ${dir}, then run \`nbt dev\` or add a standard cartridge with \`nbt add <name>\`.`
3590
- );
3591
- else
3592
- console.log(
3593
- "\nNext: run `nbt dev`, or add another standard cartridge with `nbt add <name>`."
3594
- );
3595
- }
3596
- }
3597
- async function wantExample() {
3598
- if (!process.stdin.isTTY) return true;
3599
- const rl = readline.createInterface({
3600
- input: process.stdin,
3601
- output: process.stdout
3602
- });
3603
- try {
3604
- const ans = (await rl.question("Add a hello-world example cartridge? [Y/n] ")).trim().toLowerCase();
3605
- return ans === "" || ans === "y" || ans === "yes";
3606
- } finally {
3607
- rl.close();
3608
- }
3609
- }
3610
- function scaffoldHelloCart(dir) {
3611
- fs2.mkdirSync(path3.join(dir, "migrations"), { recursive: true });
3612
- fs2.writeFileSync(path3.join(dir, "schema.nbt"), HELLO_SCHEMA);
3613
- }
3614
- customCommands.add("add");
3615
- program2.command("add").description("Vendor standard-library cartridges into this project").argument("<cartridge...>", "standard cartridge name(s)").option("--overwrite", "replace an existing local cartridge").action((cartridges, opts) => {
3616
- try {
3617
- const proj = loadNbtProject();
3618
- for (const name of cartridges) {
3619
- const dest = addStandardCartridge(
3620
- name,
3621
- proj.cartsDir,
3622
- opts.overwrite === true
3623
- );
3624
- console.log(`Added ${name} -> ${path3.relative(proj.rootDir, dest)}`);
3625
- }
3626
- console.log(
3627
- "Run `nbt migrate deploy` to deploy the local cartridge set."
3628
- );
3629
- } catch (e) {
3630
- console.error(`nbt add: ${e?.message ?? e}`);
3631
- process.exit(1);
3632
- }
3633
- });
3634
- function addStandardCartridge(name, cartsDir, overwrite) {
3635
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) {
3636
- throw new Error(`invalid cartridge name '${name}'`);
3637
- }
3638
- const src = path3.join(STDLIB, name);
3639
- if (!fs2.existsSync(path3.join(src, "schema.nbt"))) {
3640
- const available = fs2.existsSync(STDLIB) ? fs2.readdirSync(STDLIB, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().join(", ") : "";
3641
- throw new Error(
3642
- `unknown standard cartridge '${name}'${available ? ` (available: ${available})` : " (broken install: stdlib missing)"}`
3643
- );
3644
- }
3645
- const dest = path3.join(cartsDir, name);
3646
- if (fs2.existsSync(dest)) {
3647
- if (!overwrite)
3648
- throw new Error(`${dest} already exists; use --overwrite to replace it`);
3649
- fs2.rmSync(dest, { recursive: true, force: true });
3524
+ return null;
3650
3525
  }
3651
- fs2.mkdirSync(cartsDir, { recursive: true });
3652
- fs2.cpSync(src, dest, { recursive: true });
3653
- return dest;
3654
3526
  }
3655
- customCommands.add("list");
3656
- program2.command("list").description("List available standard-library modules").action(() => {
3657
- if (!fs2.existsSync(STDLIB)) {
3658
- console.error("nbt list: bundled stdlib missing (broken install)");
3659
- process.exit(1);
3660
- }
3661
- const proj = loadNbtProject();
3662
- const names = fs2.readdirSync(STDLIB, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((n) => fs2.existsSync(path3.join(STDLIB, n, "schema.nbt"))).sort();
3663
- for (const n of names) {
3664
- const installed = fs2.existsSync(
3665
- path3.join(proj.cartsDir, n, "schema.nbt")
3666
- );
3667
- console.log(installed ? `${n} (installed)` : n);
3668
- }
3669
- });
3670
- customCommands.add("update");
3671
- program2.command("update").description(
3672
- "Update vendored standard-library modules from the bundled stdlib"
3673
- ).argument("<module...>", "standard module name(s)").action((modules) => {
3674
- try {
3675
- const proj = loadNbtProject();
3676
- for (const name of modules) {
3677
- const src = path3.join(STDLIB, name);
3678
- if (!fs2.existsSync(path3.join(src, "schema.nbt"))) {
3679
- throw new Error(`unknown standard module '${name}' (see: nbt list)`);
3680
- }
3681
- const dest = path3.join(proj.cartsDir, name);
3682
- if (!fs2.existsSync(path3.join(dest, "schema.nbt"))) {
3683
- throw new Error(
3684
- `'${name}' is not installed in this project \u2014 run: nbt add ${name}`
3685
- );
3527
+ function projectConsoleUrl() {
3528
+ if (process.env.NBT_CONSOLE_URL) return process.env.NBT_CONSOLE_URL;
3529
+ let dir = process.cwd();
3530
+ for (let i = 0; i < 32; i++) {
3531
+ const candidate = path2.join(dir, "nbt.json");
3532
+ if (fs2.existsSync(candidate)) {
3533
+ try {
3534
+ const cfg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
3535
+ const dev = cfg?.environments?.dev ?? {};
3536
+ const explicit = cfg?.agent?.consoleUrl ?? dev?.consoleUrl;
3537
+ if (typeof explicit === "string" && explicit) return explicit;
3538
+ const host = typeof dev?.host === "string" && dev.host ? dev.host : "127.0.0.1";
3539
+ const port = Number(dev?.port) || 8080;
3540
+ return `http://${host}:${port}`;
3541
+ } catch {
3542
+ break;
3686
3543
  }
3687
- const written = copyStdlibOver(src, dest);
3688
- for (const f of written)
3689
- console.log(` wrote ${path3.relative(proj.rootDir, f)}`);
3690
- console.log(`Updated ${name}.`);
3691
3544
  }
3692
- console.log(
3693
- "Run `nbt migrate create <module>` to capture any schema changes, then `nbt migrate deploy`."
3694
- );
3695
- } catch (e) {
3696
- console.error(`nbt update: ${e?.message ?? e}`);
3697
- process.exit(1);
3545
+ const parent = path2.dirname(dir);
3546
+ if (parent === dir) break;
3547
+ dir = parent;
3698
3548
  }
3699
- });
3700
- function copyStdlibOver(src, dest) {
3701
- const written = [];
3702
- const walk = (s, d) => {
3703
- fs2.mkdirSync(d, { recursive: true });
3704
- for (const e of fs2.readdirSync(s, { withFileTypes: true })) {
3705
- const sp = path3.join(s, e.name);
3706
- const dp = path3.join(d, e.name);
3707
- if (e.isDirectory()) walk(sp, dp);
3708
- else {
3709
- fs2.copyFileSync(sp, dp);
3710
- written.push(dp);
3711
- }
3712
- }
3713
- };
3714
- walk(src, dest);
3715
- return written;
3549
+ return "http://127.0.0.1:8080";
3716
3550
  }
3717
- customCommands.add("dev");
3718
- program2.command("dev").description("Boot the console and live-reload local carts on .nbt change").option(
3719
- "-p, --port <port>",
3720
- "console HTTP port (default: nbt.json dev env, else 8080)"
3721
- ).option("-d, --data-dir <dir>", "console data dir (default: ./data)").action((opts) => {
3722
- runDev(opts).catch((e) => {
3723
- console.error(`nbt dev: ${e?.message ?? e}`);
3724
- process.exit(1);
3725
- });
3726
- });
3727
- function loadNbtProject() {
3551
+ function projectLlmUrl() {
3552
+ if (process.env.NBT_LLM_URL) return process.env.NBT_LLM_URL;
3728
3553
  let dir = process.cwd();
3729
3554
  for (let i = 0; i < 32; i++) {
3730
- const candidate = path3.join(dir, "nbt.json");
3555
+ const candidate = path2.join(dir, "nbt.json");
3731
3556
  if (fs2.existsSync(candidate)) {
3732
3557
  try {
3733
3558
  const cfg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
3734
- const carts = typeof cfg?.carts === "string" ? cfg.carts : "nbt";
3735
- const port = Number(cfg?.environments?.dev?.port) || 8080;
3736
- return { rootDir: dir, cartsDir: path3.resolve(dir, carts), port };
3559
+ const dev = cfg?.environments?.dev ?? {};
3560
+ const explicit = cfg?.agent?.llmUrl ?? dev?.llmUrl;
3561
+ if (typeof explicit === "string" && explicit) return explicit;
3737
3562
  } catch {
3738
3563
  break;
3739
3564
  }
3565
+ return void 0;
3740
3566
  }
3741
- const parent = path3.dirname(dir);
3567
+ const parent = path2.dirname(dir);
3742
3568
  if (parent === dir) break;
3743
3569
  dir = parent;
3744
3570
  }
3745
- return {
3746
- rootDir: process.cwd(),
3747
- cartsDir: path3.resolve(process.cwd(), "nbt"),
3748
- port: 8080
3749
- };
3750
- }
3751
- function withProjectUpDefaults(argv2) {
3752
- const hasPort = argv2.some(
3753
- (arg) => arg === "--port" || arg === "-p" || arg.startsWith("--port=")
3754
- );
3755
- if (hasPort) return argv2;
3756
- return [...argv2, "--port", String(loadNbtProject().port)];
3757
- }
3758
- function discoverCarts(cartsDir) {
3759
- if (!fs2.existsSync(cartsDir)) return [];
3760
- return fs2.readdirSync(cartsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path3.join(cartsDir, e.name)).filter((d) => fs2.existsSync(path3.join(d, "schema.nbt")));
3571
+ return void 0;
3761
3572
  }
3762
- function newestNbtMtime(dir) {
3763
- let newest = 0;
3764
- const walk = (d) => {
3765
- let entries;
3766
- try {
3767
- entries = fs2.readdirSync(d, { withFileTypes: true });
3768
- } catch {
3769
- return;
3770
- }
3771
- for (const e of entries) {
3772
- const p = path3.join(d, e.name);
3773
- if (e.isDirectory()) {
3774
- if (e.name !== ".dist" && e.name !== "node_modules") walk(p);
3775
- } else if (e.name.endsWith(".nbt")) {
3776
- try {
3777
- const m = fs2.statSync(p).mtimeMs;
3778
- if (m > newest) newest = m;
3779
- } catch {
3780
- }
3573
+ function projectBranchId() {
3574
+ if (process.env.NBT_BRANCH_ID) return process.env.NBT_BRANCH_ID;
3575
+ let dir = process.cwd();
3576
+ for (let i = 0; i < 32; i++) {
3577
+ const candidate = path2.join(dir, "nbt.json");
3578
+ if (fs2.existsSync(candidate)) {
3579
+ try {
3580
+ const cfg = JSON.parse(fs2.readFileSync(candidate, "utf8"));
3581
+ const b = cfg?.branch;
3582
+ if (typeof b === "string" && b) return b;
3583
+ } catch {
3584
+ break;
3781
3585
  }
3586
+ return void 0;
3782
3587
  }
3783
- };
3784
- walk(dir);
3785
- return newest;
3786
- }
3787
- function tcpUp(port, host = "127.0.0.1") {
3788
- return new Promise((resolve3) => {
3789
- const sock = net.connect({ port, host });
3790
- sock.once("connect", () => {
3791
- sock.destroy();
3792
- resolve3(true);
3793
- });
3794
- sock.once("error", () => resolve3(false));
3795
- sock.setTimeout(500, () => {
3796
- sock.destroy();
3797
- resolve3(false);
3798
- });
3799
- });
3588
+ const parent = path2.dirname(dir);
3589
+ if (parent === dir) break;
3590
+ dir = parent;
3591
+ }
3592
+ return void 0;
3800
3593
  }
3801
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3802
- function installCart(cartDir, port) {
3803
- const r = (0, import_node_child_process2.spawnSync)(
3804
- NBT_BIN,
3805
- ["install", "--path", cartDir, "--port", String(port)],
3806
- {
3807
- stdio: "inherit",
3808
- env: nativeEnv()
3809
- }
3810
- );
3811
- return r.status === 0;
3594
+ function storedApiKey() {
3595
+ try {
3596
+ const p = path2.join(os.homedir(), ".nbt", "credentials.json");
3597
+ const creds = JSON.parse(fs2.readFileSync(p, "utf-8"));
3598
+ const key = creds?.apiKey;
3599
+ return typeof key === "string" && key.length > 0 ? key : void 0;
3600
+ } catch {
3601
+ return void 0;
3602
+ }
3812
3603
  }
3813
- async function runDev(opts) {
3814
- const proj = loadNbtProject();
3815
- const port = opts.port ? Number(opts.port) : proj.port;
3816
- if (!Number.isFinite(port) || port <= 0)
3817
- throw new Error(`invalid port '${opts.port}'`);
3818
- const upArgs = ["up", "--port", String(port)];
3819
- if (opts.dataDir) upArgs.push("--data-dir", opts.dataDir);
3820
- console.log(`nbt dev: starting console on :${port}`);
3821
- const daemon = (0, import_node_child_process2.spawn)(CONSOLE_BIN, upArgs, {
3822
- stdio: "inherit",
3823
- env: consoleEnv()
3824
- });
3825
- let shuttingDown = false;
3826
- const shutdown = (code = 0) => {
3827
- if (shuttingDown) return;
3828
- shuttingDown = true;
3829
- try {
3830
- daemon.kill("SIGTERM");
3831
- } catch {
3832
- }
3833
- process.exit(code);
3834
- };
3835
- process.on("SIGINT", () => shutdown(0));
3836
- process.on("SIGTERM", () => shutdown(0));
3837
- daemon.on("exit", (code) => {
3838
- if (!shuttingDown) {
3839
- console.error(`nbt dev: console exited (${code ?? "signal"})`);
3840
- process.exit(code ?? 1);
3841
- }
3842
- });
3843
- let ready = false;
3844
- for (let i = 0; i < 60; i++) {
3845
- if (await tcpUp(port)) {
3846
- ready = true;
3847
- break;
3848
- }
3849
- await sleep(250);
3850
- }
3851
- if (!ready) {
3852
- console.error("nbt dev: console did not come up in time");
3853
- return shutdown(1);
3854
- }
3855
- const carts = discoverCarts(proj.cartsDir);
3856
- if (carts.length === 0) {
3857
- console.warn(
3858
- `nbt dev: no carts found under ${proj.cartsDir} (nothing to watch)`
3604
+ function forwardToAgent(args) {
3605
+ const entry = resolveAgentEntry();
3606
+ if (!entry) {
3607
+ console.error(
3608
+ "@nbt-dev/nbt: coding agent not found (expected agent/packages/coding-agent/dist/cli.js). Build it with `npm run build` in npm/agent."
3859
3609
  );
3610
+ process.exit(1);
3860
3611
  }
3861
- const applied = /* @__PURE__ */ new Map();
3862
- for (const c of carts) {
3863
- console.log(`nbt dev: install ${path3.basename(c)}`);
3864
- installCart(c, port);
3865
- applied.set(c, newestNbtMtime(c));
3612
+ const env = nativeEnv();
3613
+ env.NBT_CONSOLE_URL = projectConsoleUrl();
3614
+ const llmUrl = projectLlmUrl();
3615
+ if (llmUrl) env.NBT_LLM_URL = llmUrl;
3616
+ const branchId = projectBranchId();
3617
+ if (branchId) env.NBT_BRANCH_ID = branchId;
3618
+ if (!env.NBT_API_KEY) {
3619
+ const key = storedApiKey();
3620
+ if (key) env.NBT_API_KEY = key;
3621
+ }
3622
+ const r = (0, import_node_child_process2.spawnSync)(process.execPath, [entry, ...args], {
3623
+ stdio: "inherit",
3624
+ env
3625
+ });
3626
+ if (r.error) {
3627
+ console.error(`@nbt-dev/nbt: failed to launch agent: ${r.error.message}`);
3628
+ process.exit(1);
3866
3629
  }
3867
- console.log(
3868
- `nbt dev: watching ${proj.cartsDir} \u2014 edit .nbt to reload (Ctrl-C to stop)`
3869
- );
3870
- let busy = false;
3871
- setInterval(async () => {
3872
- if (busy || shuttingDown) return;
3873
- busy = true;
3874
- try {
3875
- for (const c of discoverCarts(proj.cartsDir)) {
3876
- const m = newestNbtMtime(c);
3877
- const prev = applied.get(c) ?? 0;
3878
- if (m > prev) {
3879
- await sleep(150);
3880
- const settled = newestNbtMtime(c);
3881
- console.log(`nbt dev: reload ${path3.basename(c)}`);
3882
- installCart(c, port);
3883
- applied.set(c, settled);
3884
- }
3885
- }
3886
- } finally {
3887
- busy = false;
3888
- }
3889
- }, 400);
3630
+ process.exit(r.status === null ? 1 : r.status);
3890
3631
  }
3891
- customCommands.add("bundle");
3892
- program2.command("bundle").description("Bundle TypeScript workflow code via esbuild").argument("<entry...>", "workflow entry point file(s)").option("-o, --outfile <dir>", "output file").action((entry, opts) => {
3893
- if (!opts.outfile) {
3894
- console.log("-o, --outfile missing.");
3895
- return;
3632
+ function packageVersion() {
3633
+ try {
3634
+ const pkg = JSON.parse(
3635
+ fs2.readFileSync(path2.join(__dirname, "..", "package.json"), "utf8")
3636
+ );
3637
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
3638
+ } catch {
3639
+ return "unknown";
3896
3640
  }
3897
- bundleWorkflows({ entryPoints: entry, outfile: opts.outfile });
3898
- });
3641
+ }
3642
+ var program2 = new Command();
3643
+ program2.name("nbt").description("nbt.dev CLI").version(packageVersion(), "-v, --version", "Print the @nbt-dev/nbt version");
3644
+ var NBT = [
3645
+ ["init", "Scaffold an nbt project"],
3646
+ ["add", "Vendor standard cartridges into this project"],
3647
+ ["list", "List available standard-library cartridges"],
3648
+ ["update", "Refresh vendored cartridges from the stdlib"],
3649
+ ["dev", "Boot the NBT runtime with live reload"],
3650
+ ["build", "Build the frontend (esbuild SPA + SSR)"],
3651
+ ["install", "Install a cartridge on a live instance"],
3652
+ ["generate", "Generate a typed client from contracts"],
3653
+ ["routes", "Generate file-based routes from app/"],
3654
+ ["migrate", "Create migrations and deploy local cartridges"],
3655
+ ["lsp", "Run the NBT language server over stdio (editors)"],
3656
+ ["editor", "Install the NBT VS Code extension (editor install)"]
3657
+ ];
3658
+ var CONSOLE = [
3659
+ ["up", "Boot the NBT runtime"],
3660
+ ["gen-cluster-key", "Generate a cluster signing key (copy to every node)"]
3661
+ ];
3662
+ var consoleCommands = new Set(CONSOLE.map(([n]) => n));
3663
+ for (const [name, summary] of [...NBT, ...CONSOLE]) {
3664
+ program2.command(name).description(`${summary} (native)`);
3665
+ }
3666
+ program2.command("agent").description("Launch the nbt coding agent (defaults to the nbt provider)");
3667
+ var customCommands = /* @__PURE__ */ new Set();
3899
3668
  customCommands.add("version");
3900
3669
  program2.command("version").description("Print the @nbt-dev/nbt version").action(() => {
3901
3670
  process.stdout.write(packageVersion() + "\n");
@@ -3942,6 +3711,7 @@ function runCompletion(words) {
3942
3711
  if (words.length <= 1) {
3943
3712
  const prefix = words[0] ?? "";
3944
3713
  const names = [
3714
+ "agent",
3945
3715
  ...NBT.map(([n]) => n),
3946
3716
  ...CONSOLE.map(([n]) => n),
3947
3717
  ...customCommands
@@ -3966,6 +3736,9 @@ var first = argv[0];
3966
3736
  if (first === "__complete__") {
3967
3737
  runCompletion(argv.slice(1));
3968
3738
  }
3739
+ if (first === "agent") {
3740
+ forwardToAgent(argv.slice(1));
3741
+ }
3969
3742
  if (!first || first === "-h" || first === "--help" || first === "help") {
3970
3743
  program2.outputHelp();
3971
3744
  process.exit(first ? 0 : 1);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@nbt-dev/nbt",
3
- "version": "0.1.1",
4
- "description": "The nbt CLI and console daemon for the nbt-dev console — a fixed-target OS for web development. Linux/x64 prebuilt binaries.",
3
+ "version": "0.1.2",
4
+ "description": "The nbt CLI and runtime for nbt-dev — a schema-first backend platform for web development. Linux/x64 + macOS prebuilt binaries.",
5
5
  "bin": {
6
6
  "nbt": "dist/nbt.js"
7
7
  },
8
8
  "files": [
9
9
  "dist/",
10
10
  "vendor/linux-x64/",
11
+ "vendor/darwin-arm64/",
12
+ "vendor/darwin-x64/",
11
13
  "stdlib/",
12
14
  "README.md",
13
15
  "LICENSE",
@@ -19,10 +21,12 @@
19
21
  "prepack": "node scripts/build.mjs"
20
22
  },
21
23
  "os": [
22
- "linux"
24
+ "linux",
25
+ "darwin"
23
26
  ],
24
27
  "cpu": [
25
- "x64"
28
+ "x64",
29
+ "arm64"
26
30
  ],
27
31
  "engines": {
28
32
  "node": ">=16"
@@ -67,9 +67,9 @@ entity Account {
67
67
  userId: string
68
68
  providerId: string
69
69
  password: string
70
- accessToken: string
71
- refreshToken: string
72
- idToken: string
70
+ accessToken: string @secret
71
+ refreshToken: string @secret
72
+ idToken: string @secret
73
73
  accessTokenExpiresAt: DateTime
74
74
  refreshTokenExpiresAt: DateTime
75
75
  scope: string
@@ -99,6 +99,9 @@ entity ApiKey {
99
99
  key: string
100
100
  permissions: string
101
101
  roles: string
102
+
103
+ @@index([key])
104
+ @@index([projectId])
102
105
  }
103
106
 
104
107
  # Console-operator SSH public keys. The console daemon authenticates SSH
@@ -21,7 +21,7 @@ entity DnsProvider {
21
21
  updatedAt: DateTime @updatedAt
22
22
  type: string # "cloudflare"
23
23
  label: string # user-facing name ("my CF account")
24
- apiToken: string # provider API token (stored as-is)
24
+ apiToken: string @secret # provider API token (sealed at rest)
25
25
  accountId: string # CF account_id, needed for add_zone + registrar calls (optional)
26
26
  status: string # "ACTIVE" | "INVALID" | "DISCONNECTED"
27
27
  lastCheckedAt: u64 # ms epoch — updated on connect + test
@@ -10,7 +10,7 @@ entity Endpoint {
10
10
  name: string
11
11
  slug: string
12
12
  status: string
13
- secret: string
13
+ secret: string @secret
14
14
  payloadCount: u64
15
15
  errorCount: u64
16
16
 
@@ -24,7 +24,7 @@ entity TwilioAccount {
24
24
  updatedAt: DateTime @updatedAt
25
25
  name: string # operator-facing label
26
26
  subAccountSid: string # AC... — populated by provision()
27
- authToken: string # subaccount auth token (stored as-is)
27
+ authToken: string @secret # subaccount auth token (sealed at rest)
28
28
  publicUrlBase: string # e.g. "https://acme.console.app"
29
29
  status: string # PENDING | ACTIVE | SUSPENDED | CLOSED
30
30
  retentionDays: s32 # 0 = keep message bodies forever; N = null body after N days
@@ -31,7 +31,7 @@ entity GsheetSync {
31
31
  createdAt: DateTime @default(now())
32
32
  updatedAt: DateTime @updatedAt
33
33
  spreadsheetId: string # parsed from the operator-provided sheet URL
34
- serviceAccountJson: string # SECRET — service-account key JSON
34
+ serviceAccountJson: string @secret # SECRET — service-account key JSON (sealed at rest)
35
35
  saEmail: string # service-account client_email (shown in devtools)
36
36
  intervalSeconds: u32 # poll cadence
37
37
  enabled: bool # sync on/off
@@ -0,0 +1,16 @@
1
+ # Secrets cart — storage substrate for named, per-environment config secrets
2
+ # (STRIPE_KEY, OPENAI_KEY, ...). The console daemon is the single writer
3
+ # (modules/api/secrets.jai); the generated CRUD is not the intended interface —
4
+ # operators set values via POST /_console/secrets (or `nbt secret set`).
5
+ #
6
+ # `value` is @secret: sealed (AEAD) at rest, replicated as ciphertext, and never
7
+ # returned by any read path. The row id is "<env>:<name>" so set/get are
8
+ # idempotent without a secondary index.
9
+ entity Secret {
10
+ id: string @id # "<env>:<name>"
11
+ createdAt: DateTime @default(now())
12
+ updatedAt: DateTime @updatedAt
13
+ name: string # slot name, e.g. STRIPE_KEY
14
+ env: string # "dev" | "staging" | "prod"
15
+ value: string @secret # sealed config value
16
+ }
@@ -22,14 +22,21 @@ entity WorkflowExecution {
22
22
  createdAt: DateTime @default(now())
23
23
  updatedAt: DateTime @updatedAt
24
24
  status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
25
+ workflowId?: string # OPTIONAL caller-chosen id (Phase 9b) for start dedup; the PK stays an auto-ULID (FK-safe). Unset on cron/event/most starts.
25
26
  workflowName: string
26
27
  args?: string # JSON arguments passed to runWorkflow
27
28
  result?: string # JSON return value (COMPLETED)
28
29
  error?: string # message + stack (FAILED)
29
30
  cursor: u32
31
+ wakeAt: u64 # durable timer (Phase 9a): ms-epoch when a parked sleep is due (0 = not sleeping). The leader reconcile resumes the instance once wall-clock passes it.
32
+ parentExec?: string # child workflow (Phase 9e): the parent execution id to resume with this child's result on completion
33
+ parentSeq?: u32 # the parent's host-call ordinal awaiting this child's result
30
34
  lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
31
35
  deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
32
36
  events: WorkflowExecutionEvent[]
37
+ signals: WorkflowSignal[] # buffered signals awaiting delivery (Phase 9d)
38
+ @@unique([workflowId]) # start dedup: a provided workflowId is unique (NULLs distinct, so unset starts never collide)
39
+ @@index([workflowId])
33
40
  }
34
41
 
35
42
  # A recurring trigger: fire `workflowName` whenever the cron expression matches.
@@ -62,6 +69,26 @@ entity WorkflowEvent {
62
69
  processed: bool # set true once the leader has fired this emission's triggers
63
70
  }
64
71
 
72
+ # A durable, buffered signal for an execution (Phase 9d). A signal that arrives
73
+ # before the workflow reaches the matching wait() — or for a name other than the
74
+ # one currently awaited — is stored here instead of being rejected; when the
75
+ # workflow parks on wait(name) it drains the oldest unconsumed matching signal and
76
+ # delivers it as the wait's result. id is a ULID so unconsumed signals drain in
77
+ # arrival order. A signal that arrives WHILE parked on the matching wait is
78
+ # delivered immediately (fast path) and never buffered.
79
+ entity WorkflowSignal {
80
+ id: ulid
81
+ createdAt: DateTime @default(now())
82
+ updatedAt: DateTime @updatedAt
83
+ execution: WorkflowExecution @relation(onDelete: Cascade) # the target instance
84
+ name: string # signal name, matched against wait(name)
85
+ payload?: string # JSON value delivered as the wait result
86
+ idempotencyKey?: string # optional caller dedup key; a re-POST with the same key is a no-op
87
+ consumed: bool # set true once delivered to a wait
88
+ @@index([execution])
89
+ @@unique([idempotencyKey]) # NULLs distinct (null-skip), so unkeyed signals never collide
90
+ }
91
+
65
92
  enum WorkflowExecutionEventKind {
66
93
  HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
67
94
  HOST_CALL_RESULT # the resolved value of a host command (replay source)
@@ -67,9 +67,9 @@ entity Account {
67
67
  userId: string
68
68
  providerId: string
69
69
  password: string
70
- accessToken: string
71
- refreshToken: string
72
- idToken: string
70
+ accessToken: string @secret
71
+ refreshToken: string @secret
72
+ idToken: string @secret
73
73
  accessTokenExpiresAt: DateTime
74
74
  refreshTokenExpiresAt: DateTime
75
75
  scope: string
@@ -99,6 +99,9 @@ entity ApiKey {
99
99
  key: string
100
100
  permissions: string
101
101
  roles: string
102
+
103
+ @@index([key])
104
+ @@index([projectId])
102
105
  }
103
106
 
104
107
  # Console-operator SSH public keys. The console daemon authenticates SSH
@@ -21,7 +21,7 @@ entity DnsProvider {
21
21
  updatedAt: DateTime @updatedAt
22
22
  type: string # "cloudflare"
23
23
  label: string # user-facing name ("my CF account")
24
- apiToken: string # provider API token (stored as-is)
24
+ apiToken: string @secret # provider API token (sealed at rest)
25
25
  accountId: string # CF account_id, needed for add_zone + registrar calls (optional)
26
26
  status: string # "ACTIVE" | "INVALID" | "DISCONNECTED"
27
27
  lastCheckedAt: u64 # ms epoch — updated on connect + test
@@ -10,7 +10,7 @@ entity Endpoint {
10
10
  name: string
11
11
  slug: string
12
12
  status: string
13
- secret: string
13
+ secret: string @secret
14
14
  payloadCount: u64
15
15
  errorCount: u64
16
16
 
@@ -24,7 +24,7 @@ entity TwilioAccount {
24
24
  updatedAt: DateTime @updatedAt
25
25
  name: string # operator-facing label
26
26
  subAccountSid: string # AC... — populated by provision()
27
- authToken: string # subaccount auth token (stored as-is)
27
+ authToken: string @secret # subaccount auth token (sealed at rest)
28
28
  publicUrlBase: string # e.g. "https://acme.console.app"
29
29
  status: string # PENDING | ACTIVE | SUSPENDED | CLOSED
30
30
  retentionDays: s32 # 0 = keep message bodies forever; N = null body after N days
@@ -31,7 +31,7 @@ entity GsheetSync {
31
31
  createdAt: DateTime @default(now())
32
32
  updatedAt: DateTime @updatedAt
33
33
  spreadsheetId: string # parsed from the operator-provided sheet URL
34
- serviceAccountJson: string # SECRET — service-account key JSON
34
+ serviceAccountJson: string @secret # SECRET — service-account key JSON (sealed at rest)
35
35
  saEmail: string # service-account client_email (shown in devtools)
36
36
  intervalSeconds: u32 # poll cadence
37
37
  enabled: bool # sync on/off
@@ -0,0 +1,16 @@
1
+ # Secrets cart — storage substrate for named, per-environment config secrets
2
+ # (STRIPE_KEY, OPENAI_KEY, ...). The console daemon is the single writer
3
+ # (modules/api/secrets.jai); the generated CRUD is not the intended interface —
4
+ # operators set values via POST /_console/secrets (or `nbt secret set`).
5
+ #
6
+ # `value` is @secret: sealed (AEAD) at rest, replicated as ciphertext, and never
7
+ # returned by any read path. The row id is "<env>:<name>" so set/get are
8
+ # idempotent without a secondary index.
9
+ entity Secret {
10
+ id: string @id # "<env>:<name>"
11
+ createdAt: DateTime @default(now())
12
+ updatedAt: DateTime @updatedAt
13
+ name: string # slot name, e.g. STRIPE_KEY
14
+ env: string # "dev" | "staging" | "prod"
15
+ value: string @secret # sealed config value
16
+ }
@@ -22,14 +22,21 @@ entity WorkflowExecution {
22
22
  createdAt: DateTime @default(now())
23
23
  updatedAt: DateTime @updatedAt
24
24
  status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
25
+ workflowId?: string # OPTIONAL caller-chosen id (Phase 9b) for start dedup; the PK stays an auto-ULID (FK-safe). Unset on cron/event/most starts.
25
26
  workflowName: string
26
27
  args?: string # JSON arguments passed to runWorkflow
27
28
  result?: string # JSON return value (COMPLETED)
28
29
  error?: string # message + stack (FAILED)
29
30
  cursor: u32
31
+ wakeAt: u64 # durable timer (Phase 9a): ms-epoch when a parked sleep is due (0 = not sleeping). The leader reconcile resumes the instance once wall-clock passes it.
32
+ parentExec?: string # child workflow (Phase 9e): the parent execution id to resume with this child's result on completion
33
+ parentSeq?: u32 # the parent's host-call ordinal awaiting this child's result
30
34
  lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
31
35
  deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
32
36
  events: WorkflowExecutionEvent[]
37
+ signals: WorkflowSignal[] # buffered signals awaiting delivery (Phase 9d)
38
+ @@unique([workflowId]) # start dedup: a provided workflowId is unique (NULLs distinct, so unset starts never collide)
39
+ @@index([workflowId])
33
40
  }
34
41
 
35
42
  # A recurring trigger: fire `workflowName` whenever the cron expression matches.
@@ -62,6 +69,26 @@ entity WorkflowEvent {
62
69
  processed: bool # set true once the leader has fired this emission's triggers
63
70
  }
64
71
 
72
+ # A durable, buffered signal for an execution (Phase 9d). A signal that arrives
73
+ # before the workflow reaches the matching wait() — or for a name other than the
74
+ # one currently awaited — is stored here instead of being rejected; when the
75
+ # workflow parks on wait(name) it drains the oldest unconsumed matching signal and
76
+ # delivers it as the wait's result. id is a ULID so unconsumed signals drain in
77
+ # arrival order. A signal that arrives WHILE parked on the matching wait is
78
+ # delivered immediately (fast path) and never buffered.
79
+ entity WorkflowSignal {
80
+ id: ulid
81
+ createdAt: DateTime @default(now())
82
+ updatedAt: DateTime @updatedAt
83
+ execution: WorkflowExecution @relation(onDelete: Cascade) # the target instance
84
+ name: string # signal name, matched against wait(name)
85
+ payload?: string # JSON value delivered as the wait result
86
+ idempotencyKey?: string # optional caller dedup key; a re-POST with the same key is a no-op
87
+ consumed: bool # set true once delivered to a wait
88
+ @@index([execution])
89
+ @@unique([idempotencyKey]) # NULLs distinct (null-skip), so unkeyed signals never collide
90
+ }
91
+
65
92
  enum WorkflowExecutionEventKind {
66
93
  HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
67
94
  HOST_CALL_RESULT # the resolved value of a host command (replay source)
Binary file
Binary file
Binary file