@moku-labs/worker 0.7.0 → 0.7.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.
@@ -761,7 +761,8 @@ const createQueuesApi = (ctx) => {
761
761
  deployManifest: () => Object.values(ctx.config).map((instance) => ({
762
762
  kind: "queue",
763
763
  name: instance.name,
764
- binding: instance.binding
764
+ binding: instance.binding,
765
+ ...instance.onMessage ? { consumer: true } : {}
765
766
  }))
766
767
  };
767
768
  };
@@ -1983,8 +1984,9 @@ const runDev = async (ctx, opts, deps) => {
1983
1984
  //#region src/plugins/deploy/infra/plan.ts
1984
1985
  /**
1985
1986
  * Decide whether a single declared resource already exists in the account, recovering its id
1986
- * (kv/d1) when it does. Durable Objects are config-only (they ship with the script), so they are
1987
- * always treated as "missing" provisioning them is a no-op that just records the binding.
1987
+ * (kv/d1) when it does. Durable Objects are config-only they ship with the Worker (`wrangler
1988
+ * deploy` + the auto-derived DO migration create the namespace), never provisioned via the API — so
1989
+ * they are treated as already EXISTING, and the plan never re-offers to "create" them each deploy.
1988
1990
  *
1989
1991
  * @param resource - The declared resource descriptor.
1990
1992
  * @param existing - The indexed set of resources already in the account.
@@ -2012,7 +2014,7 @@ const checkExisting = (resource, existing) => {
2012
2014
  }
2013
2015
  case "r2": return { exists: existing.r2.has(resource.name) };
2014
2016
  case "queue": return { exists: existing.queue.has(resource.name) };
2015
- case "do": return { exists: false };
2017
+ case "do": return { exists: true };
2016
2018
  }
2017
2019
  };
2018
2020
  /**
@@ -2527,16 +2529,20 @@ const parseJsonc = (source) => {
2527
2529
  *
2528
2530
  * @param resources - All resource descriptors from the manifest.
2529
2531
  * @param ids - Captured Cloudflare ids keyed by binding; the entry's `id` is filled from here.
2530
- * @returns One wrangler KV namespace entry per kv resource (real `id` when known, else "").
2532
+ * @returns One wrangler KV namespace entry per kv resource real `id` when known, omitted otherwise
2533
+ * (wrangler rejects an empty `id`, but a local-dev / freshly-generated config validates without one).
2531
2534
  * @example
2532
2535
  * ```ts
2533
2536
  * const kv = buildKvNamespaces([{ kind: "kv", binding: "CACHE" }], { CACHE: "ns123" });
2534
2537
  * ```
2535
2538
  */
2536
- const buildKvNamespaces = (resources, ids) => resources.filter((resource) => resource.kind === "kv").map((resource) => ({
2537
- binding: resource.binding,
2538
- id: ids[resource.binding] ?? ""
2539
- }));
2539
+ const buildKvNamespaces = (resources, ids) => resources.filter((resource) => resource.kind === "kv").map((resource) => {
2540
+ const id = ids[resource.binding];
2541
+ return id ? {
2542
+ binding: resource.binding,
2543
+ id
2544
+ } : { binding: resource.binding };
2545
+ });
2540
2546
  /**
2541
2547
  * Build the wrangler `r2_buckets` array from the manifest's r2 resources.
2542
2548
  *
@@ -2563,31 +2569,41 @@ const buildR2Buckets = (resources) => resources.filter((resource) => resource.ki
2563
2569
  * ```
2564
2570
  */
2565
2571
  const buildD1Databases = (resources, ids) => resources.filter((resource) => resource.kind === "d1").map((resource) => {
2572
+ const databaseId = ids[resource.binding];
2566
2573
  const entry = {
2567
2574
  binding: resource.binding,
2568
- database_name: resource.name,
2569
- database_id: ids[resource.binding] ?? ""
2575
+ database_name: resource.name
2570
2576
  };
2577
+ if (databaseId) entry.database_id = databaseId;
2571
2578
  if (resource.migrations) entry.migrations_dir = resource.migrations;
2572
2579
  return entry;
2573
2580
  });
2574
2581
  /**
2575
- * Build the wrangler `queues` producers section from the manifest's queue resources.
2582
+ * Build the wrangler `queues` section (producers + consumers) from the manifest's queue resources.
2583
+ * Every queue is a `producer`; a queue flagged `consumer: true` (it declares an `onMessage` handler)
2584
+ * is ALSO registered as a `consumer` so wrangler delivers its messages to this Worker's queue()
2585
+ * handler — both locally under `wrangler dev` and in production. Without the consumer entry the
2586
+ * handler never runs (the bug that silently drops a queue-driven activity feed).
2576
2587
  *
2577
2588
  * @param resources - All resource descriptors from the manifest.
2578
- * @returns The queues section, or undefined when there are no queue resources.
2589
+ * @returns The queues section (producers, plus consumers when any), or undefined when there are none.
2579
2590
  * @example
2580
2591
  * ```ts
2581
- * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY" }]);
2592
+ * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true }]);
2582
2593
  * ```
2583
2594
  */
2584
2595
  const buildQueues = (resources) => {
2585
2596
  const queueResources = resources.filter((resource) => resource.kind === "queue");
2586
2597
  if (queueResources.length === 0) return void 0;
2587
- return { producers: queueResources.map((resource) => ({
2598
+ const producers = queueResources.map((resource) => ({
2588
2599
  queue: resource.name,
2589
2600
  binding: resource.binding
2590
- })) };
2601
+ }));
2602
+ const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => ({ queue: resource.name }));
2603
+ return consumers.length > 0 ? {
2604
+ producers,
2605
+ consumers
2606
+ } : { producers };
2591
2607
  };
2592
2608
  /**
2593
2609
  * Build the wrangler `durable_objects` bindings section from the manifest's do resources.
@@ -2695,7 +2711,8 @@ const wranglerExtra = (config) => {
2695
2711
  * @param configFile - Path to the wrangler config file.
2696
2712
  * @param manifest - The assembled deploy manifest.
2697
2713
  * @param ids - Captured Cloudflare ids keyed by binding (kv namespace id, d1 database id). Defaults
2698
- * to an empty map, in which case `id`/`database_id` are written as "" (e.g. the universal path).
2714
+ * to an empty map, in which case `id`/`database_id` are OMITTED (not "") so the generated config
2715
+ * still validates for local `dev` (wrangler rejects an empty id); a deploy fills the real ids.
2699
2716
  * @param extra - Extra top-level wrangler keys to merge in (the app's `deploy.wrangler` config).
2700
2717
  * @returns Resolves once the file is written.
2701
2718
  * @example
@@ -3070,6 +3087,32 @@ const guidedUpload = async (ctx, manifest, deps) => {
3070
3087
  return true;
3071
3088
  };
3072
3089
  /**
3090
+ * The final deploy step: confirm the target (guided only), run `wrangler deploy` with interactive
3091
+ * retry, then emit deploy:complete. Resolves false when the target gate or a deploy retry is declined.
3092
+ *
3093
+ * @param ctx - The deploy plugin context.
3094
+ * @param manifest - The assembled (or caller-supplied) deploy manifest.
3095
+ * @param stage - The resolved deploy stage (for the confirm prompt).
3096
+ * @param deps - Interactivity + the confirm prompt.
3097
+ * @returns True once deployed; false when the user declined the gate or a retry (abort).
3098
+ * @example
3099
+ * ```ts
3100
+ * if (!(await guidedDeployStep(ctx, manifest, stage, deps))) return emitAborted(ctx);
3101
+ * ```
3102
+ */
3103
+ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3104
+ if (!await deps.confirm(`Deploy "${manifest.name}" to ${stage}?`)) return false;
3105
+ ctx.emit("deploy:phase", { phase: "deploy" });
3106
+ const url = await guidedStep(() => runWrangler([
3107
+ "deploy",
3108
+ "--config",
3109
+ ctx.config.configFile
3110
+ ]), HINTS.deploy, deps);
3111
+ if (url === ABORTED) return false;
3112
+ ctx.emit("deploy:complete", { url });
3113
+ return true;
3114
+ };
3115
+ /**
3073
3116
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3074
3117
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3075
3118
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3111,10 +3154,9 @@ const createDeployApi = (ctx) => ({
3111
3154
  const ci = opts?.ci ?? ctx.config.ci;
3112
3155
  const stage = opts?.stage ?? ctx.global.stage;
3113
3156
  const interactive = !ci && stdoutIsTty();
3114
- const confirm = interactive ? (0, _moku_labs_common_cli.createBrandPrompts)().confirm : async (_question) => true;
3115
3157
  const deps = {
3116
3158
  interactive,
3117
- confirm
3159
+ confirm: interactive ? (0, _moku_labs_common_cli.createBrandPrompts)().confirm : async (_question) => true
3118
3160
  };
3119
3161
  ctx.emit("deploy:phase", { phase: "auth" });
3120
3162
  if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
@@ -3127,15 +3169,7 @@ const createDeployApi = (ctx) => ({
3127
3169
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3128
3170
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3129
3171
  if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3130
- if (!await confirm(`Deploy "${manifest.name}" to ${stage}?`)) return emitAborted(ctx);
3131
- ctx.emit("deploy:phase", { phase: "deploy" });
3132
- const url = await guidedStep(() => runWrangler([
3133
- "deploy",
3134
- "--config",
3135
- ctx.config.configFile
3136
- ]), HINTS.deploy, deps);
3137
- if (url === ABORTED) return emitAborted(ctx);
3138
- ctx.emit("deploy:complete", { url });
3172
+ if (!await guidedDeployStep(ctx, manifest, stage, deps)) return emitAborted(ctx);
3139
3173
  },
3140
3174
  /**
3141
3175
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3158,6 +3192,50 @@ const createDeployApi = (ctx) => ({
3158
3192
  await runDev(ctx, opts, realDevDeps());
3159
3193
  },
3160
3194
  /**
3195
+ * Execute a SQL file against a configured D1 database via `wrangler d1 execute` — for seeding dev
3196
+ * data. Local by default (applies that database's migrations first so the file's tables exist);
3197
+ * `opts.remote` seeds Cloudflare (schema is applied by `deploy`). Generates the wrangler config up
3198
+ * front so the binding resolves even on a first run. Streams wrangler's output.
3199
+ *
3200
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
3201
+ * @param opts - Optional options.
3202
+ * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3203
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
3204
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
3205
+ * @returns Resolves once wrangler finishes executing the file.
3206
+ * @example
3207
+ * ```ts
3208
+ * await api.seed("db/seed.sql"); // local default d1 (migrate, then execute)
3209
+ * await api.seed("db/seed.sql", { remote: true }); // remote d1
3210
+ * ```
3211
+ */
3212
+ async seed(sqlFile, opts) {
3213
+ if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
3214
+ const stage = opts?.stage ?? ctx.global.stage;
3215
+ await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
3216
+ const databases = ctx.require(d1Plugin).deployManifest();
3217
+ const wanted = opts?.binding;
3218
+ const matched = wanted === void 0 ? databases : databases.filter((database) => database.binding === wanted);
3219
+ const target = matched.length === 1 ? matched[0] : void 0;
3220
+ if (target === void 0) throw new Error(wanted === void 0 ? `[moku-worker] seed: ${String(databases.length)} d1 databases configured — pass { binding } to choose one.` : `[moku-worker] seed: no d1 database is bound to "${wanted}".`);
3221
+ const scope = opts?.remote === true ? "--remote" : "--local";
3222
+ if (scope === "--local" && target.migrations !== void 0) await runWranglerInherit([
3223
+ "d1",
3224
+ "migrations",
3225
+ "apply",
3226
+ target.binding,
3227
+ "--local"
3228
+ ]);
3229
+ await runWranglerInherit([
3230
+ "d1",
3231
+ "execute",
3232
+ target.binding,
3233
+ scope,
3234
+ "--file",
3235
+ sqlFile
3236
+ ]);
3237
+ },
3238
+ /**
3161
3239
  * Scaffold a starting wrangler config (and CI files when ci is set).
3162
3240
  * Idempotent: an existing config file is left untouched.
3163
3241
  *
@@ -3284,54 +3362,10 @@ const deployPlugin = createPlugin("deploy", {
3284
3362
  /**
3285
3363
  * @file cli plugin — argv parsing helpers (isolated so they unit-test without a real process).
3286
3364
  *
3287
- * `dev` resolves its port from the command line (`bun scripts/dev.ts --port 3000`) so a consumer
3288
- * never hardcodes it in the app. Pure: takes an argv array, reads no globals. Node-only tooling.
3365
+ * `deploy`/`dev` resolve the target stage from the command line (`--stage dev`) so a consumer never
3366
+ * hardcodes it. The dev PORT is not parsed here it comes only from the `dev()` argument (no hidden
3367
+ * argv/config resolution). Pure: takes an argv array, reads no globals. Node-only tooling.
3289
3368
  */
3290
- /** The valid TCP port range a `--port` value must fall within to be accepted. */
3291
- const MAX_PORT = 65535;
3292
- /**
3293
- * Extract a `--port`/`-p` value from a single token (and the token after it, for the spaced form).
3294
- *
3295
- * @param token - The current argv token.
3296
- * @param next - The following argv token (the value, for the `--port 3000` spaced form).
3297
- * @returns The raw string value when this token is a port flag, else undefined.
3298
- * @example
3299
- * ```ts
3300
- * portValueFrom("--port=3000", undefined); // "3000"
3301
- * portValueFrom("--port", "3000"); // "3000"
3302
- * portValueFrom("--config", "x"); // undefined
3303
- * ```
3304
- */
3305
- const portValueFrom = (token, next) => {
3306
- const inline = /^(?:--port|-p)=(.+)$/u.exec(token);
3307
- if (inline) return inline[1];
3308
- if (token === "--port" || token === "-p") return next;
3309
- };
3310
- /**
3311
- * Parse a `--port <n>` / `--port=<n>` / `-p <n>` flag out of an argv array.
3312
- *
3313
- * Returns the first valid port (a positive integer ≤ 65535) found, or undefined when the flag is
3314
- * absent or its value is not a usable port — letting the caller fall back to a default.
3315
- *
3316
- * @param argv - The argv array to scan (the caller passes the process argv).
3317
- * @returns The parsed port number, or undefined when no valid `--port`/`-p` flag is present.
3318
- * @example
3319
- * ```ts
3320
- * parsePortArg(["bun", "scripts/dev.ts", "--port", "3000"]); // 3000
3321
- * parsePortArg(["bun", "scripts/dev.ts", "--port=3000"]); // 3000
3322
- * parsePortArg(["bun", "scripts/dev.ts"]); // undefined
3323
- * ```
3324
- */
3325
- const parsePortArg = (argv) => {
3326
- for (let index = 0; index < argv.length; index++) {
3327
- const token = argv[index];
3328
- if (token === void 0) continue;
3329
- const raw = portValueFrom(token, argv[index + 1]);
3330
- if (raw === void 0) continue;
3331
- const port = Number(raw);
3332
- if (Number.isInteger(port) && port > 0 && port <= MAX_PORT) return port;
3333
- }
3334
- };
3335
3369
  /**
3336
3370
  * Extract a `--stage` value from a single token (and the token after it, for the spaced form).
3337
3371
  *
@@ -3342,7 +3376,7 @@ const parsePortArg = (argv) => {
3342
3376
  * ```ts
3343
3377
  * stageValueFrom("--stage=dev", undefined); // "dev"
3344
3378
  * stageValueFrom("--stage", "dev"); // "dev"
3345
- * stageValueFrom("--port", "3000"); // undefined
3379
+ * stageValueFrom("--other", "x"); // undefined
3346
3380
  * ```
3347
3381
  */
3348
3382
  const stageValueFrom = (token, next) => {
@@ -3392,19 +3426,21 @@ const parseStageArg = (argv) => {
3392
3426
  */
3393
3427
  const createCliApi = (ctx) => ({
3394
3428
  /**
3395
- * Run the Worker locally. Resolves the port from `opts.port`, else a `--port <n>` CLI flag, else
3396
- * `ctx.config.port` (8787). Prints a branded dev-session banner, then delegates to deploy.dev; a
3397
- * `webBuild` hook (e.g. `() => webApp.cli.build()`) wires the web build into the dev loop so the
3398
- * site recompiles on change. A failure renders a branded `✗` line + non-zero exit, not a stack.
3429
+ * Run the Worker locally. The dev port comes ONLY from `opts.port` the consumer passes it (e.g.
3430
+ * parsed from its own CLI flags in scripts/dev.ts); when omitted it defaults to wrangler's 8787.
3431
+ * There is no hidden argv/config port resolution. Prints a branded dev-session banner, then
3432
+ * delegates to deploy.dev; a `webBuild` hook (e.g. `() => webApp.cli.build()`) wires the web build
3433
+ * into the dev loop so the site recompiles on change. A failure renders a branded `✗` line +
3434
+ * non-zero exit, not a stack.
3399
3435
  *
3400
3436
  * @param opts - Optional local dev options.
3401
- * @param opts.port - Local dev port to bind. Overrides the `--port` flag and the default.
3437
+ * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3402
3438
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3403
3439
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3404
3440
  * @returns Resolves when the dev session ends.
3405
3441
  * @example
3406
3442
  * ```ts
3407
- * await api.dev({ webBuild: () => web.cli.build() }); // port from --port or 8787
3443
+ * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3408
3444
  * ```
3409
3445
  */
3410
3446
  async dev(opts) {
@@ -3413,11 +3449,10 @@ const createCliApi = (ctx) => ({
3413
3449
  wordmark: "moku worker",
3414
3450
  label: "dev session"
3415
3451
  });
3416
- const port = opts?.port ?? parsePortArg(process.argv) ?? ctx.config.port;
3417
3452
  const stage = opts?.stage ?? parseStageArg(process.argv);
3418
3453
  try {
3419
3454
  await ctx.require(deployPlugin).dev({
3420
- port,
3455
+ ...opts?.port === void 0 ? {} : { port: opts.port },
3421
3456
  ...stage === void 0 ? {} : { stage },
3422
3457
  ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3423
3458
  });
@@ -3457,6 +3492,40 @@ const createCliApi = (ctx) => ({
3457
3492
  }
3458
3493
  },
3459
3494
  /**
3495
+ * Seed a configured D1 database from a SQL file (delegates to deploy.seed). Local by default;
3496
+ * `opts.remote` seeds Cloudflare. The stage is resolved from a `--stage <name>` CLI flag (so
3497
+ * `bun run dev --seed --stage dev` seeds the dev database). A failure renders a branded `✗` line
3498
+ * and sets a non-zero exit code rather than throwing.
3499
+ *
3500
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
3501
+ * @param opts - Optional options.
3502
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
3503
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
3504
+ * @returns Resolves once the seed completes (or after a failure is rendered).
3505
+ * @example
3506
+ * ```ts
3507
+ * await app.cli.seed("db/seed.sql"); // before app.cli.dev(...)
3508
+ * ```
3509
+ */
3510
+ async seed(sqlFile, opts) {
3511
+ const ui = (0, _moku_labs_common_cli.createBrandConsole)();
3512
+ ui.lockup({
3513
+ wordmark: "moku worker",
3514
+ label: "seed"
3515
+ });
3516
+ const stage = parseStageArg(process.argv);
3517
+ try {
3518
+ await ctx.require(deployPlugin).seed(sqlFile, {
3519
+ ...opts,
3520
+ ...stage === void 0 ? {} : { stage }
3521
+ });
3522
+ ui.check(true, "seeded", sqlFile);
3523
+ } catch (error) {
3524
+ ui.error(error instanceof Error ? error.message : String(error));
3525
+ process.exitCode = 1;
3526
+ }
3527
+ },
3528
+ /**
3460
3529
  * Verify the `.env` token (no sub) or print the config-derived token guidance (`"setup"`),
3461
3530
  * rendered in Moku style. `setup` works without a token; verify reports the resolved account.
3462
3531
  *
@@ -3552,6 +3621,25 @@ const createCliApi = (ctx) => ({
3552
3621
  //#region src/plugins/cli/handlers.ts
3553
3622
  /** Divider drawn before the native `wrangler dev` TUI so the moku preamble reads as one section. */
3554
3623
  const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3624
+ /** Deploy phases that are a slow, opaque wait (captured output) — worth a live spinner on a TTY. */
3625
+ const SPINNER_PHASES = new Set(["upload", "deploy"]);
3626
+ /** Braille spinner glyphs; advance one per tick. */
3627
+ const SPINNER_FRAMES = [
3628
+ "⠋",
3629
+ "⠙",
3630
+ "⠹",
3631
+ "⠸",
3632
+ "⠼",
3633
+ "⠴",
3634
+ "⠦",
3635
+ "⠧",
3636
+ "⠇",
3637
+ "⠏"
3638
+ ];
3639
+ /** Spinner tick interval (ms). */
3640
+ const SPINNER_TICK_MS = 80;
3641
+ /** Carriage-return + blanks + carriage-return that wipes the transient spinner line before settling. */
3642
+ const SPINNER_CLEAR = `\r${" ".repeat(72)}\r`;
3555
3643
  /**
3556
3644
  * Builds the hook handlers that turn global deploy events into a live progress TUI.
3557
3645
  * Each logs a clean, prefix-free message via `ctx.log`; the branded log sink (installed
@@ -3571,19 +3659,48 @@ const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3571
3659
  */
3572
3660
  const createCliHooks = (ctx) => {
3573
3661
  const ui = (0, _moku_labs_common_cli.createBrandConsole)();
3662
+ const { palette } = ui;
3663
+ let spinnerTimer;
3664
+ let spinnerLabel;
3665
+ const stopSpinner = () => {
3666
+ if (spinnerTimer !== void 0) {
3667
+ clearInterval(spinnerTimer);
3668
+ spinnerTimer = void 0;
3669
+ }
3670
+ if (spinnerLabel !== void 0) {
3671
+ process.stdout.write(SPINNER_CLEAR);
3672
+ ctx.log.info(spinnerLabel);
3673
+ spinnerLabel = void 0;
3674
+ }
3675
+ };
3676
+ const startSpinner = (label) => {
3677
+ spinnerLabel = label;
3678
+ let frame = 0;
3679
+ const text = `${label} …`;
3680
+ spinnerTimer = setInterval(() => {
3681
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0];
3682
+ frame += 1;
3683
+ process.stdout.write(`\r ${palette.pink(glyph)} ${palette.dim(text)}`);
3684
+ }, SPINNER_TICK_MS);
3685
+ };
3574
3686
  return {
3575
3687
  /**
3576
- * Log one clean line per pipeline phase: "phase" or "phase · detail".
3688
+ * Render one pipeline phase. Quick phases print a clean line ("phase" / "phase · detail"); the
3689
+ * slow opaque waits (upload / deploy) animate a branded spinner on a TTY, settling to a line when
3690
+ * the next phase or completion arrives. Off a TTY every phase is a plain line (unchanged).
3577
3691
  *
3578
3692
  * @param p - The deploy:phase event payload.
3579
3693
  * @example
3580
3694
  * ```ts
3581
3695
  * handler({ phase: "detect" }); // "detect"
3582
- * handler({ phase: "upload", detail: "3 files" }); // "upload · 3 files"
3696
+ * handler({ phase: "deploy" }); // spins on a TTY, else "deploy"
3583
3697
  * ```
3584
3698
  */
3585
3699
  "deploy:phase"(p) {
3586
- ctx.log.info(p.detail ? `${p.phase} · ${p.detail}` : p.phase);
3700
+ stopSpinner();
3701
+ const label = p.detail ? `${p.phase} · ${p.detail}` : p.phase;
3702
+ if (process.stdout.isTTY === true && SPINNER_PHASES.has(p.phase)) startSpinner(label);
3703
+ else ctx.log.info(label);
3587
3704
  },
3588
3705
  /**
3589
3706
  * Log one dev-session phase: "phase" or "phase · detail".
@@ -3633,6 +3750,7 @@ const createCliHooks = (ctx) => {
3633
3750
  * ```
3634
3751
  */
3635
3752
  "deploy:complete"(p) {
3753
+ stopSpinner();
3636
3754
  ctx.log.info(`deployed → ${p.url}`);
3637
3755
  }
3638
3756
  };
@@ -3651,7 +3769,7 @@ const createCliHooks = (ctx) => {
3651
3769
  */
3652
3770
  const cliPlugin = createPlugin("cli", {
3653
3771
  depends: [deployPlugin],
3654
- config: { port: 8787 },
3772
+ config: {},
3655
3773
  onInit: (ctx) => {
3656
3774
  ctx.log.clearSinks();
3657
3775
  ctx.log.addSink((0, _moku_labs_common_cli.brandedSink)("info"));
@@ -738,7 +738,8 @@ const createQueuesApi = (ctx) => {
738
738
  deployManifest: () => Object.values(ctx.config).map((instance) => ({
739
739
  kind: "queue",
740
740
  name: instance.name,
741
- binding: instance.binding
741
+ binding: instance.binding,
742
+ ...instance.onMessage ? { consumer: true } : {}
742
743
  }))
743
744
  };
744
745
  };
@@ -1960,8 +1961,9 @@ const runDev = async (ctx, opts, deps) => {
1960
1961
  //#region src/plugins/deploy/infra/plan.ts
1961
1962
  /**
1962
1963
  * Decide whether a single declared resource already exists in the account, recovering its id
1963
- * (kv/d1) when it does. Durable Objects are config-only (they ship with the script), so they are
1964
- * always treated as "missing" provisioning them is a no-op that just records the binding.
1964
+ * (kv/d1) when it does. Durable Objects are config-only they ship with the Worker (`wrangler
1965
+ * deploy` + the auto-derived DO migration create the namespace), never provisioned via the API — so
1966
+ * they are treated as already EXISTING, and the plan never re-offers to "create" them each deploy.
1965
1967
  *
1966
1968
  * @param resource - The declared resource descriptor.
1967
1969
  * @param existing - The indexed set of resources already in the account.
@@ -1989,7 +1991,7 @@ const checkExisting = (resource, existing) => {
1989
1991
  }
1990
1992
  case "r2": return { exists: existing.r2.has(resource.name) };
1991
1993
  case "queue": return { exists: existing.queue.has(resource.name) };
1992
- case "do": return { exists: false };
1994
+ case "do": return { exists: true };
1993
1995
  }
1994
1996
  };
1995
1997
  /**
@@ -2504,16 +2506,20 @@ const parseJsonc = (source) => {
2504
2506
  *
2505
2507
  * @param resources - All resource descriptors from the manifest.
2506
2508
  * @param ids - Captured Cloudflare ids keyed by binding; the entry's `id` is filled from here.
2507
- * @returns One wrangler KV namespace entry per kv resource (real `id` when known, else "").
2509
+ * @returns One wrangler KV namespace entry per kv resource real `id` when known, omitted otherwise
2510
+ * (wrangler rejects an empty `id`, but a local-dev / freshly-generated config validates without one).
2508
2511
  * @example
2509
2512
  * ```ts
2510
2513
  * const kv = buildKvNamespaces([{ kind: "kv", binding: "CACHE" }], { CACHE: "ns123" });
2511
2514
  * ```
2512
2515
  */
2513
- const buildKvNamespaces = (resources, ids) => resources.filter((resource) => resource.kind === "kv").map((resource) => ({
2514
- binding: resource.binding,
2515
- id: ids[resource.binding] ?? ""
2516
- }));
2516
+ const buildKvNamespaces = (resources, ids) => resources.filter((resource) => resource.kind === "kv").map((resource) => {
2517
+ const id = ids[resource.binding];
2518
+ return id ? {
2519
+ binding: resource.binding,
2520
+ id
2521
+ } : { binding: resource.binding };
2522
+ });
2517
2523
  /**
2518
2524
  * Build the wrangler `r2_buckets` array from the manifest's r2 resources.
2519
2525
  *
@@ -2540,31 +2546,41 @@ const buildR2Buckets = (resources) => resources.filter((resource) => resource.ki
2540
2546
  * ```
2541
2547
  */
2542
2548
  const buildD1Databases = (resources, ids) => resources.filter((resource) => resource.kind === "d1").map((resource) => {
2549
+ const databaseId = ids[resource.binding];
2543
2550
  const entry = {
2544
2551
  binding: resource.binding,
2545
- database_name: resource.name,
2546
- database_id: ids[resource.binding] ?? ""
2552
+ database_name: resource.name
2547
2553
  };
2554
+ if (databaseId) entry.database_id = databaseId;
2548
2555
  if (resource.migrations) entry.migrations_dir = resource.migrations;
2549
2556
  return entry;
2550
2557
  });
2551
2558
  /**
2552
- * Build the wrangler `queues` producers section from the manifest's queue resources.
2559
+ * Build the wrangler `queues` section (producers + consumers) from the manifest's queue resources.
2560
+ * Every queue is a `producer`; a queue flagged `consumer: true` (it declares an `onMessage` handler)
2561
+ * is ALSO registered as a `consumer` so wrangler delivers its messages to this Worker's queue()
2562
+ * handler — both locally under `wrangler dev` and in production. Without the consumer entry the
2563
+ * handler never runs (the bug that silently drops a queue-driven activity feed).
2553
2564
  *
2554
2565
  * @param resources - All resource descriptors from the manifest.
2555
- * @returns The queues section, or undefined when there are no queue resources.
2566
+ * @returns The queues section (producers, plus consumers when any), or undefined when there are none.
2556
2567
  * @example
2557
2568
  * ```ts
2558
- * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY" }]);
2569
+ * const q = buildQueues([{ kind: "queue", name: "tracker-activity", binding: "ACTIVITY", consumer: true }]);
2559
2570
  * ```
2560
2571
  */
2561
2572
  const buildQueues = (resources) => {
2562
2573
  const queueResources = resources.filter((resource) => resource.kind === "queue");
2563
2574
  if (queueResources.length === 0) return void 0;
2564
- return { producers: queueResources.map((resource) => ({
2575
+ const producers = queueResources.map((resource) => ({
2565
2576
  queue: resource.name,
2566
2577
  binding: resource.binding
2567
- })) };
2578
+ }));
2579
+ const consumers = queueResources.filter((resource) => resource.consumer === true).map((resource) => ({ queue: resource.name }));
2580
+ return consumers.length > 0 ? {
2581
+ producers,
2582
+ consumers
2583
+ } : { producers };
2568
2584
  };
2569
2585
  /**
2570
2586
  * Build the wrangler `durable_objects` bindings section from the manifest's do resources.
@@ -2672,7 +2688,8 @@ const wranglerExtra = (config) => {
2672
2688
  * @param configFile - Path to the wrangler config file.
2673
2689
  * @param manifest - The assembled deploy manifest.
2674
2690
  * @param ids - Captured Cloudflare ids keyed by binding (kv namespace id, d1 database id). Defaults
2675
- * to an empty map, in which case `id`/`database_id` are written as "" (e.g. the universal path).
2691
+ * to an empty map, in which case `id`/`database_id` are OMITTED (not "") so the generated config
2692
+ * still validates for local `dev` (wrangler rejects an empty id); a deploy fills the real ids.
2676
2693
  * @param extra - Extra top-level wrangler keys to merge in (the app's `deploy.wrangler` config).
2677
2694
  * @returns Resolves once the file is written.
2678
2695
  * @example
@@ -3047,6 +3064,32 @@ const guidedUpload = async (ctx, manifest, deps) => {
3047
3064
  return true;
3048
3065
  };
3049
3066
  /**
3067
+ * The final deploy step: confirm the target (guided only), run `wrangler deploy` with interactive
3068
+ * retry, then emit deploy:complete. Resolves false when the target gate or a deploy retry is declined.
3069
+ *
3070
+ * @param ctx - The deploy plugin context.
3071
+ * @param manifest - The assembled (or caller-supplied) deploy manifest.
3072
+ * @param stage - The resolved deploy stage (for the confirm prompt).
3073
+ * @param deps - Interactivity + the confirm prompt.
3074
+ * @returns True once deployed; false when the user declined the gate or a retry (abort).
3075
+ * @example
3076
+ * ```ts
3077
+ * if (!(await guidedDeployStep(ctx, manifest, stage, deps))) return emitAborted(ctx);
3078
+ * ```
3079
+ */
3080
+ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3081
+ if (!await deps.confirm(`Deploy "${manifest.name}" to ${stage}?`)) return false;
3082
+ ctx.emit("deploy:phase", { phase: "deploy" });
3083
+ const url = await guidedStep(() => runWrangler([
3084
+ "deploy",
3085
+ "--config",
3086
+ ctx.config.configFile
3087
+ ]), HINTS.deploy, deps);
3088
+ if (url === ABORTED) return false;
3089
+ ctx.emit("deploy:complete", { url });
3090
+ return true;
3091
+ };
3092
+ /**
3050
3093
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3051
3094
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3052
3095
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3088,10 +3131,9 @@ const createDeployApi = (ctx) => ({
3088
3131
  const ci = opts?.ci ?? ctx.config.ci;
3089
3132
  const stage = opts?.stage ?? ctx.global.stage;
3090
3133
  const interactive = !ci && stdoutIsTty();
3091
- const confirm = interactive ? createBrandPrompts().confirm : async (_question) => true;
3092
3134
  const deps = {
3093
3135
  interactive,
3094
- confirm
3136
+ confirm: interactive ? createBrandPrompts().confirm : async (_question) => true
3095
3137
  };
3096
3138
  ctx.emit("deploy:phase", { phase: "auth" });
3097
3139
  if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
@@ -3104,15 +3146,7 @@ const createDeployApi = (ctx) => ({
3104
3146
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3105
3147
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3106
3148
  if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3107
- if (!await confirm(`Deploy "${manifest.name}" to ${stage}?`)) return emitAborted(ctx);
3108
- ctx.emit("deploy:phase", { phase: "deploy" });
3109
- const url = await guidedStep(() => runWrangler([
3110
- "deploy",
3111
- "--config",
3112
- ctx.config.configFile
3113
- ]), HINTS.deploy, deps);
3114
- if (url === ABORTED) return emitAborted(ctx);
3115
- ctx.emit("deploy:complete", { url });
3149
+ if (!await guidedDeployStep(ctx, manifest, stage, deps)) return emitAborted(ctx);
3116
3150
  },
3117
3151
  /**
3118
3152
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3135,6 +3169,50 @@ const createDeployApi = (ctx) => ({
3135
3169
  await runDev(ctx, opts, realDevDeps());
3136
3170
  },
3137
3171
  /**
3172
+ * Execute a SQL file against a configured D1 database via `wrangler d1 execute` — for seeding dev
3173
+ * data. Local by default (applies that database's migrations first so the file's tables exist);
3174
+ * `opts.remote` seeds Cloudflare (schema is applied by `deploy`). Generates the wrangler config up
3175
+ * front so the binding resolves even on a first run. Streams wrangler's output.
3176
+ *
3177
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
3178
+ * @param opts - Optional options.
3179
+ * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3180
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
3181
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
3182
+ * @returns Resolves once wrangler finishes executing the file.
3183
+ * @example
3184
+ * ```ts
3185
+ * await api.seed("db/seed.sql"); // local default d1 (migrate, then execute)
3186
+ * await api.seed("db/seed.sql", { remote: true }); // remote d1
3187
+ * ```
3188
+ */
3189
+ async seed(sqlFile, opts) {
3190
+ if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
3191
+ const stage = opts?.stage ?? ctx.global.stage;
3192
+ await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
3193
+ const databases = ctx.require(d1Plugin).deployManifest();
3194
+ const wanted = opts?.binding;
3195
+ const matched = wanted === void 0 ? databases : databases.filter((database) => database.binding === wanted);
3196
+ const target = matched.length === 1 ? matched[0] : void 0;
3197
+ if (target === void 0) throw new Error(wanted === void 0 ? `[moku-worker] seed: ${String(databases.length)} d1 databases configured — pass { binding } to choose one.` : `[moku-worker] seed: no d1 database is bound to "${wanted}".`);
3198
+ const scope = opts?.remote === true ? "--remote" : "--local";
3199
+ if (scope === "--local" && target.migrations !== void 0) await runWranglerInherit([
3200
+ "d1",
3201
+ "migrations",
3202
+ "apply",
3203
+ target.binding,
3204
+ "--local"
3205
+ ]);
3206
+ await runWranglerInherit([
3207
+ "d1",
3208
+ "execute",
3209
+ target.binding,
3210
+ scope,
3211
+ "--file",
3212
+ sqlFile
3213
+ ]);
3214
+ },
3215
+ /**
3138
3216
  * Scaffold a starting wrangler config (and CI files when ci is set).
3139
3217
  * Idempotent: an existing config file is left untouched.
3140
3218
  *
@@ -3261,54 +3339,10 @@ const deployPlugin = createPlugin("deploy", {
3261
3339
  /**
3262
3340
  * @file cli plugin — argv parsing helpers (isolated so they unit-test without a real process).
3263
3341
  *
3264
- * `dev` resolves its port from the command line (`bun scripts/dev.ts --port 3000`) so a consumer
3265
- * never hardcodes it in the app. Pure: takes an argv array, reads no globals. Node-only tooling.
3342
+ * `deploy`/`dev` resolve the target stage from the command line (`--stage dev`) so a consumer never
3343
+ * hardcodes it. The dev PORT is not parsed here it comes only from the `dev()` argument (no hidden
3344
+ * argv/config resolution). Pure: takes an argv array, reads no globals. Node-only tooling.
3266
3345
  */
3267
- /** The valid TCP port range a `--port` value must fall within to be accepted. */
3268
- const MAX_PORT = 65535;
3269
- /**
3270
- * Extract a `--port`/`-p` value from a single token (and the token after it, for the spaced form).
3271
- *
3272
- * @param token - The current argv token.
3273
- * @param next - The following argv token (the value, for the `--port 3000` spaced form).
3274
- * @returns The raw string value when this token is a port flag, else undefined.
3275
- * @example
3276
- * ```ts
3277
- * portValueFrom("--port=3000", undefined); // "3000"
3278
- * portValueFrom("--port", "3000"); // "3000"
3279
- * portValueFrom("--config", "x"); // undefined
3280
- * ```
3281
- */
3282
- const portValueFrom = (token, next) => {
3283
- const inline = /^(?:--port|-p)=(.+)$/u.exec(token);
3284
- if (inline) return inline[1];
3285
- if (token === "--port" || token === "-p") return next;
3286
- };
3287
- /**
3288
- * Parse a `--port <n>` / `--port=<n>` / `-p <n>` flag out of an argv array.
3289
- *
3290
- * Returns the first valid port (a positive integer ≤ 65535) found, or undefined when the flag is
3291
- * absent or its value is not a usable port — letting the caller fall back to a default.
3292
- *
3293
- * @param argv - The argv array to scan (the caller passes the process argv).
3294
- * @returns The parsed port number, or undefined when no valid `--port`/`-p` flag is present.
3295
- * @example
3296
- * ```ts
3297
- * parsePortArg(["bun", "scripts/dev.ts", "--port", "3000"]); // 3000
3298
- * parsePortArg(["bun", "scripts/dev.ts", "--port=3000"]); // 3000
3299
- * parsePortArg(["bun", "scripts/dev.ts"]); // undefined
3300
- * ```
3301
- */
3302
- const parsePortArg = (argv) => {
3303
- for (let index = 0; index < argv.length; index++) {
3304
- const token = argv[index];
3305
- if (token === void 0) continue;
3306
- const raw = portValueFrom(token, argv[index + 1]);
3307
- if (raw === void 0) continue;
3308
- const port = Number(raw);
3309
- if (Number.isInteger(port) && port > 0 && port <= MAX_PORT) return port;
3310
- }
3311
- };
3312
3346
  /**
3313
3347
  * Extract a `--stage` value from a single token (and the token after it, for the spaced form).
3314
3348
  *
@@ -3319,7 +3353,7 @@ const parsePortArg = (argv) => {
3319
3353
  * ```ts
3320
3354
  * stageValueFrom("--stage=dev", undefined); // "dev"
3321
3355
  * stageValueFrom("--stage", "dev"); // "dev"
3322
- * stageValueFrom("--port", "3000"); // undefined
3356
+ * stageValueFrom("--other", "x"); // undefined
3323
3357
  * ```
3324
3358
  */
3325
3359
  const stageValueFrom = (token, next) => {
@@ -3369,19 +3403,21 @@ const parseStageArg = (argv) => {
3369
3403
  */
3370
3404
  const createCliApi = (ctx) => ({
3371
3405
  /**
3372
- * Run the Worker locally. Resolves the port from `opts.port`, else a `--port <n>` CLI flag, else
3373
- * `ctx.config.port` (8787). Prints a branded dev-session banner, then delegates to deploy.dev; a
3374
- * `webBuild` hook (e.g. `() => webApp.cli.build()`) wires the web build into the dev loop so the
3375
- * site recompiles on change. A failure renders a branded `✗` line + non-zero exit, not a stack.
3406
+ * Run the Worker locally. The dev port comes ONLY from `opts.port` the consumer passes it (e.g.
3407
+ * parsed from its own CLI flags in scripts/dev.ts); when omitted it defaults to wrangler's 8787.
3408
+ * There is no hidden argv/config port resolution. Prints a branded dev-session banner, then
3409
+ * delegates to deploy.dev; a `webBuild` hook (e.g. `() => webApp.cli.build()`) wires the web build
3410
+ * into the dev loop so the site recompiles on change. A failure renders a branded `✗` line +
3411
+ * non-zero exit, not a stack.
3376
3412
  *
3377
3413
  * @param opts - Optional local dev options.
3378
- * @param opts.port - Local dev port to bind. Overrides the `--port` flag and the default.
3414
+ * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3379
3415
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3380
3416
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3381
3417
  * @returns Resolves when the dev session ends.
3382
3418
  * @example
3383
3419
  * ```ts
3384
- * await api.dev({ webBuild: () => web.cli.build() }); // port from --port or 8787
3420
+ * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3385
3421
  * ```
3386
3422
  */
3387
3423
  async dev(opts) {
@@ -3390,11 +3426,10 @@ const createCliApi = (ctx) => ({
3390
3426
  wordmark: "moku worker",
3391
3427
  label: "dev session"
3392
3428
  });
3393
- const port = opts?.port ?? parsePortArg(process.argv) ?? ctx.config.port;
3394
3429
  const stage = opts?.stage ?? parseStageArg(process.argv);
3395
3430
  try {
3396
3431
  await ctx.require(deployPlugin).dev({
3397
- port,
3432
+ ...opts?.port === void 0 ? {} : { port: opts.port },
3398
3433
  ...stage === void 0 ? {} : { stage },
3399
3434
  ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3400
3435
  });
@@ -3434,6 +3469,40 @@ const createCliApi = (ctx) => ({
3434
3469
  }
3435
3470
  },
3436
3471
  /**
3472
+ * Seed a configured D1 database from a SQL file (delegates to deploy.seed). Local by default;
3473
+ * `opts.remote` seeds Cloudflare. The stage is resolved from a `--stage <name>` CLI flag (so
3474
+ * `bun run dev --seed --stage dev` seeds the dev database). A failure renders a branded `✗` line
3475
+ * and sets a non-zero exit code rather than throwing.
3476
+ *
3477
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
3478
+ * @param opts - Optional options.
3479
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
3480
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
3481
+ * @returns Resolves once the seed completes (or after a failure is rendered).
3482
+ * @example
3483
+ * ```ts
3484
+ * await app.cli.seed("db/seed.sql"); // before app.cli.dev(...)
3485
+ * ```
3486
+ */
3487
+ async seed(sqlFile, opts) {
3488
+ const ui = createBrandConsole();
3489
+ ui.lockup({
3490
+ wordmark: "moku worker",
3491
+ label: "seed"
3492
+ });
3493
+ const stage = parseStageArg(process.argv);
3494
+ try {
3495
+ await ctx.require(deployPlugin).seed(sqlFile, {
3496
+ ...opts,
3497
+ ...stage === void 0 ? {} : { stage }
3498
+ });
3499
+ ui.check(true, "seeded", sqlFile);
3500
+ } catch (error) {
3501
+ ui.error(error instanceof Error ? error.message : String(error));
3502
+ process.exitCode = 1;
3503
+ }
3504
+ },
3505
+ /**
3437
3506
  * Verify the `.env` token (no sub) or print the config-derived token guidance (`"setup"`),
3438
3507
  * rendered in Moku style. `setup` works without a token; verify reports the resolved account.
3439
3508
  *
@@ -3529,6 +3598,25 @@ const createCliApi = (ctx) => ({
3529
3598
  //#region src/plugins/cli/handlers.ts
3530
3599
  /** Divider drawn before the native `wrangler dev` TUI so the moku preamble reads as one section. */
3531
3600
  const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3601
+ /** Deploy phases that are a slow, opaque wait (captured output) — worth a live spinner on a TTY. */
3602
+ const SPINNER_PHASES = new Set(["upload", "deploy"]);
3603
+ /** Braille spinner glyphs; advance one per tick. */
3604
+ const SPINNER_FRAMES = [
3605
+ "⠋",
3606
+ "⠙",
3607
+ "⠹",
3608
+ "⠸",
3609
+ "⠼",
3610
+ "⠴",
3611
+ "⠦",
3612
+ "⠧",
3613
+ "⠇",
3614
+ "⠏"
3615
+ ];
3616
+ /** Spinner tick interval (ms). */
3617
+ const SPINNER_TICK_MS = 80;
3618
+ /** Carriage-return + blanks + carriage-return that wipes the transient spinner line before settling. */
3619
+ const SPINNER_CLEAR = `\r${" ".repeat(72)}\r`;
3532
3620
  /**
3533
3621
  * Builds the hook handlers that turn global deploy events into a live progress TUI.
3534
3622
  * Each logs a clean, prefix-free message via `ctx.log`; the branded log sink (installed
@@ -3548,19 +3636,48 @@ const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3548
3636
  */
3549
3637
  const createCliHooks = (ctx) => {
3550
3638
  const ui = createBrandConsole();
3639
+ const { palette } = ui;
3640
+ let spinnerTimer;
3641
+ let spinnerLabel;
3642
+ const stopSpinner = () => {
3643
+ if (spinnerTimer !== void 0) {
3644
+ clearInterval(spinnerTimer);
3645
+ spinnerTimer = void 0;
3646
+ }
3647
+ if (spinnerLabel !== void 0) {
3648
+ process.stdout.write(SPINNER_CLEAR);
3649
+ ctx.log.info(spinnerLabel);
3650
+ spinnerLabel = void 0;
3651
+ }
3652
+ };
3653
+ const startSpinner = (label) => {
3654
+ spinnerLabel = label;
3655
+ let frame = 0;
3656
+ const text = `${label} …`;
3657
+ spinnerTimer = setInterval(() => {
3658
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0];
3659
+ frame += 1;
3660
+ process.stdout.write(`\r ${palette.pink(glyph)} ${palette.dim(text)}`);
3661
+ }, SPINNER_TICK_MS);
3662
+ };
3551
3663
  return {
3552
3664
  /**
3553
- * Log one clean line per pipeline phase: "phase" or "phase · detail".
3665
+ * Render one pipeline phase. Quick phases print a clean line ("phase" / "phase · detail"); the
3666
+ * slow opaque waits (upload / deploy) animate a branded spinner on a TTY, settling to a line when
3667
+ * the next phase or completion arrives. Off a TTY every phase is a plain line (unchanged).
3554
3668
  *
3555
3669
  * @param p - The deploy:phase event payload.
3556
3670
  * @example
3557
3671
  * ```ts
3558
3672
  * handler({ phase: "detect" }); // "detect"
3559
- * handler({ phase: "upload", detail: "3 files" }); // "upload · 3 files"
3673
+ * handler({ phase: "deploy" }); // spins on a TTY, else "deploy"
3560
3674
  * ```
3561
3675
  */
3562
3676
  "deploy:phase"(p) {
3563
- ctx.log.info(p.detail ? `${p.phase} · ${p.detail}` : p.phase);
3677
+ stopSpinner();
3678
+ const label = p.detail ? `${p.phase} · ${p.detail}` : p.phase;
3679
+ if (process.stdout.isTTY === true && SPINNER_PHASES.has(p.phase)) startSpinner(label);
3680
+ else ctx.log.info(label);
3564
3681
  },
3565
3682
  /**
3566
3683
  * Log one dev-session phase: "phase" or "phase · detail".
@@ -3610,6 +3727,7 @@ const createCliHooks = (ctx) => {
3610
3727
  * ```
3611
3728
  */
3612
3729
  "deploy:complete"(p) {
3730
+ stopSpinner();
3613
3731
  ctx.log.info(`deployed → ${p.url}`);
3614
3732
  }
3615
3733
  };
@@ -3628,7 +3746,7 @@ const createCliHooks = (ctx) => {
3628
3746
  */
3629
3747
  const cliPlugin = createPlugin("cli", {
3630
3748
  depends: [deployPlugin],
3631
- config: { port: 8787 },
3749
+ config: {},
3632
3750
  onInit: (ctx) => {
3633
3751
  ctx.log.clearSinks();
3634
3752
  ctx.log.addSink(brandedSink("info"));
package/dist/cli.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-C8DdTtzn.cjs");
2
+ const require_cli = require("./cli-BPnG_JGR.cjs");
3
3
  exports.cliPlugin = require_cli.cliPlugin;
4
4
  exports.deployPlugin = require_cli.deployPlugin;
package/dist/cli.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-BKOUpKtC.cjs";
1
+ import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-DCweBI9s.cjs";
2
2
  export { type ExternalManifest, type ResourceManifest, cliPlugin, deployPlugin };
package/dist/cli.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-BKOUpKtC.mjs";
1
+ import { i as ResourceManifest, n as cliPlugin, r as ExternalManifest, t as deployPlugin } from "./index-DCweBI9s.mjs";
2
2
  export { type ExternalManifest, type ResourceManifest, cliPlugin, deployPlugin };
package/dist/cli.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as deployPlugin, t as cliPlugin } from "./cli-Bb37rYq_.mjs";
1
+ import { n as deployPlugin, t as cliPlugin } from "./cli-D8UmSqh4.mjs";
2
2
  export { cliPlugin, deployPlugin };
@@ -178,6 +178,7 @@ type ResourceManifest = {
178
178
  kind: "queue";
179
179
  name: string;
180
180
  binding: string;
181
+ consumer?: boolean;
181
182
  } | {
182
183
  kind: "do";
183
184
  binding: string;
@@ -252,34 +253,35 @@ type TokenRequirement = {
252
253
  };
253
254
  //#endregion
254
255
  //#region src/plugins/cli/types.d.ts
255
- /** Resolved configuration for the cli plugin. Flat; complete defaults so omission never yields undefined. */
256
- type Config = {
257
- /**
258
- * Default local dev port forwarded to deploy.dev when dev() gets no port.
259
- * Passed through to `wrangler dev --port <n>`.
260
- *
261
- * @default 8787
262
- */
263
- readonly port: number;
264
- };
256
+ /**
257
+ * Resolved configuration for the cli plugin. The cli surface is configuration-free: the dev port is
258
+ * NOT set here (it comes only from `dev({ port })`), so there are no keys to set under
259
+ * `pluginConfigs.cli`.
260
+ */
261
+ type Config = Record<string, never>;
265
262
  /** Public api surface of the cli plugin, mounted at app.cli.*. */
266
263
  type Api = {
267
264
  /**
268
- * Run the Worker locally via Wrangler (delegates to deploy.dev). Resolves the port from
269
- * `opts.port`, else a `--port <n>` CLI flag, else the configured default (8787). A failure renders
270
- * a branded `✗` line and sets a non-zero exit code rather than throwing a raw stack trace.
265
+ * Run the Worker locally via Wrangler (delegates to deploy.dev). The dev port comes only from
266
+ * `opts.port` the consumer passes it (e.g. parsed from its own CLI flags); it defaults to 8787
267
+ * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
+ * throwing a raw stack trace.
271
269
  *
272
- * @param opts - Optional port override and web build hook.
273
- * @param opts.port - Local dev port to bind (overrides the `--port` flag and the default).
270
+ * @param opts - Optional port, stage, and web build hook.
271
+ * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
272
+ * @param opts.stage - Stage for the generated wrangler config's resource names. Falls back to the
273
+ * `--stage` CLI flag, then the app's configured stage. Pass it explicitly from a script for a
274
+ * self-documenting `dev({ stage })` instead of relying on the hidden flag.
274
275
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
275
276
  * @returns Resolves when the dev session ends.
276
277
  * @example
277
278
  * ```ts
278
- * await app.cli.dev({ webBuild: () => web.cli.build() }); // port from --port or 8787
279
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
279
280
  * ```
280
281
  */
281
282
  dev(opts?: {
282
283
  port?: number;
284
+ stage?: string;
283
285
  webBuild?: WebBuild;
284
286
  }): Promise<void>;
285
287
  /**
@@ -287,20 +289,44 @@ type Api = {
287
289
  * `{ ci: true }` for the automated/non-interactive path (CI). A failure renders a branded `✗`
288
290
  * line and sets a non-zero exit code rather than throwing a raw stack trace.
289
291
  *
290
- * @param opts - Optional ci flag and a web build hook.
292
+ * @param opts - Optional ci flag, stage, and a web build hook.
291
293
  * @param opts.ci - Automated mode: never prompts, auto-confirms. Omit/false → guided on a TTY.
294
+ * @param opts.stage - Stage for the generated wrangler config's resource names (e.g. "production",
295
+ * "staging"). Falls back to the `--stage` CLI flag, then the app's configured stage. Pass it
296
+ * explicitly from a script for a self-documenting `deploy({ stage })` instead of the hidden flag.
292
297
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
293
298
  * @returns Resolves once the deploy completes (or after a failure is rendered).
294
299
  * @example
295
300
  * ```ts
296
- * await app.cli.deploy({ webBuild: () => web.cli.build() }); // guided
297
- * await app.cli.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI
301
+ * await app.cli.deploy({ stage: "production", webBuild: () => web.cli.build() }); // guided
302
+ * await app.cli.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI
298
303
  * ```
299
304
  */
300
305
  deploy(opts?: {
301
306
  ci?: boolean;
307
+ stage?: string;
302
308
  webBuild?: WebBuild;
303
309
  }): Promise<void>;
310
+ /**
311
+ * Seed a configured D1 database from a SQL file (delegates to deploy.seed). Local by default
312
+ * (applies the database's migrations first so its tables exist, then executes the file);
313
+ * `opts.remote` seeds Cloudflare. A failure renders a branded `✗` line and sets a non-zero exit
314
+ * code rather than throwing.
315
+ *
316
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
317
+ * @param opts - Optional options.
318
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
319
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
320
+ * @returns Resolves once the seed completes (or after a failure is rendered).
321
+ * @example
322
+ * ```ts
323
+ * await app.cli.seed("db/seed.sql"); // local; --stage honored
324
+ * ```
325
+ */
326
+ seed(sqlFile: string, opts?: {
327
+ binding?: string;
328
+ remote?: boolean;
329
+ }): Promise<void>;
304
330
  /**
305
331
  * Verify the `.env` Cloudflare token (no sub), or print the config-derived token-creation
306
332
  * guidance (`"setup"`). Delegates to deploy.verifyAuth() / deploy.tokenInstructions().
@@ -389,6 +415,11 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
389
415
  stage?: string;
390
416
  webBuild?: WebBuild;
391
417
  }): Promise<void>;
418
+ seed(sqlFile: string, opts?: {
419
+ stage?: string;
420
+ binding?: string;
421
+ remote?: boolean;
422
+ }): Promise<void>;
392
423
  init: (opts?: {
393
424
  ci?: boolean;
394
425
  }) => Promise<void>;
@@ -178,6 +178,7 @@ type ResourceManifest = {
178
178
  kind: "queue";
179
179
  name: string;
180
180
  binding: string;
181
+ consumer?: boolean;
181
182
  } | {
182
183
  kind: "do";
183
184
  binding: string;
@@ -252,34 +253,35 @@ type TokenRequirement = {
252
253
  };
253
254
  //#endregion
254
255
  //#region src/plugins/cli/types.d.ts
255
- /** Resolved configuration for the cli plugin. Flat; complete defaults so omission never yields undefined. */
256
- type Config = {
257
- /**
258
- * Default local dev port forwarded to deploy.dev when dev() gets no port.
259
- * Passed through to `wrangler dev --port <n>`.
260
- *
261
- * @default 8787
262
- */
263
- readonly port: number;
264
- };
256
+ /**
257
+ * Resolved configuration for the cli plugin. The cli surface is configuration-free: the dev port is
258
+ * NOT set here (it comes only from `dev({ port })`), so there are no keys to set under
259
+ * `pluginConfigs.cli`.
260
+ */
261
+ type Config = Record<string, never>;
265
262
  /** Public api surface of the cli plugin, mounted at app.cli.*. */
266
263
  type Api = {
267
264
  /**
268
- * Run the Worker locally via Wrangler (delegates to deploy.dev). Resolves the port from
269
- * `opts.port`, else a `--port <n>` CLI flag, else the configured default (8787). A failure renders
270
- * a branded `✗` line and sets a non-zero exit code rather than throwing a raw stack trace.
265
+ * Run the Worker locally via Wrangler (delegates to deploy.dev). The dev port comes only from
266
+ * `opts.port` the consumer passes it (e.g. parsed from its own CLI flags); it defaults to 8787
267
+ * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
+ * throwing a raw stack trace.
271
269
  *
272
- * @param opts - Optional port override and web build hook.
273
- * @param opts.port - Local dev port to bind (overrides the `--port` flag and the default).
270
+ * @param opts - Optional port, stage, and web build hook.
271
+ * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
272
+ * @param opts.stage - Stage for the generated wrangler config's resource names. Falls back to the
273
+ * `--stage` CLI flag, then the app's configured stage. Pass it explicitly from a script for a
274
+ * self-documenting `dev({ stage })` instead of relying on the hidden flag.
274
275
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
275
276
  * @returns Resolves when the dev session ends.
276
277
  * @example
277
278
  * ```ts
278
- * await app.cli.dev({ webBuild: () => web.cli.build() }); // port from --port or 8787
279
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
279
280
  * ```
280
281
  */
281
282
  dev(opts?: {
282
283
  port?: number;
284
+ stage?: string;
283
285
  webBuild?: WebBuild;
284
286
  }): Promise<void>;
285
287
  /**
@@ -287,20 +289,44 @@ type Api = {
287
289
  * `{ ci: true }` for the automated/non-interactive path (CI). A failure renders a branded `✗`
288
290
  * line and sets a non-zero exit code rather than throwing a raw stack trace.
289
291
  *
290
- * @param opts - Optional ci flag and a web build hook.
292
+ * @param opts - Optional ci flag, stage, and a web build hook.
291
293
  * @param opts.ci - Automated mode: never prompts, auto-confirms. Omit/false → guided on a TTY.
294
+ * @param opts.stage - Stage for the generated wrangler config's resource names (e.g. "production",
295
+ * "staging"). Falls back to the `--stage` CLI flag, then the app's configured stage. Pass it
296
+ * explicitly from a script for a self-documenting `deploy({ stage })` instead of the hidden flag.
292
297
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
293
298
  * @returns Resolves once the deploy completes (or after a failure is rendered).
294
299
  * @example
295
300
  * ```ts
296
- * await app.cli.deploy({ webBuild: () => web.cli.build() }); // guided
297
- * await app.cli.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI
301
+ * await app.cli.deploy({ stage: "production", webBuild: () => web.cli.build() }); // guided
302
+ * await app.cli.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI
298
303
  * ```
299
304
  */
300
305
  deploy(opts?: {
301
306
  ci?: boolean;
307
+ stage?: string;
302
308
  webBuild?: WebBuild;
303
309
  }): Promise<void>;
310
+ /**
311
+ * Seed a configured D1 database from a SQL file (delegates to deploy.seed). Local by default
312
+ * (applies the database's migrations first so its tables exist, then executes the file);
313
+ * `opts.remote` seeds Cloudflare. A failure renders a branded `✗` line and sets a non-zero exit
314
+ * code rather than throwing.
315
+ *
316
+ * @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
317
+ * @param opts - Optional options.
318
+ * @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
319
+ * @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
320
+ * @returns Resolves once the seed completes (or after a failure is rendered).
321
+ * @example
322
+ * ```ts
323
+ * await app.cli.seed("db/seed.sql"); // local; --stage honored
324
+ * ```
325
+ */
326
+ seed(sqlFile: string, opts?: {
327
+ binding?: string;
328
+ remote?: boolean;
329
+ }): Promise<void>;
304
330
  /**
305
331
  * Verify the `.env` Cloudflare token (no sub), or print the config-derived token-creation
306
332
  * guidance (`"setup"`). Delegates to deploy.verifyAuth() / deploy.tokenInstructions().
@@ -389,6 +415,11 @@ declare const deployPlugin: import("@moku-labs/core").PluginInstance<"deploy", C
389
415
  stage?: string;
390
416
  webBuild?: WebBuild;
391
417
  }): Promise<void>;
418
+ seed(sqlFile: string, opts?: {
419
+ stage?: string;
420
+ binding?: string;
421
+ remote?: boolean;
422
+ }): Promise<void>;
392
423
  init: (opts?: {
393
424
  ci?: boolean;
394
425
  }) => Promise<void>;
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-C8DdTtzn.cjs");
2
+ const require_cli = require("./cli-BPnG_JGR.cjs");
3
3
  let _moku_labs_common = require("@moku-labs/common");
4
4
  //#region src/env-provider.ts
5
5
  /**
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-BKOUpKtC.cjs";
1
+ import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-DCweBI9s.cjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
4
4
 
@@ -852,12 +852,15 @@ type Api = QueueProducerApi & {
852
852
  * Return this plugin's deploy metadata (one entry per configured instance), read by the deploy
853
853
  * plugin. Build-time only — takes no env.
854
854
  *
855
- * @returns One queue deploy descriptor per configured instance.
855
+ * @returns One queue deploy descriptor per configured instance. `consumer: true` marks an instance
856
+ * that declares an `onMessage` handler — the deploy plugin registers those as wrangler
857
+ * `consumers` so this Worker actually receives the queue's messages.
856
858
  */
857
859
  deployManifest(): Array<{
858
860
  kind: "queue";
859
861
  name: string;
860
862
  binding: string;
863
+ consumer?: boolean;
861
864
  }>;
862
865
  };
863
866
  /**
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-BKOUpKtC.mjs";
1
+ import { a as WorkerConfig, c as WorkerPluginCtx, i as ResourceManifest, n as cliPlugin, o as WorkerEnv, r as ExternalManifest, s as WorkerEvents, t as deployPlugin } from "./index-DCweBI9s.mjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  import { PluginCtx, PluginCtx as PluginCtx$1, PluginInstance } from "@moku-labs/core";
4
4
 
@@ -852,12 +852,15 @@ type Api = QueueProducerApi & {
852
852
  * Return this plugin's deploy metadata (one entry per configured instance), read by the deploy
853
853
  * plugin. Build-time only — takes no env.
854
854
  *
855
- * @returns One queue deploy descriptor per configured instance.
855
+ * @returns One queue deploy descriptor per configured instance. `consumer: true` marks an instance
856
+ * that declares an `onMessage` handler — the deploy plugin registers those as wrangler
857
+ * `consumers` so this Worker actually receives the queue's messages.
856
858
  */
857
859
  deployManifest(): Array<{
858
860
  kind: "queue";
859
861
  name: string;
860
862
  binding: string;
863
+ consumer?: boolean;
861
864
  }>;
862
865
  };
863
866
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as kvPlugin, c as d1Plugin, d as createCore, f as createPlugin$1, i as queuesPlugin, l as bindingsPlugin, n as deployPlugin, o as durableObjectsPlugin, p as stagePlugin, r as storagePlugin, s as defineDurableObject, t as cliPlugin, u as coreConfig } from "./cli-Bb37rYq_.mjs";
1
+ import { a as kvPlugin, c as d1Plugin, d as createCore, f as createPlugin$1, i as queuesPlugin, l as bindingsPlugin, n as deployPlugin, o as durableObjectsPlugin, p as stagePlugin, r as storagePlugin, s as defineDurableObject, t as cliPlugin, u as coreConfig } from "./cli-D8UmSqh4.mjs";
2
2
  import { envPlugin, logPlugin } from "@moku-labs/common";
3
3
  //#region src/env-provider.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moku-labs/worker",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Cloudflare Worker framework for Moku — Durable Objects, Queues, R2, D1, and KV plugins that compose with Moku Web.",
5
5
  "repository": {
6
6
  "type": "git",