@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.
@@ -1608,6 +1608,76 @@ const runWranglerInherit = (args) => {
1608
1608
  });
1609
1609
  };
1610
1610
  //#endregion
1611
+ //#region src/plugins/deploy/seed.ts
1612
+ /**
1613
+ * @file deploy plugin — shared D1 seed helpers (resolve the target db, run a configured seed).
1614
+ *
1615
+ * Pure orchestration over an INJECTED wrangler runner, so the post-deploy REMOTE seed (api.ts) and
1616
+ * the dev-session LOCAL seed (dev/runner.ts) stay in lockstep — same file, same KV-reset semantics,
1617
+ * differing only in the `--remote` / `--local` scope. Migrations are NOT applied here: each caller
1618
+ * applies the schema first (the deploy's migration step / dev's local-migrate step), then seeds.
1619
+ * Node-only; never imported by the runtime Worker bundle.
1620
+ */
1621
+ /**
1622
+ * Resolve the single configured d1 database (or the one bound to `binding` when several exist) from
1623
+ * the d1 plugin's manifest. The shared resolver behind `seed()`, the post-deploy seed, and the dev
1624
+ * seed; throws a branded error when the choice is ambiguous (none/several, no binding) or unknown.
1625
+ *
1626
+ * @param ctx - The deploy plugin context.
1627
+ * @param binding - The d1 binding to target when more than one is configured; the sole one otherwise.
1628
+ * @returns The resolved d1 resource descriptor (its binding + optional migrations dir).
1629
+ * @throws {Error} When no single database resolves (none/several without a binding, or unknown binding).
1630
+ * @example
1631
+ * ```ts
1632
+ * const db = resolveD1(ctx, "DB");
1633
+ * ```
1634
+ */
1635
+ const resolveD1 = (ctx, binding) => {
1636
+ const databases = ctx.require(d1Plugin).deployManifest();
1637
+ const matched = binding === void 0 ? databases : databases.filter((db) => db.binding === binding);
1638
+ const target = matched.length === 1 ? matched[0] : void 0;
1639
+ 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}".`);
1640
+ return target;
1641
+ };
1642
+ /**
1643
+ * Run a configured seed against one scope: execute the seed SQL against the d1 database, then delete
1644
+ * each configured cached KV key so the next read rebuilds it from the freshly-seeded rows. The
1645
+ * schema is assumed to exist (the caller applies migrations first), so this never migrates. The
1646
+ * wrangler runner is injected so the same orchestration serves the streamed deploy path and the
1647
+ * injectable dev path.
1648
+ *
1649
+ * @param ctx - The deploy plugin context.
1650
+ * @param run - The wrangler runner to execute each command through.
1651
+ * @param seed - The resolved seed config (SQL file, optional binding, KV keys to reset).
1652
+ * @param scope - The wrangler scope: `--remote` (deploy) or `--local` (dev).
1653
+ * @returns Resolves once the seed file has executed and every cached KV key is cleared.
1654
+ * @throws {Error} When no d1 database is configured, or the seed's binding cannot be resolved.
1655
+ * @example
1656
+ * ```ts
1657
+ * await runConfiguredSeed(ctx, runWranglerInherit, ctx.config.seed, "--remote");
1658
+ * ```
1659
+ */
1660
+ const runConfiguredSeed = async (ctx, run, seed, scope) => {
1661
+ if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
1662
+ await run([
1663
+ "d1",
1664
+ "execute",
1665
+ resolveD1(ctx, seed.binding).binding,
1666
+ scope,
1667
+ "--file",
1668
+ seed.file
1669
+ ]);
1670
+ for (const entry of seed.resetKv ?? []) await run([
1671
+ "kv",
1672
+ "key",
1673
+ "delete",
1674
+ entry.key,
1675
+ "--binding",
1676
+ entry.binding,
1677
+ scope
1678
+ ]);
1679
+ };
1680
+ //#endregion
1611
1681
  //#region src/plugins/deploy/dev/build.ts
1612
1682
  /**
1613
1683
  * @file deploy plugin — dev site-rebuild resolution.
@@ -1701,7 +1771,8 @@ const buildSite = async (ctx, webBuild) => {
1701
1771
  * @file deploy plugin — debounced filesystem watcher for dev.
1702
1772
  *
1703
1773
  * Watches the top-level directories implied by the config globs (recursive) and fires a debounced
1704
- * change callback with the last changed path. Uses node:fs.watch no extra dependency.
1774
+ * change callback with the SET of paths changed in the window (so a burst of edits coalesces into
1775
+ * one rebuild that knows every changed file). Uses node:fs.watch — no extra dependency.
1705
1776
  * Node-only; never imported by the runtime Worker bundle.
1706
1777
  */
1707
1778
  /**
@@ -1724,27 +1795,29 @@ const watchDirectories = (globs) => {
1724
1795
  return [...directories];
1725
1796
  };
1726
1797
  /**
1727
- * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with
1728
- * the last changed path. Missing directories are skipped silently.
1798
+ * Watch the directories implied by `globs` and fire `onChange` (debounced by `debounceMs`) with the
1799
+ * distinct set of paths changed within the window. Missing directories are skipped silently.
1729
1800
  *
1730
1801
  * @param globs - Watch globs.
1731
1802
  * @param debounceMs - Coalesce rapid changes into one callback within this window.
1732
- * @param onChange - Called with the last changed path after the debounce settles.
1803
+ * @param onChange - Called with the changed paths (snapshot of the window) after the debounce settles.
1733
1804
  * @returns A handle whose close() stops all watchers and cancels any pending callback.
1734
1805
  * @example
1735
1806
  * ```ts
1736
- * const handle = watchPaths(["src/**\/*.ts"], 120, p => rebuild(p));
1807
+ * const handle = watchPaths(["src/**\/*.ts"], 120, paths => rebuild(paths));
1737
1808
  * handle.close();
1738
1809
  * ```
1739
1810
  */
1740
1811
  const watchPaths = (globs, debounceMs, onChange) => {
1741
1812
  let timer;
1742
- let lastPath = "";
1813
+ const changed = /* @__PURE__ */ new Set();
1743
1814
  const fire = (changedPath) => {
1744
- lastPath = changedPath;
1815
+ changed.add(changedPath);
1745
1816
  if (timer !== void 0) clearTimeout(timer);
1746
1817
  timer = setTimeout(() => {
1747
- onChange(lastPath);
1818
+ const batch = [...changed];
1819
+ changed.clear();
1820
+ onChange(batch);
1748
1821
  }, debounceMs);
1749
1822
  };
1750
1823
  const watchers = [];
@@ -1874,27 +1947,50 @@ const realDevDeps = () => ({
1874
1947
  */
1875
1948
  const d1MigrationBindings = (ctx) => ctx.has("d1") ? ctx.require(d1Plugin).deployManifest().filter((manifest) => manifest.migrations !== void 0).map((manifest) => manifest.binding) : [];
1876
1949
  /**
1877
- * Rebuild the site once and announce the result. A failed build keeps the session alive (it just
1878
- * emits dev:error and serves the last good build).
1950
+ * One-line description of a changed-path batch for the `dev:phase rebuild` detail: the single path,
1951
+ * or the first path plus a `(+N more)` tail. Empty batches (defensive) read as "site".
1952
+ *
1953
+ * @param paths - The changed paths the watcher coalesced for this rebuild.
1954
+ * @returns The detail string for the rebuild phase event.
1955
+ * @example
1956
+ * ```ts
1957
+ * describeChanges(["src/a.ts", "src/b.css"]); // "src/a.ts (+1 more)"
1958
+ * ```
1959
+ */
1960
+ const describeChanges = (paths) => {
1961
+ const [first, ...rest] = paths;
1962
+ if (first === void 0) return "site";
1963
+ return rest.length === 0 ? first : `${first} (+${String(rest.length)} more)`;
1964
+ };
1965
+ /**
1966
+ * Rebuild the site once for a changed-path batch and announce the result. The FAST path is the
1967
+ * incremental `onChange(changedPaths)` hook (e.g. `web.cli.update`) when wired; otherwise it falls
1968
+ * back to a full `webBuild()` rebuild (via deps.build) — the prior behavior. A failed rebuild keeps
1969
+ * the session alive (it just emits dev:error and serves the last good build). Both paths share one
1970
+ * `dev:phase rebuild` → `dev:rebuilt`/`dev:error` envelope so the branded dev TUI is identical.
1879
1971
  *
1880
1972
  * @param ctx - The deploy plugin context.
1881
1973
  * @param deps - The injected dev deps.
1882
- * @param changedPath - The path that triggered the rebuild.
1883
- * @param webBuild - Optional call-time web build hook threaded into the rebuild.
1974
+ * @param changedPaths - The paths that triggered the rebuild (the watcher's debounced set).
1975
+ * @param hooks - The consumer rebuild hooks.
1976
+ * @param hooks.webBuild - Full rebuild (used when `onChange` is absent — the prior behavior).
1977
+ * @param hooks.onChange - Incremental rebuild for the changed set (the fast path when wired).
1884
1978
  * @returns Resolves once the rebuild attempt completes.
1885
1979
  * @example
1886
1980
  * ```ts
1887
- * await rebuild(ctx, deps, "src/app.tsx", () => web.cli.build());
1981
+ * await rebuild(ctx, deps, ["src/app.tsx"], { onChange: c => web.cli.update(c) });
1888
1982
  * ```
1889
1983
  */
1890
- const rebuild = async (ctx, deps, changedPath, webBuild) => {
1984
+ const rebuild = async (ctx, deps, changedPaths, hooks) => {
1891
1985
  ctx.emit("dev:phase", {
1892
1986
  phase: "rebuild",
1893
- detail: changedPath
1987
+ detail: describeChanges(changedPaths)
1894
1988
  });
1895
1989
  const started = deps.now();
1896
1990
  try {
1897
- const { files } = await deps.build(ctx, webBuild);
1991
+ let files;
1992
+ if (hooks.onChange) files = fileCountOf(await hooks.onChange(changedPaths));
1993
+ else files = (await deps.build(ctx, hooks.webBuild)).files;
1898
1994
  ctx.emit("dev:rebuilt", {
1899
1995
  files,
1900
1996
  ms: deps.now() - started
@@ -1904,29 +2000,58 @@ const rebuild = async (ctx, deps, changedPath, webBuild) => {
1904
2000
  }
1905
2001
  };
1906
2002
  /**
1907
- * Run a long-lived dev session: cold build (local d1 migrate) spawn `wrangler dev`
1908
- * watch + rebuild on change teardown on signal.
2003
+ * Load the configured seed into the LOCAL D1 for a `dev --seed` session: execute the SQL file, then
2004
+ * clear the configured cached KV keys so the app rebuilds them from the freshly-seeded rows. The
2005
+ * schema already exists (the migrate step above runs first), so this never migrates — the local
2006
+ * analogue of the deploy's remote seed, over the same `pluginConfigs.deploy.seed` config.
2007
+ *
2008
+ * @param ctx - The deploy plugin context.
2009
+ * @param deps - The injected dev deps (for the wrangler runner).
2010
+ * @returns Resolves once the seed file has executed and every cached KV key is cleared.
2011
+ * @throws {Error} When `--seed` is set but no seed is configured under `pluginConfigs.deploy.seed`.
2012
+ * @example
2013
+ * ```ts
2014
+ * await seedLocal(ctx, realDevDeps());
2015
+ * ```
2016
+ */
2017
+ const seedLocal = async (ctx, deps) => {
2018
+ const config = ctx.config.seed;
2019
+ if (config === void 0) throw new Error("[moku-worker] dev({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed.");
2020
+ ctx.emit("dev:phase", {
2021
+ phase: "seed",
2022
+ detail: config.file
2023
+ });
2024
+ await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
2025
+ };
2026
+ /**
2027
+ * Run a long-lived dev session: cold build → (local d1 migrate) → (local seed) → spawn `wrangler
2028
+ * dev` → watch + rebuild on change → teardown on signal.
1909
2029
  *
1910
2030
  * @param ctx - The deploy plugin context (config + emit + require/has).
1911
2031
  * @param opts - Optional options.
1912
2032
  * @param opts.port - Local dev port (default 8787).
1913
- * @param opts.webBuild - Web build hook (re)run on cold build + each change (e.g. `() => web.cli.build()`).
2033
+ * @param opts.webBuild - Cold-build hook (also the per-change rebuild when `onChange` is omitted).
2034
+ * @param opts.onChange - Incremental per-change rebuild hook (e.g. `c => web.cli.update(c)`); when
2035
+ * set, each debounced change rebuilds only the changed paths instead of a full `webBuild()`.
2036
+ * @param opts.seed - Load the configured seed into the LOCAL D1 (+ reset its KV keys) before serving.
1914
2037
  * @param deps - Injected side effects (real ones from realDevDeps in production).
1915
2038
  * @returns Resolves when the session ends (SIGINT).
1916
2039
  * @example
1917
2040
  * ```ts
1918
- * await runDev(ctx, { port: 8787, webBuild: () => web.cli.build() }, realDevDeps());
2041
+ * await runDev(ctx, { port: 8787, seed: true, webBuild: () => web.cli.build() }, realDevDeps());
1919
2042
  * ```
1920
2043
  */
1921
2044
  const runDev = async (ctx, opts, deps) => {
1922
2045
  const port = opts?.port ?? 8787;
1923
2046
  const webBuild = opts?.webBuild;
2047
+ const onChange = opts?.onChange;
2048
+ const seed = opts?.seed === true;
1924
2049
  ctx.emit("dev:phase", {
1925
2050
  phase: "build",
1926
2051
  detail: "site"
1927
2052
  });
1928
2053
  await deps.build(ctx, webBuild);
1929
- const migrationBindings = ctx.config.migrateLocal ? d1MigrationBindings(ctx) : [];
2054
+ const migrationBindings = ctx.config.migrateLocal || seed ? d1MigrationBindings(ctx) : [];
1930
2055
  if (migrationBindings.length > 0) {
1931
2056
  ctx.emit("dev:phase", {
1932
2057
  phase: "migrate",
@@ -1940,6 +2065,7 @@ const runDev = async (ctx, opts, deps) => {
1940
2065
  "--local"
1941
2066
  ]);
1942
2067
  }
2068
+ if (seed) await seedLocal(ctx, deps);
1943
2069
  ctx.emit("dev:phase", {
1944
2070
  phase: "serve",
1945
2071
  detail: `http://localhost:${String(port)}`
@@ -1952,7 +2078,10 @@ const runDev = async (ctx, opts, deps) => {
1952
2078
  ctx.config.configFile,
1953
2079
  "--live-reload"
1954
2080
  ]);
1955
- const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPath) => rebuild(ctx, deps, changedPath, webBuild));
2081
+ const watcher = deps.watch(ctx.config.watch, ctx.config.debounceMs, (changedPaths) => rebuild(ctx, deps, changedPaths, {
2082
+ webBuild,
2083
+ onChange
2084
+ }));
1956
2085
  await Promise.race([deps.untilSignal(), child.whenExited]);
1957
2086
  ctx.emit("dev:phase", { phase: "stopping" });
1958
2087
  watcher.close();
@@ -2919,17 +3048,33 @@ const HINTS = {
2919
3048
  deploy: "wrangler deploy failed — review the output above, then retry."
2920
3049
  };
2921
3050
  /**
2922
- * Emit the terminal `aborted` phase the single exit every guided gate/retry funnels through when
2923
- * the user stops the deploy. Factored out so each abort path renders one consistent line.
3051
+ * Emit the terminal `aborted` phase AND build the matching {@link DeployReport} the single exit
3052
+ * every guided gate/retry funnels through when the user stops the deploy (or auth was never set up).
3053
+ * Centralizing it keeps every abort path emitting one consistent line and returning the same shaped
3054
+ * report: `status: "aborted"`, both post-steps `"skipped"`, no errors — so a calling script sees a
3055
+ * clean stop, never a half-filled success, and the remote-DB migration/seed are guaranteed unrun.
2924
3056
  *
2925
3057
  * @param ctx - The deploy plugin context.
2926
- * @returns Nothing.
3058
+ * @param stage - The resolved deploy stage (echoed into the report).
3059
+ * @param startedAt - The run's start timestamp, for the elapsed field.
3060
+ * @returns The aborted deploy report.
2927
3061
  * @example
2928
3062
  * ```ts
2929
- * if (declined) return emitAborted(ctx);
3063
+ * if (declined) return aborted(ctx, stage, startedAt);
2930
3064
  * ```
2931
3065
  */
2932
- const emitAborted = (ctx) => ctx.emit("deploy:phase", { phase: "aborted" });
3066
+ const aborted = (ctx, stage, startedAt) => {
3067
+ ctx.emit("deploy:phase", { phase: "aborted" });
3068
+ return {
3069
+ ok: false,
3070
+ status: "aborted",
3071
+ stage,
3072
+ migration: "skipped",
3073
+ seed: "skipped",
3074
+ elapsedMs: Date.now() - startedAt,
3075
+ errors: []
3076
+ };
3077
+ };
2933
3078
  /**
2934
3079
  * The full guided token setup shown after an auth failure on a TTY. Offers to walk the user through
2935
3080
  * it, and when accepted: prints WHERE to create the Cloudflare token (dashboard URL, which template,
@@ -3149,6 +3294,104 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3149
3294
  return url;
3150
3295
  };
3151
3296
  /**
3297
+ * Apply pending D1 migrations to the REMOTE database for every configured d1 instance that ships a
3298
+ * migrations dir — the generic, deploy-owned analogue of `wrangler d1 migrations apply <binding>
3299
+ * --remote`. The wrangler config was written earlier in the pipeline, so each binding resolves. The
3300
+ * caller runs this only AFTER a successful deploy, so a deploy that never happened never migrates a
3301
+ * remote DB. Streams wrangler's output; throws on the first non-zero exit (the caller folds it into
3302
+ * the report).
3303
+ *
3304
+ * @param ctx - The deploy plugin context.
3305
+ * @returns Resolves once every configured database's remote migrations have been applied.
3306
+ * @example
3307
+ * ```ts
3308
+ * await applyRemoteMigrations(ctx);
3309
+ * ```
3310
+ */
3311
+ const applyRemoteMigrations = async (ctx) => {
3312
+ if (!ctx.has("d1")) return;
3313
+ for (const database of ctx.require(d1Plugin).deployManifest()) if (database.migrations !== void 0) await runWranglerInherit([
3314
+ "d1",
3315
+ "migrations",
3316
+ "apply",
3317
+ database.binding,
3318
+ "--remote"
3319
+ ]);
3320
+ };
3321
+ /**
3322
+ * Render a post-deploy step's failure as a branded line and capture its message into `errors` —
3323
+ * folding the failure into the report instead of throwing, so a deploy that already went live still
3324
+ * yields a complete, honest report when a later remote step (migration/seed) fails.
3325
+ *
3326
+ * @param ui - The branded console to render the error through.
3327
+ * @param errors - The accumulator the captured message is pushed into.
3328
+ * @param error - The thrown error (or value) to brand and capture.
3329
+ * @returns The captured (branded) message.
3330
+ * @example
3331
+ * ```ts
3332
+ * captureFailure(ui, errors, new Error("[moku-worker] seed failed"));
3333
+ * ```
3334
+ */
3335
+ const captureFailure = (ui, errors, error) => {
3336
+ const message = error instanceof Error ? error.message : String(error);
3337
+ ui.error(message);
3338
+ errors.push(message);
3339
+ return message;
3340
+ };
3341
+ /**
3342
+ * Run the post-deploy remote steps — REACHED ONLY ON A SUCCESSFUL DEPLOY (every gate in `run` returns
3343
+ * early before here), so a deploy that never happened never touches a remote DB. Applies remote D1
3344
+ * migrations (when requested), then loads the configured seed (when requested) — but skips the seed
3345
+ * if the migration it depends on failed. Each step's failure is RENDERED inline and CAPTURED in
3346
+ * `errors` (never thrown), so one failed step still yields a complete, honest report.
3347
+ *
3348
+ * @param ctx - The deploy plugin context.
3349
+ * @param want - Which post-steps the caller requested.
3350
+ * @param want.migration - Whether to apply pending remote D1 migrations.
3351
+ * @param want.seed - Whether to load the configured remote seed (and reset its KV keys).
3352
+ * @returns The migration + seed outcomes and any captured branded errors.
3353
+ * @example
3354
+ * ```ts
3355
+ * const post = await runPostDeploy(ctx, { migration: true, seed: true });
3356
+ * ```
3357
+ */
3358
+ const runPostDeploy = async (ctx, want) => {
3359
+ const ui = createBrandConsole();
3360
+ const errors = [];
3361
+ let migration = "skipped";
3362
+ if (want.migration) try {
3363
+ await applyRemoteMigrations(ctx);
3364
+ migration = "applied";
3365
+ ui.check(true, "migrated", "remote D1");
3366
+ } catch (error) {
3367
+ migration = "failed";
3368
+ captureFailure(ui, errors, error);
3369
+ }
3370
+ let seed = "skipped";
3371
+ if (want.seed && migration === "failed") {
3372
+ seed = "failed";
3373
+ captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] seed skipped — the remote migration it depends on failed."));
3374
+ } else if (want.seed) {
3375
+ const config = ctx.config.seed;
3376
+ if (config === void 0) {
3377
+ seed = "failed";
3378
+ captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] deploy({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed."));
3379
+ } else try {
3380
+ await runConfiguredSeed(ctx, runWranglerInherit, config, "--remote");
3381
+ seed = "applied";
3382
+ ui.check(true, "seeded", config.file);
3383
+ } catch (error) {
3384
+ seed = "failed";
3385
+ captureFailure(ui, errors, error);
3386
+ }
3387
+ }
3388
+ return {
3389
+ migration,
3390
+ seed,
3391
+ errors
3392
+ };
3393
+ };
3394
+ /**
3152
3395
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3153
3396
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3154
3397
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3164,13 +3407,19 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3164
3407
  const createDeployApi = (ctx) => ({
3165
3408
  /**
3166
3409
  * Run the full deploy pipeline: detect → preflight (check-before-create) → provision (only the
3167
- * missing) → wrangler-config (with real ids) → upload → deploy. When opts.manifest is supplied
3168
- * it is used verbatim (universal path).
3410
+ * missing) → wrangler-config (with real ids) → upload → deploy, then ONLY on a successful
3411
+ * deploy the requested post-deploy remote steps (migration, seed). When opts.manifest is
3412
+ * supplied it is used verbatim (universal path).
3169
3413
  *
3170
3414
  * On a TTY the run is GUIDED end to end: each gate is confirmed, and every failure is recovered
3171
- * interactively rather than thrown — a missing/invalid token offers `auth setup`, and the build,
3172
- * infra, upload, and `wrangler deploy` steps offer a retry. In CI/pipes it fails fast (no prompt,
3173
- * the first error propagates to the branded CLI handler).
3415
+ * interactively rather than thrown — a missing/invalid token offers a `.env.local` scaffold, and
3416
+ * the build, infra, upload, and `wrangler deploy` steps offer a retry. In CI/pipes it fails fast
3417
+ * (no prompt, the first error propagates to the branded CLI handler).
3418
+ *
3419
+ * Resolves to a {@link DeployReport}. Every abort path (a declined gate, or auth never set up)
3420
+ * returns `status: "aborted"` BEFORE the post-deploy steps, so `migration`/`seed` run only when
3421
+ * the worker actually went live — a first `deploy --seed` with no token aborts cleanly instead of
3422
+ * falling through to a raw `wrangler … --remote` auth error.
3174
3423
  *
3175
3424
  * @param opts - Optional run options.
3176
3425
  * @param opts.ci - CI/automated mode: never prompts, auto-confirms every gate, fails fast. When
@@ -3179,11 +3428,15 @@ const createDeployApi = (ctx) => ({
3179
3428
  * @param opts.stage - Target stage; suffixes resource names (`production` = bare). Falls back to the app stage.
3180
3429
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
3181
3430
  * @param opts.manifest - Caller-supplied manifest (bypasses deployManifest() assembly).
3182
- * @returns Resolves once the deploy completes.
3431
+ * @param opts.migration - After a successful deploy, apply pending remote D1 migrations for every
3432
+ * configured d1 instance that ships migrations. Skipped (not attempted) on an aborted deploy.
3433
+ * @param opts.seed - After a successful deploy (+ migration), load the seed configured under
3434
+ * `pluginConfigs.deploy.seed` into the remote D1 and reset its cached KV keys. Skipped on abort.
3435
+ * @returns The deploy report (status, url, resource tally, migration/seed outcome, errors).
3183
3436
  * @example
3184
3437
  * ```ts
3185
- * await api.run({ webBuild: () => web.cli.build() }); // guided on a TTY
3186
- * await api.run({ ci: true, manifest: { name: "w", compatibilityDate: "2026-06-17", resources: [] } });
3438
+ * const report = await api.run({ webBuild: () => web.cli.build(), migration: true, seed: true });
3439
+ * if (!report.ok) process.exitCode = 1; // aborted or a post-step failed
3187
3440
  * ```
3188
3441
  */
3189
3442
  async run(opts) {
@@ -3196,26 +3449,44 @@ const createDeployApi = (ctx) => ({
3196
3449
  };
3197
3450
  const startedAt = Date.now();
3198
3451
  ctx.emit("deploy:phase", { phase: "auth" });
3199
- if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
3200
- if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return emitAborted(ctx);
3452
+ if (!await guidedAuth(ctx, deps)) return aborted(ctx, stage, startedAt);
3453
+ if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return aborted(ctx, stage, startedAt);
3201
3454
  ctx.emit("deploy:phase", { phase: "detect" });
3202
3455
  const manifest = opts?.manifest ?? assembleManifest(ctx, stage);
3203
3456
  ctx.emit("deploy:phase", { phase: "provision" });
3204
3457
  const provisioned = await guidedProvision(ctx, manifest, ci, deps);
3205
- if (provisioned === ABORTED) return emitAborted(ctx);
3458
+ if (provisioned === ABORTED) return aborted(ctx, stage, startedAt);
3206
3459
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3207
3460
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3208
- if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3461
+ if (!await guidedUpload(ctx, manifest, deps)) return aborted(ctx, stage, startedAt);
3209
3462
  const url = await guidedDeployStep(ctx, manifest, stage, deps);
3210
- if (url === void 0) return emitAborted(ctx);
3463
+ if (url === void 0) return aborted(ctx, stage, startedAt);
3464
+ const resources = {
3465
+ created: provisioned.created.length,
3466
+ exists: provisioned.skipped.length,
3467
+ failed: provisioned.failed.length
3468
+ };
3211
3469
  renderDeploySummary(createBrandConsole(), {
3212
3470
  url,
3213
3471
  stage,
3214
- created: provisioned.created.length,
3215
- exists: provisioned.skipped.length,
3216
- failed: provisioned.failed.length,
3472
+ ...resources,
3217
3473
  elapsedMs: Date.now() - startedAt
3218
3474
  });
3475
+ const post = await runPostDeploy(ctx, {
3476
+ migration: opts?.migration === true,
3477
+ seed: opts?.seed === true
3478
+ });
3479
+ return {
3480
+ ok: post.errors.length === 0,
3481
+ status: post.errors.length === 0 ? "deployed" : "failed",
3482
+ stage,
3483
+ url,
3484
+ resources,
3485
+ migration: post.migration,
3486
+ seed: post.seed,
3487
+ elapsedMs: Date.now() - startedAt,
3488
+ errors: post.errors
3489
+ };
3219
3490
  },
3220
3491
  /**
3221
3492
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3225,11 +3496,15 @@ const createDeployApi = (ctx) => ({
3225
3496
  * @param opts - Optional options.
3226
3497
  * @param opts.port - Local dev port (default 8787).
3227
3498
  * @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
3228
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3499
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3500
+ * per-change rebuild when `onChange` is omitted.
3501
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`).
3502
+ * @param opts.seed - Load the configured seed (`pluginConfigs.deploy.seed`) into the LOCAL D1 and
3503
+ * reset its cached KV keys before serving — the local analogue of `deploy({ seed: true })`.
3229
3504
  * @returns Resolves when the dev session ends.
3230
3505
  * @example
3231
3506
  * ```ts
3232
- * await api.dev({ port: 8787, webBuild: () => web.cli.build() });
3507
+ * await api.dev({ port: 8787, seed: true, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3233
3508
  * ```
3234
3509
  */
3235
3510
  async dev(opts) {
@@ -3259,11 +3534,7 @@ const createDeployApi = (ctx) => ({
3259
3534
  if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
3260
3535
  const stage = opts?.stage ?? ctx.global.stage;
3261
3536
  await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
3262
- const databases = ctx.require(d1Plugin).deployManifest();
3263
- const wanted = opts?.binding;
3264
- const matched = wanted === void 0 ? databases : databases.filter((database) => database.binding === wanted);
3265
- const target = matched.length === 1 ? matched[0] : void 0;
3266
- 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}".`);
3537
+ const target = resolveD1(ctx, opts?.binding);
3267
3538
  const scope = opts?.remote === true ? "--remote" : "--local";
3268
3539
  if (scope === "--local" && target.migrations !== void 0) await runWranglerInherit([
3269
3540
  "d1",
@@ -3482,11 +3753,16 @@ const createCliApi = (ctx) => ({
3482
3753
  * @param opts - Optional local dev options.
3483
3754
  * @param opts.port - Local dev port to bind. Defaults to 8787 when omitted.
3484
3755
  * @param opts.stage - Stage for the generated wrangler config; falls back to `--stage` then the app stage.
3485
- * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
3756
+ * @param opts.webBuild - Cold-build the web site (e.g. `() => webApp.cli.build()`); also the
3757
+ * per-change rebuild when `onChange` is omitted.
3758
+ * @param opts.onChange - Incremental per-change rebuild (e.g. `changes => webApp.cli.update(changes)`),
3759
+ * so each change rebuilds only the changed paths instead of a full `webBuild()`.
3760
+ * @param opts.seed - Load the configured seed (`pluginConfigs.deploy.seed`) into the LOCAL D1 and
3761
+ * reset its cached KV keys before serving — the local analogue of `deploy({ seed: true })`.
3486
3762
  * @returns Resolves when the dev session ends.
3487
3763
  * @example
3488
3764
  * ```ts
3489
- * await api.dev({ port: 7878, webBuild: () => web.cli.build() });
3765
+ * await api.dev({ port: 7878, seed: true, webBuild: () => web.cli.build(), onChange: c => web.cli.update(c) });
3490
3766
  * ```
3491
3767
  */
3492
3768
  async dev(opts) {
@@ -3500,7 +3776,9 @@ const createCliApi = (ctx) => ({
3500
3776
  await ctx.require(deployPlugin).dev({
3501
3777
  ...opts?.port === void 0 ? {} : { port: opts.port },
3502
3778
  ...stage === void 0 ? {} : { stage },
3503
- ...opts?.webBuild ? { webBuild: opts.webBuild } : {}
3779
+ ...opts?.webBuild ? { webBuild: opts.webBuild } : {},
3780
+ ...opts?.onChange ? { onChange: opts.onChange } : {},
3781
+ ...opts?.seed ? { seed: opts.seed } : {}
3504
3782
  });
3505
3783
  ui.check(true, "dev session stopped cleanly");
3506
3784
  } catch (error) {
@@ -3509,32 +3787,48 @@ const createCliApi = (ctx) => ({
3509
3787
  }
3510
3788
  },
3511
3789
  /**
3512
- * One-command Cloudflare deploy; forwards opts verbatim to deploy.run. Guided/interactive by
3513
- * default; `{ ci: true }` runs the automated path (CI). A `webBuild` hook builds the web site
3514
- * first (before `wrangler deploy`). A failure renders a branded `✗` line + non-zero exit code
3515
- * (matching cli.auth/doctor), never a raw stack trace.
3790
+ * One-command Cloudflare deploy; forwards opts verbatim to deploy.run, then — only on a successful
3791
+ * deploy the requested post-deploy migration/seed. Guided/interactive by default; `{ ci: true }`
3792
+ * runs the automated path (CI). A `webBuild` hook builds the web site first (before `wrangler
3793
+ * deploy`). RETURNS the structured {@link DeployReport}; on a failure it also renders a branded `✗`
3794
+ * line + sets a non-zero exit code (matching cli.auth/doctor), never a raw stack trace.
3516
3795
  *
3517
3796
  * @param opts - Optional deploy options.
3518
3797
  * @param opts.ci - Automated mode: never prompts, auto-confirms. Omit/false → guided on a TTY.
3519
3798
  * @param opts.stage - Target stage (resource-name suffix); falls back to `--stage` then the app stage.
3520
3799
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
3521
- * @returns Resolves once the deploy completes (or after a failure is rendered).
3800
+ * @param opts.migration - Apply pending remote D1 migrations after a successful deploy (skipped on abort).
3801
+ * @param opts.seed - Load the configured remote seed (`pluginConfigs.deploy.seed`) after a
3802
+ * successful deploy (+ migration); skipped on an aborted deploy.
3803
+ * @returns The deploy report (status, url, resource tally, migration/seed outcome, errors).
3522
3804
  * @example
3523
3805
  * ```ts
3524
- * await api.deploy({ webBuild: () => web.cli.build() }); // guided, app stage
3525
- * await api.deploy({ ci: true, webBuild: () => web.cli.build() }); // CI; `--stage dev` honored
3806
+ * const report = await api.deploy({ webBuild: () => web.cli.build(), migration: true, seed: true });
3807
+ * if (report.status === "aborted") return; // creds not set up yet nothing shipped
3526
3808
  * ```
3527
3809
  */
3528
3810
  async deploy(opts) {
3529
3811
  const stage = opts?.stage ?? parseStageArg(process.argv);
3530
3812
  try {
3531
- await ctx.require(deployPlugin).run({
3813
+ const report = await ctx.require(deployPlugin).run({
3532
3814
  ...opts,
3533
3815
  ...stage === void 0 ? {} : { stage }
3534
3816
  });
3817
+ if (report.status === "failed") process.exitCode = 1;
3818
+ return report;
3535
3819
  } catch (error) {
3536
- createBrandConsole().error(error instanceof Error ? error.message : String(error));
3820
+ const message = error instanceof Error ? error.message : String(error);
3821
+ createBrandConsole().error(message);
3537
3822
  process.exitCode = 1;
3823
+ return {
3824
+ ok: false,
3825
+ status: "failed",
3826
+ stage: stage ?? "production",
3827
+ migration: "skipped",
3828
+ seed: "skipped",
3829
+ elapsedMs: 0,
3830
+ errors: [message]
3831
+ };
3538
3832
  }
3539
3833
  },
3540
3834
  /**
package/dist/cli.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-DkoPBbJC.cjs");
2
+ const require_cli = require("./cli-CfgYgnzW.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-BuY9o1u0.cjs";
1
+ import { a as ResourceManifest, i as ExternalManifest, n as cliPlugin, t as deployPlugin } from "./index-Cb3vzZte.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-BuY9o1u0.mjs";
1
+ import { a as ResourceManifest, i as ExternalManifest, n as cliPlugin, t as deployPlugin } from "./index-Cb3vzZte.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-By06KF-9.mjs";
1
+ import { n as deployPlugin, t as cliPlugin } from "./cli-Cs3R3Jhr.mjs";
2
2
  export { cliPlugin, deployPlugin };