@moku-labs/worker 0.7.1 → 0.7.3

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.
@@ -1984,8 +1984,9 @@ const runDev = async (ctx, opts, deps) => {
1984
1984
  //#region src/plugins/deploy/infra/plan.ts
1985
1985
  /**
1986
1986
  * Decide whether a single declared resource already exists in the account, recovering its id
1987
- * (kv/d1) when it does. Durable Objects are config-only (they ship with the script), so they are
1988
- * 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.
1989
1990
  *
1990
1991
  * @param resource - The declared resource descriptor.
1991
1992
  * @param existing - The indexed set of resources already in the account.
@@ -2013,7 +2014,7 @@ const checkExisting = (resource, existing) => {
2013
2014
  }
2014
2015
  case "r2": return { exists: existing.r2.has(resource.name) };
2015
2016
  case "queue": return { exists: existing.queue.has(resource.name) };
2016
- case "do": return { exists: false };
2017
+ case "do": return { exists: true };
2017
2018
  }
2018
2019
  };
2019
2020
  /**
@@ -2207,6 +2208,57 @@ const renderProvisionResult = (ui, result) => {
2207
2208
  }
2208
2209
  }
2209
2210
  };
2211
+ /**
2212
+ * Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
2213
+ * and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
2214
+ *
2215
+ * @param ms - The elapsed milliseconds.
2216
+ * @returns The compact duration string.
2217
+ * @example
2218
+ * ```ts
2219
+ * formatDuration(4234); // "4.2s"
2220
+ * ```
2221
+ */
2222
+ const formatDuration = (ms) => {
2223
+ if (ms < 1e3) return `${String(ms)}ms`;
2224
+ const seconds = ms / 1e3;
2225
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
2226
+ const whole = Math.floor(seconds);
2227
+ return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
2228
+ };
2229
+ /**
2230
+ * Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
2231
+ * live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
2232
+ * key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
2233
+ * and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
2234
+ *
2235
+ * @param ui - The branded console to render through.
2236
+ * @param summary - The deploy summary fields.
2237
+ * @param summary.url - The live deployed URL (the panel headline).
2238
+ * @param summary.stage - The target stage the worker deployed to.
2239
+ * @param summary.created - How many resources were created this run.
2240
+ * @param summary.exists - How many resources already existed (skipped).
2241
+ * @param summary.failed - How many resources failed to provision.
2242
+ * @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
2243
+ * @example
2244
+ * ```ts
2245
+ * renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, failed: 0, elapsedMs: 4234 });
2246
+ * ```
2247
+ */
2248
+ const renderDeploySummary = (ui, summary) => {
2249
+ const { palette } = ui;
2250
+ const tally = `${String(summary.exists)} exist · ${String(summary.created)} created`;
2251
+ const failedLabel = palette.red(`${String(summary.failed)} failed`);
2252
+ const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
2253
+ ui.heading("Deployed");
2254
+ ui.box([
2255
+ palette.pink(summary.url),
2256
+ "",
2257
+ `${palette.dim("stage".padEnd(10))}${summary.stage}`,
2258
+ `${palette.dim("resources".padEnd(10))}${resources}`,
2259
+ `${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
2260
+ ]);
2261
+ };
2210
2262
  //#endregion
2211
2263
  //#region src/plugins/deploy/naming.ts
2212
2264
  /**
@@ -3086,6 +3138,34 @@ const guidedUpload = async (ctx, manifest, deps) => {
3086
3138
  return true;
3087
3139
  };
3088
3140
  /**
3141
+ * The final deploy step: confirm the target (guided only), run `wrangler deploy` with interactive
3142
+ * retry, then emit deploy:complete. Returns the deployed URL, or undefined when the target gate or a
3143
+ * deploy retry is declined (so the caller renders the summary panel only on a real success).
3144
+ *
3145
+ * @param ctx - The deploy plugin context.
3146
+ * @param manifest - The assembled (or caller-supplied) deploy manifest.
3147
+ * @param stage - The resolved deploy stage (for the confirm prompt).
3148
+ * @param deps - Interactivity + the confirm prompt.
3149
+ * @returns The deployed URL once live; undefined when the user declined the gate or a retry (abort).
3150
+ * @example
3151
+ * ```ts
3152
+ * const url = await guidedDeployStep(ctx, manifest, stage, deps);
3153
+ * if (url === undefined) return emitAborted(ctx);
3154
+ * ```
3155
+ */
3156
+ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3157
+ if (!await deps.confirm(`Deploy "${manifest.name}" to ${stage}?`)) return void 0;
3158
+ ctx.emit("deploy:phase", { phase: "deploy" });
3159
+ const url = await guidedStep(() => runWrangler([
3160
+ "deploy",
3161
+ "--config",
3162
+ ctx.config.configFile
3163
+ ]), HINTS.deploy, deps);
3164
+ if (url === ABORTED) return void 0;
3165
+ ctx.emit("deploy:complete", { url });
3166
+ return url;
3167
+ };
3168
+ /**
3089
3169
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3090
3170
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3091
3171
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3127,11 +3207,11 @@ const createDeployApi = (ctx) => ({
3127
3207
  const ci = opts?.ci ?? ctx.config.ci;
3128
3208
  const stage = opts?.stage ?? ctx.global.stage;
3129
3209
  const interactive = !ci && stdoutIsTty();
3130
- const confirm = interactive ? (0, _moku_labs_common_cli.createBrandPrompts)().confirm : async (_question) => true;
3131
3210
  const deps = {
3132
3211
  interactive,
3133
- confirm
3212
+ confirm: interactive ? (0, _moku_labs_common_cli.createBrandPrompts)().confirm : async (_question) => true
3134
3213
  };
3214
+ const startedAt = Date.now();
3135
3215
  ctx.emit("deploy:phase", { phase: "auth" });
3136
3216
  if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
3137
3217
  if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return emitAborted(ctx);
@@ -3143,15 +3223,16 @@ const createDeployApi = (ctx) => ({
3143
3223
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3144
3224
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3145
3225
  if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3146
- if (!await confirm(`Deploy "${manifest.name}" to ${stage}?`)) return emitAborted(ctx);
3147
- ctx.emit("deploy:phase", { phase: "deploy" });
3148
- const url = await guidedStep(() => runWrangler([
3149
- "deploy",
3150
- "--config",
3151
- ctx.config.configFile
3152
- ]), HINTS.deploy, deps);
3153
- if (url === ABORTED) return emitAborted(ctx);
3154
- ctx.emit("deploy:complete", { url });
3226
+ const url = await guidedDeployStep(ctx, manifest, stage, deps);
3227
+ if (url === void 0) return emitAborted(ctx);
3228
+ renderDeploySummary((0, _moku_labs_common_cli.createBrandConsole)(), {
3229
+ url,
3230
+ stage,
3231
+ created: provisioned.created.length,
3232
+ exists: provisioned.skipped.length,
3233
+ failed: provisioned.failed.length,
3234
+ elapsedMs: Date.now() - startedAt
3235
+ });
3155
3236
  },
3156
3237
  /**
3157
3238
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3603,6 +3684,25 @@ const createCliApi = (ctx) => ({
3603
3684
  //#region src/plugins/cli/handlers.ts
3604
3685
  /** Divider drawn before the native `wrangler dev` TUI so the moku preamble reads as one section. */
3605
3686
  const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3687
+ /** Deploy phases that are a slow, opaque wait (captured output) — worth a live spinner on a TTY. */
3688
+ const SPINNER_PHASES = new Set(["upload", "deploy"]);
3689
+ /** Braille spinner glyphs; advance one per tick. */
3690
+ const SPINNER_FRAMES = [
3691
+ "⠋",
3692
+ "⠙",
3693
+ "⠹",
3694
+ "⠸",
3695
+ "⠼",
3696
+ "⠴",
3697
+ "⠦",
3698
+ "⠧",
3699
+ "⠇",
3700
+ "⠏"
3701
+ ];
3702
+ /** Spinner tick interval (ms). */
3703
+ const SPINNER_TICK_MS = 80;
3704
+ /** Carriage-return + blanks + carriage-return that wipes the transient spinner line before settling. */
3705
+ const SPINNER_CLEAR = `\r${" ".repeat(72)}\r`;
3606
3706
  /**
3607
3707
  * Builds the hook handlers that turn global deploy events into a live progress TUI.
3608
3708
  * Each logs a clean, prefix-free message via `ctx.log`; the branded log sink (installed
@@ -3617,24 +3717,53 @@ const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3617
3717
  * const hooks = createCliHooks(ctx);
3618
3718
  * hooks["deploy:phase"]({ phase: "detect" }); // logs "detect" → renders " › detect"
3619
3719
  * hooks["dev:phase"]({ phase: "serve", detail: "http://localhost:8787" }); // "serve · …"
3620
- * hooks["deploy:complete"]({ url: "https://x.workers.dev" }); // "deployed https://x.workers.dev"
3720
+ * hooks["deploy:complete"](); // settles the deploy spinner; the "Deployed" panel renders separately
3621
3721
  * ```
3622
3722
  */
3623
3723
  const createCliHooks = (ctx) => {
3624
3724
  const ui = (0, _moku_labs_common_cli.createBrandConsole)();
3725
+ const { palette } = ui;
3726
+ let spinnerTimer;
3727
+ let spinnerLabel;
3728
+ const stopSpinner = () => {
3729
+ if (spinnerTimer !== void 0) {
3730
+ clearInterval(spinnerTimer);
3731
+ spinnerTimer = void 0;
3732
+ }
3733
+ if (spinnerLabel !== void 0) {
3734
+ process.stdout.write(SPINNER_CLEAR);
3735
+ ctx.log.info(spinnerLabel);
3736
+ spinnerLabel = void 0;
3737
+ }
3738
+ };
3739
+ const startSpinner = (label) => {
3740
+ spinnerLabel = label;
3741
+ let frame = 0;
3742
+ const text = `${label} …`;
3743
+ spinnerTimer = setInterval(() => {
3744
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0];
3745
+ frame += 1;
3746
+ process.stdout.write(`\r ${palette.pink(glyph)} ${palette.dim(text)}`);
3747
+ }, SPINNER_TICK_MS);
3748
+ };
3625
3749
  return {
3626
3750
  /**
3627
- * Log one clean line per pipeline phase: "phase" or "phase · detail".
3751
+ * Render one pipeline phase. Quick phases print a clean line ("phase" / "phase · detail"); the
3752
+ * slow opaque waits (upload / deploy) animate a branded spinner on a TTY, settling to a line when
3753
+ * the next phase or completion arrives. Off a TTY every phase is a plain line (unchanged).
3628
3754
  *
3629
3755
  * @param p - The deploy:phase event payload.
3630
3756
  * @example
3631
3757
  * ```ts
3632
3758
  * handler({ phase: "detect" }); // "detect"
3633
- * handler({ phase: "upload", detail: "3 files" }); // "upload · 3 files"
3759
+ * handler({ phase: "deploy" }); // spins on a TTY, else "deploy"
3634
3760
  * ```
3635
3761
  */
3636
3762
  "deploy:phase"(p) {
3637
- ctx.log.info(p.detail ? `${p.phase} · ${p.detail}` : p.phase);
3763
+ stopSpinner();
3764
+ const label = p.detail ? `${p.phase} · ${p.detail}` : p.phase;
3765
+ if (process.stdout.isTTY === true && SPINNER_PHASES.has(p.phase)) startSpinner(label);
3766
+ else ctx.log.info(label);
3638
3767
  },
3639
3768
  /**
3640
3769
  * Log one dev-session phase: "phase" or "phase · detail".
@@ -3675,16 +3804,16 @@ const createCliHooks = (ctx) => {
3675
3804
  ctx.log.warn(p.message);
3676
3805
  },
3677
3806
  /**
3678
- * Log the terminal success line with the deployed URL.
3807
+ * Settle the final deploy spinner. The deployed URL + summary now render as a branded panel (the
3808
+ * deploy plugin's renderDeploySummary), so the cli no longer logs a duplicate `deployed → url`.
3679
3809
  *
3680
- * @param p - The deploy:complete event payload.
3681
3810
  * @example
3682
3811
  * ```ts
3683
- * handler({ url: "https://my-worker.workers.dev" }); // "deployed https://my-worker.workers.dev"
3812
+ * handler(); // clears the `deploy` spinner; the "Deployed" panel follows from the deploy plugin
3684
3813
  * ```
3685
3814
  */
3686
- "deploy:complete"(p) {
3687
- ctx.log.info(`deployed → ${p.url}`);
3815
+ "deploy:complete"() {
3816
+ stopSpinner();
3688
3817
  }
3689
3818
  };
3690
3819
  };
@@ -1961,8 +1961,9 @@ const runDev = async (ctx, opts, deps) => {
1961
1961
  //#region src/plugins/deploy/infra/plan.ts
1962
1962
  /**
1963
1963
  * Decide whether a single declared resource already exists in the account, recovering its id
1964
- * (kv/d1) when it does. Durable Objects are config-only (they ship with the script), so they are
1965
- * 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.
1966
1967
  *
1967
1968
  * @param resource - The declared resource descriptor.
1968
1969
  * @param existing - The indexed set of resources already in the account.
@@ -1990,7 +1991,7 @@ const checkExisting = (resource, existing) => {
1990
1991
  }
1991
1992
  case "r2": return { exists: existing.r2.has(resource.name) };
1992
1993
  case "queue": return { exists: existing.queue.has(resource.name) };
1993
- case "do": return { exists: false };
1994
+ case "do": return { exists: true };
1994
1995
  }
1995
1996
  };
1996
1997
  /**
@@ -2184,6 +2185,57 @@ const renderProvisionResult = (ui, result) => {
2184
2185
  }
2185
2186
  }
2186
2187
  };
2188
+ /**
2189
+ * Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
2190
+ * and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
2191
+ *
2192
+ * @param ms - The elapsed milliseconds.
2193
+ * @returns The compact duration string.
2194
+ * @example
2195
+ * ```ts
2196
+ * formatDuration(4234); // "4.2s"
2197
+ * ```
2198
+ */
2199
+ const formatDuration = (ms) => {
2200
+ if (ms < 1e3) return `${String(ms)}ms`;
2201
+ const seconds = ms / 1e3;
2202
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
2203
+ const whole = Math.floor(seconds);
2204
+ return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
2205
+ };
2206
+ /**
2207
+ * Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
2208
+ * live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
2209
+ * key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
2210
+ * and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
2211
+ *
2212
+ * @param ui - The branded console to render through.
2213
+ * @param summary - The deploy summary fields.
2214
+ * @param summary.url - The live deployed URL (the panel headline).
2215
+ * @param summary.stage - The target stage the worker deployed to.
2216
+ * @param summary.created - How many resources were created this run.
2217
+ * @param summary.exists - How many resources already existed (skipped).
2218
+ * @param summary.failed - How many resources failed to provision.
2219
+ * @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
2220
+ * @example
2221
+ * ```ts
2222
+ * renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, failed: 0, elapsedMs: 4234 });
2223
+ * ```
2224
+ */
2225
+ const renderDeploySummary = (ui, summary) => {
2226
+ const { palette } = ui;
2227
+ const tally = `${String(summary.exists)} exist · ${String(summary.created)} created`;
2228
+ const failedLabel = palette.red(`${String(summary.failed)} failed`);
2229
+ const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
2230
+ ui.heading("Deployed");
2231
+ ui.box([
2232
+ palette.pink(summary.url),
2233
+ "",
2234
+ `${palette.dim("stage".padEnd(10))}${summary.stage}`,
2235
+ `${palette.dim("resources".padEnd(10))}${resources}`,
2236
+ `${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
2237
+ ]);
2238
+ };
2187
2239
  //#endregion
2188
2240
  //#region src/plugins/deploy/naming.ts
2189
2241
  /**
@@ -3063,6 +3115,34 @@ const guidedUpload = async (ctx, manifest, deps) => {
3063
3115
  return true;
3064
3116
  };
3065
3117
  /**
3118
+ * The final deploy step: confirm the target (guided only), run `wrangler deploy` with interactive
3119
+ * retry, then emit deploy:complete. Returns the deployed URL, or undefined when the target gate or a
3120
+ * deploy retry is declined (so the caller renders the summary panel only on a real success).
3121
+ *
3122
+ * @param ctx - The deploy plugin context.
3123
+ * @param manifest - The assembled (or caller-supplied) deploy manifest.
3124
+ * @param stage - The resolved deploy stage (for the confirm prompt).
3125
+ * @param deps - Interactivity + the confirm prompt.
3126
+ * @returns The deployed URL once live; undefined when the user declined the gate or a retry (abort).
3127
+ * @example
3128
+ * ```ts
3129
+ * const url = await guidedDeployStep(ctx, manifest, stage, deps);
3130
+ * if (url === undefined) return emitAborted(ctx);
3131
+ * ```
3132
+ */
3133
+ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
3134
+ if (!await deps.confirm(`Deploy "${manifest.name}" to ${stage}?`)) return void 0;
3135
+ ctx.emit("deploy:phase", { phase: "deploy" });
3136
+ const url = await guidedStep(() => runWrangler([
3137
+ "deploy",
3138
+ "--config",
3139
+ ctx.config.configFile
3140
+ ]), HINTS.deploy, deps);
3141
+ if (url === ABORTED) return void 0;
3142
+ ctx.emit("deploy:complete", { url });
3143
+ return url;
3144
+ };
3145
+ /**
3066
3146
  * Create the deploy api. Assembles the manifest from each resource plugin's own deployManifest(),
3067
3147
  * runs an infra preflight (check-before-create + id capture), generates config, uploads, and runs
3068
3148
  * `wrangler deploy`, emitting global deploy events along the way.
@@ -3104,11 +3184,11 @@ const createDeployApi = (ctx) => ({
3104
3184
  const ci = opts?.ci ?? ctx.config.ci;
3105
3185
  const stage = opts?.stage ?? ctx.global.stage;
3106
3186
  const interactive = !ci && stdoutIsTty();
3107
- const confirm = interactive ? createBrandPrompts().confirm : async (_question) => true;
3108
3187
  const deps = {
3109
3188
  interactive,
3110
- confirm
3189
+ confirm: interactive ? createBrandPrompts().confirm : async (_question) => true
3111
3190
  };
3191
+ const startedAt = Date.now();
3112
3192
  ctx.emit("deploy:phase", { phase: "auth" });
3113
3193
  if (!await guidedAuth(ctx, deps)) return emitAborted(ctx);
3114
3194
  if (!await guidedWebBuild(ctx, opts?.webBuild ?? ctx.config.webBuild, deps)) return emitAborted(ctx);
@@ -3120,15 +3200,16 @@ const createDeployApi = (ctx) => ({
3120
3200
  ctx.emit("deploy:phase", { phase: "wrangler-config" });
3121
3201
  await writeWranglerConfig(ctx.config.configFile, manifest, provisioned.ids, wranglerExtra(ctx.config));
3122
3202
  if (!await guidedUpload(ctx, manifest, deps)) return emitAborted(ctx);
3123
- if (!await confirm(`Deploy "${manifest.name}" to ${stage}?`)) return emitAborted(ctx);
3124
- ctx.emit("deploy:phase", { phase: "deploy" });
3125
- const url = await guidedStep(() => runWrangler([
3126
- "deploy",
3127
- "--config",
3128
- ctx.config.configFile
3129
- ]), HINTS.deploy, deps);
3130
- if (url === ABORTED) return emitAborted(ctx);
3131
- ctx.emit("deploy:complete", { url });
3203
+ const url = await guidedDeployStep(ctx, manifest, stage, deps);
3204
+ if (url === void 0) return emitAborted(ctx);
3205
+ renderDeploySummary(createBrandConsole(), {
3206
+ url,
3207
+ stage,
3208
+ created: provisioned.created.length,
3209
+ exists: provisioned.skipped.length,
3210
+ failed: provisioned.failed.length,
3211
+ elapsedMs: Date.now() - startedAt
3212
+ });
3132
3213
  },
3133
3214
  /**
3134
3215
  * Start a long-lived local dev session: cold-build the Moku site, spawn `wrangler dev
@@ -3580,6 +3661,25 @@ const createCliApi = (ctx) => ({
3580
3661
  //#region src/plugins/cli/handlers.ts
3581
3662
  /** Divider drawn before the native `wrangler dev` TUI so the moku preamble reads as one section. */
3582
3663
  const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3664
+ /** Deploy phases that are a slow, opaque wait (captured output) — worth a live spinner on a TTY. */
3665
+ const SPINNER_PHASES = new Set(["upload", "deploy"]);
3666
+ /** Braille spinner glyphs; advance one per tick. */
3667
+ const SPINNER_FRAMES = [
3668
+ "⠋",
3669
+ "⠙",
3670
+ "⠹",
3671
+ "⠸",
3672
+ "⠼",
3673
+ "⠴",
3674
+ "⠦",
3675
+ "⠧",
3676
+ "⠇",
3677
+ "⠏"
3678
+ ];
3679
+ /** Spinner tick interval (ms). */
3680
+ const SPINNER_TICK_MS = 80;
3681
+ /** Carriage-return + blanks + carriage-return that wipes the transient spinner line before settling. */
3682
+ const SPINNER_CLEAR = `\r${" ".repeat(72)}\r`;
3583
3683
  /**
3584
3684
  * Builds the hook handlers that turn global deploy events into a live progress TUI.
3585
3685
  * Each logs a clean, prefix-free message via `ctx.log`; the branded log sink (installed
@@ -3594,24 +3694,53 @@ const WRANGLER_DIVIDER = ` ── wrangler ${"─".repeat(48)}`;
3594
3694
  * const hooks = createCliHooks(ctx);
3595
3695
  * hooks["deploy:phase"]({ phase: "detect" }); // logs "detect" → renders " › detect"
3596
3696
  * hooks["dev:phase"]({ phase: "serve", detail: "http://localhost:8787" }); // "serve · …"
3597
- * hooks["deploy:complete"]({ url: "https://x.workers.dev" }); // "deployed https://x.workers.dev"
3697
+ * hooks["deploy:complete"](); // settles the deploy spinner; the "Deployed" panel renders separately
3598
3698
  * ```
3599
3699
  */
3600
3700
  const createCliHooks = (ctx) => {
3601
3701
  const ui = createBrandConsole();
3702
+ const { palette } = ui;
3703
+ let spinnerTimer;
3704
+ let spinnerLabel;
3705
+ const stopSpinner = () => {
3706
+ if (spinnerTimer !== void 0) {
3707
+ clearInterval(spinnerTimer);
3708
+ spinnerTimer = void 0;
3709
+ }
3710
+ if (spinnerLabel !== void 0) {
3711
+ process.stdout.write(SPINNER_CLEAR);
3712
+ ctx.log.info(spinnerLabel);
3713
+ spinnerLabel = void 0;
3714
+ }
3715
+ };
3716
+ const startSpinner = (label) => {
3717
+ spinnerLabel = label;
3718
+ let frame = 0;
3719
+ const text = `${label} …`;
3720
+ spinnerTimer = setInterval(() => {
3721
+ const glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0];
3722
+ frame += 1;
3723
+ process.stdout.write(`\r ${palette.pink(glyph)} ${palette.dim(text)}`);
3724
+ }, SPINNER_TICK_MS);
3725
+ };
3602
3726
  return {
3603
3727
  /**
3604
- * Log one clean line per pipeline phase: "phase" or "phase · detail".
3728
+ * Render one pipeline phase. Quick phases print a clean line ("phase" / "phase · detail"); the
3729
+ * slow opaque waits (upload / deploy) animate a branded spinner on a TTY, settling to a line when
3730
+ * the next phase or completion arrives. Off a TTY every phase is a plain line (unchanged).
3605
3731
  *
3606
3732
  * @param p - The deploy:phase event payload.
3607
3733
  * @example
3608
3734
  * ```ts
3609
3735
  * handler({ phase: "detect" }); // "detect"
3610
- * handler({ phase: "upload", detail: "3 files" }); // "upload · 3 files"
3736
+ * handler({ phase: "deploy" }); // spins on a TTY, else "deploy"
3611
3737
  * ```
3612
3738
  */
3613
3739
  "deploy:phase"(p) {
3614
- ctx.log.info(p.detail ? `${p.phase} · ${p.detail}` : p.phase);
3740
+ stopSpinner();
3741
+ const label = p.detail ? `${p.phase} · ${p.detail}` : p.phase;
3742
+ if (process.stdout.isTTY === true && SPINNER_PHASES.has(p.phase)) startSpinner(label);
3743
+ else ctx.log.info(label);
3615
3744
  },
3616
3745
  /**
3617
3746
  * Log one dev-session phase: "phase" or "phase · detail".
@@ -3652,16 +3781,16 @@ const createCliHooks = (ctx) => {
3652
3781
  ctx.log.warn(p.message);
3653
3782
  },
3654
3783
  /**
3655
- * Log the terminal success line with the deployed URL.
3784
+ * Settle the final deploy spinner. The deployed URL + summary now render as a branded panel (the
3785
+ * deploy plugin's renderDeploySummary), so the cli no longer logs a duplicate `deployed → url`.
3656
3786
  *
3657
- * @param p - The deploy:complete event payload.
3658
3787
  * @example
3659
3788
  * ```ts
3660
- * handler({ url: "https://my-worker.workers.dev" }); // "deployed https://my-worker.workers.dev"
3789
+ * handler(); // clears the `deploy` spinner; the "Deployed" panel follows from the deploy plugin
3661
3790
  * ```
3662
3791
  */
3663
- "deploy:complete"(p) {
3664
- ctx.log.info(`deployed → ${p.url}`);
3792
+ "deploy:complete"() {
3793
+ stopSpinner();
3665
3794
  }
3666
3795
  };
3667
3796
  };
package/dist/cli.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-BBO_YNVC.cjs");
2
+ const require_cli = require("./cli-DQcpvh2s.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-Dse6wZJH.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-Dse6wZJH.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-D67ea3Lu.mjs";
1
+ import { n as deployPlugin, t as cliPlugin } from "./cli-MICK1cvv.mjs";
2
2
  export { cliPlugin, deployPlugin };
@@ -267,17 +267,21 @@ type Api = {
267
267
  * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
268
  * throwing a raw stack trace.
269
269
  *
270
- * @param opts - Optional port and web build hook.
270
+ * @param opts - Optional port, stage, and web build hook.
271
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.
272
275
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
273
276
  * @returns Resolves when the dev session ends.
274
277
  * @example
275
278
  * ```ts
276
- * await app.cli.dev({ port: 7878, webBuild: () => web.cli.build() });
279
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
277
280
  * ```
278
281
  */
279
282
  dev(opts?: {
280
283
  port?: number;
284
+ stage?: string;
281
285
  webBuild?: WebBuild;
282
286
  }): Promise<void>;
283
287
  /**
@@ -285,18 +289,22 @@ type Api = {
285
289
  * `{ ci: true }` for the automated/non-interactive path (CI). A failure renders a branded `✗`
286
290
  * line and sets a non-zero exit code rather than throwing a raw stack trace.
287
291
  *
288
- * @param opts - Optional ci flag and a web build hook.
292
+ * @param opts - Optional ci flag, stage, and a web build hook.
289
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.
290
297
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
291
298
  * @returns Resolves once the deploy completes (or after a failure is rendered).
292
299
  * @example
293
300
  * ```ts
294
- * await app.cli.deploy({ webBuild: () => web.cli.build() }); // guided
295
- * 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
296
303
  * ```
297
304
  */
298
305
  deploy(opts?: {
299
306
  ci?: boolean;
307
+ stage?: string;
300
308
  webBuild?: WebBuild;
301
309
  }): Promise<void>;
302
310
  /**
@@ -267,17 +267,21 @@ type Api = {
267
267
  * when omitted. A failure renders a branded `✗` line and sets a non-zero exit code rather than
268
268
  * throwing a raw stack trace.
269
269
  *
270
- * @param opts - Optional port and web build hook.
270
+ * @param opts - Optional port, stage, and web build hook.
271
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.
272
275
  * @param opts.webBuild - Rebuild the web site on change (e.g. `() => webApp.cli.build()`).
273
276
  * @returns Resolves when the dev session ends.
274
277
  * @example
275
278
  * ```ts
276
- * await app.cli.dev({ port: 7878, webBuild: () => web.cli.build() });
279
+ * await app.cli.dev({ stage: "dev", port: 7878, webBuild: () => web.cli.build() });
277
280
  * ```
278
281
  */
279
282
  dev(opts?: {
280
283
  port?: number;
284
+ stage?: string;
281
285
  webBuild?: WebBuild;
282
286
  }): Promise<void>;
283
287
  /**
@@ -285,18 +289,22 @@ type Api = {
285
289
  * `{ ci: true }` for the automated/non-interactive path (CI). A failure renders a branded `✗`
286
290
  * line and sets a non-zero exit code rather than throwing a raw stack trace.
287
291
  *
288
- * @param opts - Optional ci flag and a web build hook.
292
+ * @param opts - Optional ci flag, stage, and a web build hook.
289
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.
290
297
  * @param opts.webBuild - Build the web site first (e.g. `() => webApp.cli.build()`), before deploy.
291
298
  * @returns Resolves once the deploy completes (or after a failure is rendered).
292
299
  * @example
293
300
  * ```ts
294
- * await app.cli.deploy({ webBuild: () => web.cli.build() }); // guided
295
- * 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
296
303
  * ```
297
304
  */
298
305
  deploy(opts?: {
299
306
  ci?: boolean;
307
+ stage?: string;
300
308
  webBuild?: WebBuild;
301
309
  }): Promise<void>;
302
310
  /**
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_cli = require("./cli-BBO_YNVC.cjs");
2
+ const require_cli = require("./cli-DQcpvh2s.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-Dse6wZJH.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
 
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-Dse6wZJH.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
 
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-D67ea3Lu.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-MICK1cvv.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.1",
3
+ "version": "0.7.3",
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",