@moku-labs/worker 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1631,6 +1631,76 @@ const runWranglerInherit = (args) => {
1631
1631
  });
1632
1632
  };
1633
1633
  //#endregion
1634
+ //#region src/plugins/deploy/seed.ts
1635
+ /**
1636
+ * @file deploy plugin — shared D1 seed helpers (resolve the target db, run a configured seed).
1637
+ *
1638
+ * Pure orchestration over an INJECTED wrangler runner, so the post-deploy REMOTE seed (api.ts) and
1639
+ * the dev-session LOCAL seed (dev/runner.ts) stay in lockstep — same file, same KV-reset semantics,
1640
+ * differing only in the `--remote` / `--local` scope. Migrations are NOT applied here: each caller
1641
+ * applies the schema first (the deploy's migration step / dev's local-migrate step), then seeds.
1642
+ * Node-only; never imported by the runtime Worker bundle.
1643
+ */
1644
+ /**
1645
+ * Resolve the single configured d1 database (or the one bound to `binding` when several exist) from
1646
+ * the d1 plugin's manifest. The shared resolver behind `seed()`, the post-deploy seed, and the dev
1647
+ * seed; throws a branded error when the choice is ambiguous (none/several, no binding) or unknown.
1648
+ *
1649
+ * @param ctx - The deploy plugin context.
1650
+ * @param binding - The d1 binding to target when more than one is configured; the sole one otherwise.
1651
+ * @returns The resolved d1 resource descriptor (its binding + optional migrations dir).
1652
+ * @throws {Error} When no single database resolves (none/several without a binding, or unknown binding).
1653
+ * @example
1654
+ * ```ts
1655
+ * const db = resolveD1(ctx, "DB");
1656
+ * ```
1657
+ */
1658
+ const resolveD1 = (ctx, binding) => {
1659
+ const databases = ctx.require(d1Plugin).deployManifest();
1660
+ const matched = binding === void 0 ? databases : databases.filter((db) => db.binding === binding);
1661
+ const target = matched.length === 1 ? matched[0] : void 0;
1662
+ if (target === void 0) throw new Error(binding === 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 "${binding}".`);
1663
+ return target;
1664
+ };
1665
+ /**
1666
+ * Run a configured seed against one scope: execute the seed SQL against the d1 database, then delete
1667
+ * each configured cached KV key so the next read rebuilds it from the freshly-seeded rows. The
1668
+ * schema is assumed to exist (the caller applies migrations first), so this never migrates. The
1669
+ * wrangler runner is injected so the same orchestration serves the streamed deploy path and the
1670
+ * injectable dev path.
1671
+ *
1672
+ * @param ctx - The deploy plugin context.
1673
+ * @param run - The wrangler runner to execute each command through.
1674
+ * @param seed - The resolved seed config (SQL file, optional binding, KV keys to reset).
1675
+ * @param scope - The wrangler scope: `--remote` (deploy) or `--local` (dev).
1676
+ * @returns Resolves once the seed file has executed and every cached KV key is cleared.
1677
+ * @throws {Error} When no d1 database is configured, or the seed's binding cannot be resolved.
1678
+ * @example
1679
+ * ```ts
1680
+ * await runConfiguredSeed(ctx, runWranglerInherit, ctx.config.seed, "--remote");
1681
+ * ```
1682
+ */
1683
+ const runConfiguredSeed = async (ctx, run, seed, scope) => {
1684
+ if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
1685
+ await run([
1686
+ "d1",
1687
+ "execute",
1688
+ resolveD1(ctx, seed.binding).binding,
1689
+ scope,
1690
+ "--file",
1691
+ seed.file
1692
+ ]);
1693
+ for (const entry of seed.resetKv ?? []) await run([
1694
+ "kv",
1695
+ "key",
1696
+ "delete",
1697
+ entry.key,
1698
+ "--binding",
1699
+ entry.binding,
1700
+ scope
1701
+ ]);
1702
+ };
1703
+ //#endregion
1634
1704
  //#region src/plugins/deploy/dev/build.ts
1635
1705
  /**
1636
1706
  * @file deploy plugin — dev site-rebuild resolution.
@@ -1724,7 +1794,8 @@ const buildSite = async (ctx, webBuild) => {
1724
1794
  * @file deploy plugin — debounced filesystem watcher for dev.
1725
1795
  *
1726
1796
  * Watches the top-level directories implied by the config globs (recursive) and fires a debounced
1727
- * change callback with the last changed path. Uses node:fs.watch no extra dependency.
1797
+ * change callback with the SET of paths changed in the window (so a burst of edits coalesces into
1798
+ * one rebuild that knows every changed file). Uses node:fs.watch — no extra dependency.
1728
1799
  * Node-only; never imported by the runtime Worker bundle.
1729
1800
  */
1730
1801
  /**
@@ -1747,27 +1818,29 @@ const watchDirectories = (globs) => {
1747
1818
  return [...directories];
1748
1819
  };
1749
1820
  /**
1750
- * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with
1751
- * the last changed path. Missing directories are skipped silently.
1821
+ * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with the
1822
+ * distinct set of paths changed within the window. Missing directories are skipped silently.
1752
1823
  *
1753
1824
  * @param globs - Watch globs.
1754
1825
  * @param debounceMs - Coalesce rapid changes into one callback within this window.
1755
- * @param onChange - Called with the last changed path after the debounce settles.
1826
+ * @param onChange - Called with the changed paths (snapshot of the window) after the debounce settles.
1756
1827
  * @returns A handle whose close() stops all watchers and cancels any pending callback.
1757
1828
  * @example
1758
1829
  * ```ts
1759
- * const handle = watchPaths(["src/**\/*.ts"], 120, p => rebuild(p));
1830
+ * const handle = watchPaths(["src/**\/*.ts"], 120, paths => rebuild(paths));
1760
1831
  * handle.close();
1761
1832
  * ```
1762
1833
  */
1763
1834
  const watchPaths = (globs, debounceMs, onChange) => {
1764
1835
  let timer;
1765
- let lastPath = "";
1836
+ const changed = /* @__PURE__ */ new Set();
1766
1837
  const fire = (changedPath) => {
1767
- lastPath = changedPath;
1838
+ changed.add(changedPath);
1768
1839
  if (timer !== void 0) clearTimeout(timer);
1769
1840
  timer = setTimeout(() => {
1770
- onChange(lastPath);
1841
+ const batch = [...changed];
1842
+ changed.clear();
1843
+ onChange(batch);
1771
1844
  }, debounceMs);
1772
1845
  };
1773
1846
  const watchers = [];
@@ -1897,27 +1970,50 @@ const realDevDeps = () => ({
1897
1970
  */
1898
1971
  const d1MigrationBindings = (ctx) => ctx.has("d1") ? ctx.require(d1Plugin).deployManifest().filter((manifest) => manifest.migrations !== void 0).map((manifest) => manifest.binding) : [];
1899
1972
  /**
1900
- * Rebuild the site once and announce the result. A failed build keeps the session alive (it just
1901
- * emits dev:error and serves the last good build).
1973
+ * One-line description of a changed-path batch for the `dev:phase rebuild` detail: the single path,
1974
+ * or the first path plus a `(+N more)` tail. Empty batches (defensive) read as "site".
1975
+ *
1976
+ * @param paths - The changed paths the watcher coalesced for this rebuild.
1977
+ * @returns The detail string for the rebuild phase event.
1978
+ * @example
1979
+ * ```ts
1980
+ * describeChanges(["src/a.ts", "src/b.css"]); // "src/a.ts (+1 more)"
1981
+ * ```
1982
+ */
1983
+ const describeChanges = (paths) => {
1984
+ const [first, ...rest] = paths;
1985
+ if (first === void 0) return "site";
1986
+ return rest.length === 0 ? first : `${first} (+${String(rest.length)} more)`;
1987
+ };
1988
+ /**
1989
+ * Rebuild the site once for a changed-path batch and announce the result. The FAST path is the
1990
+ * incremental `onChange(changedPaths)` hook (e.g. `web.cli.update`) when wired; otherwise it falls
1991
+ * back to a full `webBuild()` rebuild (via deps.build) — the prior behavior. A failed rebuild keeps
1992
+ * the session alive (it just emits dev:error and serves the last good build). Both paths share one
1993
+ * `dev:phase rebuild` → `dev:rebuilt`/`dev:error` envelope so the branded dev TUI is identical.
1902
1994
  *
1903
1995
  * @param ctx - The deploy plugin context.
1904
1996
  * @param deps - The injected dev deps.
1905
- * @param changedPath - The path that triggered the rebuild.
1906
- * @param webBuild - Optional call-time web build hook threaded into the rebuild.
1997
+ * @param changedPaths - The paths that triggered the rebuild (the watcher's debounced set).
1998
+ * @param hooks - The consumer rebuild hooks.
1999
+ * @param hooks.webBuild - Full rebuild (used when `onChange` is absent — the prior behavior).
2000
+ * @param hooks.onChange - Incremental rebuild for the changed set (the fast path when wired).
1907
2001
  * @returns Resolves once the rebuild attempt completes.
1908
2002
  * @example
1909
2003
  * ```ts
1910
- * await rebuild(ctx, deps, "src/app.tsx", () => web.cli.build());
2004
+ * await rebuild(ctx, deps, ["src/app.tsx"], { onChange: c => web.cli.update(c) });
1911
2005
  * ```
1912
2006
  */
1913
- const rebuild = async (ctx, deps, changedPath, webBuild) => {
2007
+ const rebuild = async (ctx, deps, changedPaths, hooks) => {
1914
2008
  ctx.emit("dev:phase", {
1915
2009
  phase: "rebuild",
1916
- detail: changedPath
2010
+ detail: describeChanges(changedPaths)
1917
2011
  });
1918
2012
  const started = deps.now();
1919
2013
  try {
1920
- const { files } = await deps.build(ctx, webBuild);
2014
+ let files;
2015
+ if (hooks.onChange) files = fileCountOf(await hooks.onChange(changedPaths));
2016
+ else files = (await deps.build(ctx, hooks.webBuild)).files;
1921
2017
  ctx.emit("dev:rebuilt", {
1922
2018
  files,
1923
2019
  ms: deps.now() - started
@@ -1927,29 +2023,58 @@ const rebuild = async (ctx, deps, changedPath, webBuild) => {
1927
2023
  }
1928
2024
  };
1929
2025
  /**
1930
- * Run a long-lived dev session: cold build (local d1 migrate) spawn `wrangler dev`
1931
- * watch + rebuild on change teardown on signal.
2026
+ * Load the configured seed into the LOCAL D1 for a `dev --seed` session: execute the SQL file, then
2027
+ * clear the configured cached KV keys so the app rebuilds them from the freshly-seeded rows. The
2028
+ * schema already exists (the migrate step above runs first), so this never migrates — the local
2029
+ * analogue of the deploy's remote seed, over the same `pluginConfigs.deploy.seed` config.
2030
+ *
2031
+ * @param ctx - The deploy plugin context.
2032
+ * @param deps - The injected dev deps (for the wrangler runner).
2033
+ * @returns Resolves once the seed file has executed and every cached KV key is cleared.
2034
+ * @throws {Error} When `--seed` is set but no seed is configured under `pluginConfigs.deploy.seed`.
2035
+ * @example
2036
+ * ```ts
2037
+ * await seedLocal(ctx, realDevDeps());
2038
+ * ```
2039
+ */
2040
+ const seedLocal = async (ctx, deps) => {
2041
+ const config = ctx.config.seed;
2042
+ if (config === void 0) throw new Error("[moku-worker] dev({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed.");
2043
+ ctx.emit("dev:phase", {
2044
+ phase: "seed",
2045
+ detail: config.file
2046
+ });
2047
+ await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
2048
+ };
2049
+ /**
2050
+ * Run a long-lived dev session: cold build → (local d1 migrate) → (local seed) → spawn `wrangler
2051
+ * dev` → watch + rebuild on change → teardown on signal.
1932
2052
  *
1933
2053
  * @param ctx - The deploy plugin context (config + emit + require/has).
1934
2054
  * @param opts - Optional options.
1935
2055
  * @param opts.port - Local dev port (default 8787).
1936
- * @param opts.webBuild - Web build hook (re)run on cold build + each change (e.g. `() => web.cli.build()`).
2056
+ * @param opts.webBuild - Cold-build hook (also the per-change rebuild when `onChange` is omitted).
2057
+ * @param opts.onChange - Incremental per-change rebuild hook (e.g. `c => web.cli.update(c)`); when
2058
+ * set, each debounced change rebuilds only the changed paths instead of a full `webBuild()`.
2059
+ * @param opts.seed - Load the configured seed into the LOCAL D1 (+ reset its KV keys) before serving.
1937
2060
  * @param deps - Injected side effects (real ones from realDevDeps in production).
1938
2061
  * @returns Resolves when the session ends (SIGINT).
1939
2062
  * @example
1940
2063
  * ```ts
1941
- * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build() }, realDevDeps());
2064
+ * await runDev(ctx, { port: 8787, seed: true, webBuild: () => web.cli.build() }, realDevDeps());
1942
2065
  * ```
1943
2066
  */
1944
2067
  const runDev = async (ctx, opts, deps) => {
1945
2068
  const port = opts?.port ?? 8787;
1946
2069
  const webBuild = opts?.webBuild;
2070
+ const onChange = opts?.onChange;
2071
+ const seed = opts?.seed === true;
1947
2072
  ctx.emit("dev:phase", {
1948
2073
  phase: "build",
1949
2074
  detail: "site"
1950
2075
  });
1951
2076
  await deps.build(ctx, webBuild);
1952
- const migrationBindings = ctx.config.migrateLocal ? d1MigrationBindings(ctx) : [];
2077
+ const migrationBindings = ctx.config.migrateLocal || seed ? d1MigrationBindings(ctx) : [];
1953
2078
  if (migrationBindings.length > 0) {
1954
2079
  ctx.emit("dev:phase", {
1955
2080
  phase: "migrate",
@@ -1963,6 +2088,7 @@ const runDev = async (ctx, opts, deps) => {
1963
2088
  "--local"
1964
2089
  ]);
1965
2090
  }
2091
+ if (seed) await seedLocal(ctx, deps);
1966
2092
  ctx.emit("dev:phase", {
1967
2093
  phase: "serve",
1968
2094
  detail: `http://localhost:${String(port)}`
@@ -1975,7 +2101,10 @@ const runDev = async (ctx, opts, deps) => {
1975
2101
  ctx.config.configFile,
1976
2102
  "--live-reload"
1977
2103
  ]);
1978
- const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPath) => rebuild(ctx, deps, changedPath, webBuild));
2104
+ const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPaths) => rebuild(ctx, deps, changedPaths, {
2105
+ webBuild,
2106
+ onChange
2107
+ }));
1979
2108
  await Promise.race([deps.untilSignal(), child.whenExited]);
1980
2109
  ctx.emit("dev:phase", { phase: "stopping" });
1981
2110
  watcher.close();
@@ -2942,17 +3071,33 @@ const HINTS = {
2942
3071
  deploy: "wrangler deploy failed — review the output above, then retry."
2943
3072
  };
2944
3073
  /**
2945
- * Emit the terminal `aborted` phase the single exit every guided gate/retry funnels through when
2946
- * the user stops the deploy. Factored out so each abort path renders one consistent line.
3074
+ * Emit the terminal `aborted` phase AND build the matching {@link DeployReport} the single exit
3075
+ * every guided gate/retry funnels through when the user stops the deploy (or auth was never set up).
3076
+ * Centralizing it keeps every abort path emitting one consistent line and returning the same shaped
3077
+ * report: `status: "aborted"`, both post-steps `"skipped"`, no errors — so a calling script sees a
3078
+ * clean stop, never a half-filled success, and the remote-DB migration/seed are guaranteed unrun.
2947
3079
  *
2948
3080
  * @param ctx - The deploy plugin context.
2949
- * @returns Nothing.
3081
+ * @param stage - The resolved deploy stage (echoed into the report).
3082
+ * @param startedAt - The run's start timestamp, for the elapsed field.
3083
+ * @returns The aborted deploy report.
2950
3084
  * @example
2951
3085
  * ```ts
2952
- * if (declined) return emitAborted(ctx);
3086
+ * if (declined) return aborted(ctx, stage, startedAt);
2953
3087
  * ```
2954
3088
  */
2955
- const emitAborted = (ctx) => ctx.emit("deploy:phase", { phase: "aborted" });
3089
+ const aborted = (ctx, stage, startedAt) => {
3090
+ ctx.emit("deploy:phase", { phase: "aborted" });
3091
+ return {
3092
+ ok: false,
3093
+ status: "aborted",
3094
+ stage,
3095
+ migration: "skipped",
3096
+ seed: "skipped",
3097
+ elapsedMs: Date.now() - startedAt,
3098
+ errors: []
3099
+ };
3100
+ };
2956
3101
  /**
2957
3102
  * The full guided token setup shown after an auth failure on a TTY. Offers to walk the user through
2958
3103
  * it, and when accepted: prints WHERE to create the Cloudflare token (dashboard URL, which template,
@@ -3172,6 +3317,104 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3172
3317
  return url;
3173
3318
  };
3174
3319
  /**
3320
+ * Apply pending D1 migrations to the REMOTE database for every configured d1 instance that ships a
3321
+ * migrations dir — the generic, deploy-owned analogue of `wrangler d1 migrations apply <binding>
3322
+ * --remote`. The wrangler config was written earlier in the pipeline, so each binding resolves. The
3323
+ * caller runs this only AFTER a successful deploy, so a deploy that never happened never migrates a
3324
+ * remote DB. Streams wrangler's output; throws on the first non-zero exit (the caller folds it into
3325
+ * the report).
3326
+ *
3327
+ * @param ctx - The deploy plugin context.
3328
+ * @returns Resolves once every configured database's remote migrations have been applied.
3329
+ * @example
3330
+ * ```ts
3331
+ * await applyRemoteMigrations(ctx);
3332
+ * ```
3333
+ */
3334
+ const applyRemoteMigrations = async (ctx) => {
3335
+ if (!ctx.has("d1")) return;
3336
+ for (const database of ctx.require(d1Plugin).deployManifest()) if (database.migrations !== void 0) await runWranglerInherit([
3337
+ "d1",
3338
+ "migrations",
3339
+ "apply",
3340
+ database.binding,
3341
+ "--remote"
3342
+ ]);
3343
+ };
3344
+ /**
3345
+ * Render a post-deploy step's failure as a branded line and capture its message into `errors` —
3346
+ * folding the failure into the report instead of throwing, so a deploy that already went live still
3347
+ * yields a complete, honest report when a later remote step (migration/seed) fails.
3348
+ *
3349
+ * @param ui - The branded console to render the error through.
3350
+ * @param errors - The accumulator the captured message is pushed into.
3351
+ * @param error - The thrown error (or value) to brand and capture.
3352
+ * @returns The captured (branded) message.
3353
+ * @example
3354
+ * ```ts
3355
+ * captureFailure(ui, errors, new Error("[moku-worker] seed failed"));
3356
+ * ```
3357
+ */
3358
+ const captureFailure = (ui, errors, error) => {
3359
+ const message = error instanceof Error ? error.message : String(error);
3360
+ ui.error(message);
3361
+ errors.push(message);
3362
+ return message;
3363
+ };
3364
+ /**
3365
+ * Run the post-deploy remote steps — REACHED ONLY ON A SUCCESSFUL DEPLOY (every gate in `run` returns
3366
+ * early before here), so a deploy that never happened never touches a remote DB. Applies remote D1
3367
+ * migrations (when requested), then loads the configured seed (when requested) — but skips the seed
3368
+ * if the migration it depends on failed. Each step's failure is RENDERED inline and CAPTURED in
3369
+ * `errors` (never thrown), so one failed step still yields a complete, honest report.
3370
+ *
3371
+ * @param ctx - The deploy plugin context.
3372
+ * @param want - Which post-steps the caller requested.
3373
+ * @param want.migration - Whether to apply pending remote D1 migrations.
3374
+ * @param want.seed - Whether to load the configured remote seed (and reset its KV keys).
3375
+ * @returns The migration + seed outcomes and any captured branded errors.
3376
+ * @example
3377
+ * ```ts
3378
+ * const post = await runPostDeploy(ctx, { migration: true, seed: true });
3379
+ * ```
3380
+ */
3381
+ const runPostDeploy = async (ctx, want) => {
3382
+ const ui = (0, _moku_labs_common_cli.createBrandConsole)();
3383
+ const errors = [];
3384
+ let migration = "skipped";
3385
+ if (want.migration) try {
3386
+ await applyRemoteMigrations(ctx);
3387
+ migration = "applied";
3388
+ ui.check(true, "migrated", "remote D1");
3389
+ } catch (error) {
3390
+ migration = "failed";
3391
+ captureFailure(ui, errors, error);
3392
+ }
3393
+ let seed = "skipped";
3394
+ if (want.seed && migration === "failed") {
3395
+ seed = "failed";
3396
+ captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] seed skipped — the remote migration it depends on failed."));
3397
+ } else if (want.seed) {
3398
+ const config = ctx.config.seed;
3399
+ if (config === void 0) {
3400
+ seed = "failed";
3401
+ captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] deploy({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed."));
3402
+ } else try {
3403
+ await runConfiguredSeed(ctx, runWranglerInherit, config, "--remote");
3404
+ seed = "applied";
3405
+ ui.check(true, "seeded", config.file);
3406
+ } catch (error) {
3407
+ seed = "failed";
3408
+ captureFailure(ui, errors, error);
3409
+ }
3410
+ }
3411
+ return {
3412
+ migration,
3413
+ seed,
3414
+ errors
3415
+ };
3416
+ };
3417
+ /**
3175
3418
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3176
3419
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3177
3420
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3187,13 +3430,19 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3187
3430
  const createDeployApi = (ctx) => ({
3188
3431
  /**
3189
3432
  * Run the full deploy pipeline: detect → preflight (check-before-create) → provision (only the
3190
- * missing) → wrangler-config (with real ids) → upload → deploy. When opts.manifest is supplied
3191
- * it is used verbatim (universal path).
3433
+ * missing) → wrangler-config (with real ids) → upload → deploy, then ONLY on a successful
3434
+ * deploy the requested post-deploy remote steps (migration, seed). When opts.manifest is
3435
+ * supplied it is used verbatim (universal path).
3192
3436
  *
3193
3437
  * On a TTY the run is GUIDED end to end: each gate is confirmed, and every failure is recovered
3194
- * interactively rather than thrown — a missing/invalid token offers `auth setup`, and the build,
3195
- * infra, upload, and `wrangler deploy` steps offer a retry. In CI/pipes it fails fast (no prompt,
3196
- * the first error propagates to the branded CLI handler).
3438
+ * interactively rather than thrown — a missing/invalid token offers a `.env.local` scaffold, and
3439
+ * the build, infra, upload, and `wrangler deploy` steps offer a retry. In CI/pipes it fails fast
3440
+ * (no prompt, the first error propagates to the branded CLI handler).
3441
+ *
3442
+ * Resolves to a {@link DeployReport}. Every abort path (a declined gate, or auth never set up)
3443
+ * returns `status: "aborted"` BEFORE the post-deploy steps, so `migration`/`seed` run only when
3444
+ * the worker actually went live — a first `deploy --seed` with no token aborts cleanly instead of
3445
+ * falling through to a raw `wrangler … --remote` auth error.
3197
3446
  *
3198
3447
  * @param opts - Optional run options.
3199
3448
  * @param opts.ci - CI/automated mode: never prompts, auto-confirms every gate, fails fast. When
@@ -3202,11 +3451,15 @@ const createDeployApi = (ctx) => ({
3202
3451
  * @param opts.stage - Target stage; suffixes resource names (`production` = bare). Falls back to the app stage.
3203
3452
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
3204
3453
  * @param opts.manifest - Caller-supplied manifest (bypasses deployManifest() assembly).
3205
- * @returns Resolves once the deploy completes.
3454
+ * @param opts.migration - After a successful deploy, apply pending remote D1 migrations for every
3455
+ * configured d1 instance that ships migrations. Skipped (not attempted) on an aborted deploy.
3456
+ * @param opts.seed - After a successful deploy (+ migration), load the seed configured under
3457
+ * `pluginConfigs.deploy.seed` into the remote D1 and reset its cached KV keys. Skipped on abort.
3458
+ * @returns The deploy report (status, url, resource tally, migration/seed outcome, errors).
3206
3459
  * @example
3207
3460
  * ```ts
3208
- * await api.run({ webBuild: () => web.cli.build() }); // guided on a TTY
3209
- * await api.run({ ci: true, manifest: { name: "w", compatibilityDate: "2026-06-17", resources: [] } });
3461
+ * const report = await api.run({ webBuild: () => web.cli.build(), migration: true, seed: true });
3462
+ * if (!report.ok) process.exitCode = 1; // aborted or a post-step failed
3210
3463
  * ```
3211
3464
  */
3212
3465
  async run(opts) {
@@ -3219,26 +3472,44 @@ const createDeployApi = (ctx) => ({
3219
3472
  };
3220
3473
  const startedAt = Date.now();
3221
3474
  ctx.emit("deploy:phase", { phase: "auth" });
3222
- if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
3223
- if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return emitAborted(ctx);
3475
+ if (!await guidedAuth(ctx, deps)) return aborted(ctx, stage, startedAt);
3476
+ if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return aborted(ctx, stage, startedAt);
3224
3477
  ctx.emit("deploy:phase", { phase: "detect" });
3225
3478
  const manifest = opts?.manifest ?? assembleManifest(ctx, stage);
3226
3479
  ctx.emit("deploy:phase", { phase: "provision" });
3227
3480
  const provisioned = await guidedProvision(ctx, manifest, ci, deps);
3228
- if (provisioned === ABORTED) return emitAborted(ctx);
3481
+ if (provisioned === ABORTED) return aborted(ctx, stage, startedAt);
3229
3482
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3230
3483
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3231
- if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3484
+ if (!await guidedUpload(ctx, manifest, deps)) return aborted(ctx, stage, startedAt);
3232
3485
  const url = await guidedDeployStep(ctx, manifest, stage, deps);
3233
- if (url === void 0) return emitAborted(ctx);
3486
+ if (url === void 0) return aborted(ctx, stage, startedAt);
3487
+ const resources = {
3488
+ created: provisioned.created.length,
3489
+ exists: provisioned.skipped.length,
3490
+ failed: provisioned.failed.length
3491
+ };
3234
3492
  renderDeploySummary((0, _moku_labs_common_cli.createBrandConsole)(), {
3235
3493
  url,
3236
3494
  stage,
3237
- created: provisioned.created.length,
3238
- exists: provisioned.skipped.length,
3239
- failed: provisioned.failed.length,
3495
+ ...resources,
3240
3496
  elapsedMs: Date.now() - startedAt
3241
3497
  });
3498
+ const post = await runPostDeploy(ctx, {
3499
+ migration: opts?.migration === true,
3500
+ seed: opts?.seed === true
3501
+ });
3502
+ return {
3503
+ ok: post.errors.length === 0,
3504
+ status: post.errors.length === 0 ? "deployed" : "failed",
3505
+ stage,
3506
+ url,
3507
+ resources,
3508
+ migration: post.migration,
3509
+ seed: post.seed,
3510
+ elapsedMs: Date.now() - startedAt,
3511
+ errors: post.errors
3512
+ };
3242
3513
  },
3243
3514
  /**
3244
3515
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3248,11 +3519,15 @@ const createDeployApi = (ctx) => ({
3248
3519
  * @param opts - Optional options.
3249
3520
  * @param opts.port - Local dev port (default 8787).
3250
3521
  * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3251
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3522
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3523
+ * per-change rebuild when `onChange` is omitted.
3524
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`).
3525
+ * @param opts.seed - Load the configured seed (`pluginConfigs.deploy.seed`) into the LOCAL D1 and
3526
+ * reset its cached KV keys before serving — the local analogue of `deploy({ seed: true })`.
3252
3527
  * @returns Resolves when the dev session ends.
3253
3528
  * @example
3254
3529
  * ```ts
3255
- * await api.dev({ port: 8787, webBuild: () => web.cli.build() });
3530
+ * await api.dev({ port: 8787, seed: true, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3256
3531
  * ```
3257
3532
  */
3258
3533
  async dev(opts) {
@@ -3282,11 +3557,7 @@ const createDeployApi = (ctx) => ({
3282
3557
  if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
3283
3558
  const stage = opts?.stage ?? ctx.global.stage;
3284
3559
  await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
3285
- const databases = ctx.require(d1Plugin).deployManifest();
3286
- const wanted = opts?.binding;
3287
- const matched = wanted === void 0 ? databases : databases.filter((database) => database.binding === wanted);
3288
- const target = matched.length === 1 ? matched[0] : void 0;
3289
- 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}".`);
3560
+ const target = resolveD1(ctx, opts?.binding);
3290
3561
  const scope = opts?.remote === true ? "--remote" : "--local";
3291
3562
  if (scope === "--local" && target.migrations !== void 0) await runWranglerInherit([
3292
3563
  "d1",
@@ -3505,11 +3776,16 @@ const createCliApi = (ctx) => ({
3505
3776
  * @param opts - Optional local dev options.
3506
3777
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3507
3778
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3508
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3779
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3780
+ * per-change rebuild when `onChange` is omitted.
3781
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
3782
+ * so each change rebuilds only the changed paths instead of a full `webBuild()`.
3783
+ * @param opts.seed - Load the configured seed (`pluginConfigs.deploy.seed`) into the LOCAL D1 and
3784
+ * reset its cached KV keys before serving — the local analogue of `deploy({ seed: true })`.
3509
3785
  * @returns Resolves when the dev session ends.
3510
3786
  * @example
3511
3787
  * ```ts
3512
- * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3788
+ * await api.dev({ port: 7878, seed: true, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3513
3789
  * ```
3514
3790
  */
3515
3791
  async dev(opts) {
@@ -3523,7 +3799,9 @@ const createCliApi = (ctx) => ({
3523
3799
  await ctx.require(deployPlugin).dev({
3524
3800
  ...opts?.port === void 0 ? {} : { port: opts.port },
3525
3801
  ...stage === void 0 ? {} : { stage },
3526
- ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3802
+ ...opts?.webBuild ? { webBuild: opts.webBuild } : {},
3803
+ ...opts?.onChange ? { onChange: opts.onChange } : {},
3804
+ ...opts?.seed ? { seed: opts.seed } : {}
3527
3805
  });
3528
3806
  ui.check(true, "dev session stopped cleanly");
3529
3807
  } catch (error) {
@@ -3532,32 +3810,48 @@ const createCliApi = (ctx) => ({
3532
3810
  }
3533
3811
  },
3534
3812
  /**
3535
- * One-command Cloudflare deploy; forwards opts verbatim to deploy.run. Guided/interactive by
3536
- * default; `{ ci: true }` runs the automated path (CI). A `webBuild` hook builds the web site
3537
- * first (before `wrangler deploy`). A failure renders a branded `✗` line + non-zero exit code
3538
- * (matching cli.auth/doctor), never a raw stack trace.
3813
+ * One-command Cloudflare deploy; forwards opts verbatim to deploy.run, then — only on a successful
3814
+ * deploy the requested post-deploy migration/seed. Guided/interactive by default; `{ ci: true }`
3815
+ * runs the automated path (CI). A `webBuild` hook builds the web site first (before `wrangler
3816
+ * deploy`). RETURNS the structured {@link DeployReport}; on a failure it also renders a branded `✗`
3817
+ * line + sets a non-zero exit code (matching cli.auth/doctor), never a raw stack trace.
3539
3818
  *
3540
3819
  * @param opts - Optional deploy options.
3541
3820
  * @param opts.ci - Automated mode: never prompts, auto-confirms. Omit/false → guided on a TTY.
3542
3821
  * @param opts.stage - Target stage (resource-name suffix); falls back to `--stage` then the app stage.
3543
3822
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
3544
- * @returns Resolves once the deploy completes (or after a failure is rendered).
3823
+ * @param opts.migration - Apply pending remote D1 migrations after a successful deploy (skipped on abort).
3824
+ * @param opts.seed - Load the configured remote seed (`pluginConfigs.deploy.seed`) after a
3825
+ * successful deploy (+ migration); skipped on an aborted deploy.
3826
+ * @returns The deploy report (status, url, resource tally, migration/seed outcome, errors).
3545
3827
  * @example
3546
3828
  * ```ts
3547
- * await api.deploy({ webBuild: () => web.cli.build() }); // guided, app stage
3548
- * await api.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI; `--stage dev` honored
3829
+ * const report = await api.deploy({ webBuild: () => web.cli.build(), migration: true, seed: true });
3830
+ * if (report.status === "aborted") return; // creds not set up yet nothing shipped
3549
3831
  * ```
3550
3832
  */
3551
3833
  async deploy(opts) {
3552
3834
  const stage = opts?.stage ?? parseStageArg(process.argv);
3553
3835
  try {
3554
- await ctx.require(deployPlugin).run({
3836
+ const report = await ctx.require(deployPlugin).run({
3555
3837
  ...opts,
3556
3838
  ...stage === void 0 ? {} : { stage }
3557
3839
  });
3840
+ if (report.status === "failed") process.exitCode = 1;
3841
+ return report;
3558
3842
  } catch (error) {
3559
- (0, _moku_labs_common_cli.createBrandConsole)().error(error instanceof Error ? error.message : String(error));
3843
+ const message = error instanceof Error ? error.message : String(error);
3844
+ (0, _moku_labs_common_cli.createBrandConsole)().error(message);
3560
3845
  process.exitCode = 1;
3846
+ return {
3847
+ ok: false,
3848
+ status: "failed",
3849
+ stage: stage ?? "production",
3850
+ migration: "skipped",
3851
+ seed: "skipped",
3852
+ elapsedMs: 0,
3853
+ errors: [message]
3854
+ };
3561
3855
  }
3562
3856
  },
3563
3857
  /**