@moku-labs/web 1.3.1 → 1.4.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.
package/dist/index.cjs CHANGED
@@ -3842,7 +3842,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
3842
3842
  * ```
3843
3843
  */
3844
3844
  function wrap(body) {
3845
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
3845
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
3846
3846
  }
3847
3847
  /**
3848
3848
  * Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
@@ -4526,6 +4526,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
4526
4526
  const BODY_PLACEHOLDER = "<!--moku:body-->";
4527
4527
  /** Template placeholder for the injected asset `<link>`/`<script>` tags. */
4528
4528
  const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
4529
+ /** Template placeholder for the page's locale (`<html lang>`). */
4530
+ const LANG_PLACEHOLDER = "<!--moku:lang-->";
4529
4531
  /**
4530
4532
  * Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
4531
4533
  * as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
@@ -4573,14 +4575,16 @@ function buildAssetTags(ctx) {
4573
4575
  * ```
4574
4576
  */
4575
4577
  function renderDocument(parts) {
4576
- return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
4578
+ return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
4577
4579
  }
4578
4580
  /**
4579
- * Fill a shell template's `<!--moku:head-->` / `<!--moku:body-->` /
4580
- * `<!--moku:assets-->` placeholders deterministically at build time.
4581
+ * Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
4582
+ * `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
4583
+ * time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
4584
+ * shared template stays locale-correct across every locale.
4581
4585
  *
4582
4586
  * @param template - The raw shell template HTML.
4583
- * @param parts - The composed head/body/assets pieces.
4587
+ * @param parts - The composed head/body/assets/locale pieces.
4584
4588
  * @returns The filled document string.
4585
4589
  * @example
4586
4590
  * ```ts
@@ -4588,7 +4592,7 @@ function renderDocument(parts) {
4588
4592
  * ```
4589
4593
  */
4590
4594
  function fillTemplate(template, parts) {
4591
- return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
4595
+ return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
4592
4596
  }
4593
4597
  /**
4594
4598
  * Resolve the compiled entry for a manifest definition, asserting the router
@@ -5840,7 +5844,8 @@ function buildWranglerArgs(input) {
5840
5844
  "--project-name",
5841
5845
  input.slug,
5842
5846
  "--branch",
5843
- branch
5847
+ branch,
5848
+ "--commit-dirty=true"
5844
5849
  ];
5845
5850
  }
5846
5851
  /**
@@ -6449,17 +6454,18 @@ function validateConfig$1(ctx) {
6449
6454
  ctx.require(sitePlugin);
6450
6455
  }
6451
6456
  /**
6452
- * Run wrangler for the prepared argv and surface its scrubbed result, translating
6453
- * a non-zero exit into the classified deploy error. The API token is read from env
6454
- * here so it never crosses a logging boundary; only scrubbed output is returned.
6455
- * Shared by `run()` (deploy) and `createProject()` (project create).
6457
+ * Run wrangler for the prepared argv and return its stdout, translating a non-zero
6458
+ * exit into the classified deploy error. The API token is read from env here so it
6459
+ * never crosses a logging boundary; the scrubbed stderr is used only to classify a
6460
+ * failure it is never logged (that was console noise), so nothing leaks. Shared by
6461
+ * `run()` (deploy) and `createProject()` (project create).
6456
6462
  *
6457
6463
  * @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
6458
6464
  * @param args - The fully-built, pre-validated wrangler argv.
6459
- * @returns The wrangler `stdout` plus the scrubbed `stderr` to log on success.
6465
+ * @returns The wrangler `stdout` (for URL/id parsing on a deploy).
6460
6466
  * @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
6461
6467
  * @example
6462
- * const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
6468
+ * const stdout = await executeWrangler(ctx, args);
6463
6469
  */
6464
6470
  async function executeWrangler(ctx, args) {
6465
6471
  const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
@@ -6473,10 +6479,7 @@ async function executeWrangler(ctx, args) {
6473
6479
  const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
6474
6480
  throw deployError(code, message);
6475
6481
  }
6476
- return {
6477
- stdout,
6478
- scrubbedStderr
6479
- };
6482
+ return stdout;
6480
6483
  }
6481
6484
  /**
6482
6485
  * Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
@@ -6533,9 +6536,7 @@ function createApi$2(ctx) {
6533
6536
  root
6534
6537
  });
6535
6538
  const start = Date.now();
6536
- const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
6537
- ctx.log.info(scrubbedStderr);
6538
- const result = buildDeployResult(stdout, branch, start);
6539
+ const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
6539
6540
  ctx.state.lastDeployment = result;
6540
6541
  ctx.emit("deploy:complete", {
6541
6542
  url: result.url,
@@ -6595,11 +6596,10 @@ function createApi$2(ctx) {
6595
6596
  async createProject() {
6596
6597
  const name = toSlug(ctx.require(sitePlugin).name());
6597
6598
  const branch = ctx.config.productionBranch ?? "main";
6598
- const { scrubbedStderr } = await executeWrangler(ctx, buildProjectCreateArgs({
6599
+ await executeWrangler(ctx, buildProjectCreateArgs({
6599
6600
  slug: name,
6600
6601
  branch
6601
6602
  }));
6602
- ctx.log.info(scrubbedStderr);
6603
6603
  return {
6604
6604
  name,
6605
6605
  branch
@@ -8766,16 +8766,24 @@ function createPanelRenderer(options = {}) {
8766
8766
  write(line);
8767
8767
  },
8768
8768
  /**
8769
- * Render the deploy result from a `deploy:complete` event: a `✓ DEPLOYED → url` line
8770
- * with the URL the hero value, then a dim `branch · id · time` line beneath it.
8769
+ * Render the deploy result from a `deploy:complete` event as a full-width box (matching
8770
+ * the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
8771
+ * then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
8772
+ * URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
8771
8773
  *
8772
8774
  * @param result - The `deploy:complete` payload.
8773
8775
  * @example
8774
8776
  * render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
8775
8777
  */
8776
8778
  deployed(result) {
8777
- const meta = palette.dim(`branch ${result.branch} · ${result.deploymentId} · ${result.durationMs}ms`);
8778
- writeBlock([` ${palette.green("✓")} ${palette.bold("DEPLOYED")} ${palette.dim("→")} ${palette.cyan(result.url)}`, ` ${meta}`]);
8779
+ const dot = palette.dim("·");
8780
+ const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
8781
+ const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
8782
+ if (result.url) {
8783
+ const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
8784
+ lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
8785
+ } else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
8786
+ writeBlock(box(lines, color, BOX_INNER));
8779
8787
  },
8780
8788
  /**
8781
8789
  * Render a neutral informational line.
@@ -8870,6 +8878,29 @@ function createPanelRenderer(options = {}) {
8870
8878
  */
8871
8879
  /** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
8872
8880
  const YES_PATTERN = /^y(es)?$/i;
8881
+ /** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
8882
+ const PROMPT_WIDTH = 66;
8883
+ /** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
8884
+ const PROMPT_COLOR = supportsColor();
8885
+ /** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
8886
+ const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
8887
+ /**
8888
+ * Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
8889
+ * a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
8890
+ * to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
8891
+ *
8892
+ * @param question - The yes/no question to display.
8893
+ * @returns The readline prompt string (the typed answer follows the caret).
8894
+ * @example
8895
+ * confirmPrompt("Deploy dist/ to Cloudflare Pages?");
8896
+ */
8897
+ function confirmPrompt(question) {
8898
+ if (!PROMPT_COLOR) return `${question} [y/N] `;
8899
+ const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
8900
+ const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
8901
+ const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
8902
+ return `${left}${" ".repeat(gap)}${right}`;
8903
+ }
8873
8904
  /**
8874
8905
  * Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
8875
8906
  *
@@ -8927,7 +8958,7 @@ function defaultConfirm(question) {
8927
8958
  input: process.stdin,
8928
8959
  output: process.stdout
8929
8960
  });
8930
- readline.question(`${question} [y/N] `, (answer) => {
8961
+ readline.question(confirmPrompt(question), (answer) => {
8931
8962
  readline.close();
8932
8963
  resolve(YES_PATTERN.test(answer.trim()));
8933
8964
  });
@@ -8950,8 +8981,8 @@ function defaultSelect(question, choices) {
8950
8981
  input: process.stdin,
8951
8982
  output: process.stdout
8952
8983
  });
8953
- for (const [index, choice] of choices.entries()) console.log(` ${index + 1}) ${choice}`);
8954
- readline.question(`${question} [1-${choices.length}] `, (answer) => {
8984
+ console.log(selectChoicesBlock(question, choices));
8985
+ readline.question(selectPrompt(question, choices.length), (answer) => {
8955
8986
  readline.close();
8956
8987
  const picked = Number.parseInt(answer.trim(), 10);
8957
8988
  resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
@@ -8959,6 +8990,36 @@ function defaultSelect(question, choices) {
8959
8990
  });
8960
8991
  }
8961
8992
  /**
8993
+ * Render the select block: a brand `◆` marker + the question, then each choice as an
8994
+ * indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
8995
+ * list (the question rides the prompt instead).
8996
+ *
8997
+ * @param question - The prompt shown above the choices (styled mode only).
8998
+ * @param choices - The selectable option labels.
8999
+ * @returns The multi-line choices block.
9000
+ * @example
9001
+ * selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
9002
+ */
9003
+ function selectChoicesBlock(question, choices) {
9004
+ if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
9005
+ return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
9006
+ }
9007
+ /**
9008
+ * Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
9009
+ * or the plain `question [1-N] ` form off a color TTY (where the question is not printed
9010
+ * separately).
9011
+ *
9012
+ * @param question - The prompt (used only by the plain fallback).
9013
+ * @param count - The number of choices.
9014
+ * @returns The readline prompt string.
9015
+ * @example
9016
+ * selectPrompt("Set up a workflow?", 3);
9017
+ */
9018
+ function selectPrompt(question, count) {
9019
+ if (!PROMPT_COLOR) return `${question} [1-${count}] `;
9020
+ return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
9021
+ }
9022
+ /**
8962
9023
  * Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
8963
9024
  * and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
8964
9025
  * FS watch is registered.
package/dist/index.d.cts CHANGED
@@ -1615,7 +1615,17 @@ type Config$3 = {
1615
1615
  body?: string;
1616
1616
  }; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
1617
1617
  localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
1618
- clientEntry?: string; /** HTML shell template with `<!--moku:head-->`/`<!--moku:body-->`/`<!--moku:assets-->` placeholders. */
1618
+ clientEntry?: string;
1619
+ /**
1620
+ * Path to a custom HTML document shell, giving the app full control over the
1621
+ * scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
1622
+ * Placeholders, substituted per page at build time:
1623
+ * `<!--moku:lang-->` (page locale for `<html lang>`),
1624
+ * `<!--moku:head-->` (composed `<head>` inner HTML),
1625
+ * `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
1626
+ * `<!--moku:body-->` (SSR body HTML).
1627
+ * When unset, the built-in shell is used (it emits charset + viewport by default).
1628
+ */
1619
1629
  template?: string;
1620
1630
  };
1621
1631
  /**
package/dist/index.d.mts CHANGED
@@ -1615,7 +1615,17 @@ type Config$3 = {
1615
1615
  body?: string;
1616
1616
  }; /** Emit per-path i18n bare-path redirect HTML pages. Default `false`. */
1617
1617
  localeRedirects?: boolean; /** Authoritative client bundle entry path (overrides the conventional scan). */
1618
- clientEntry?: string; /** HTML shell template with `<!--moku:head-->`/`<!--moku:body-->`/`<!--moku:assets-->` placeholders. */
1618
+ clientEntry?: string;
1619
+ /**
1620
+ * Path to a custom HTML document shell, giving the app full control over the
1621
+ * scaffold (charset, viewport, `<html lang>`, body attributes, wrapper markup).
1622
+ * Placeholders, substituted per page at build time:
1623
+ * `<!--moku:lang-->` (page locale for `<html lang>`),
1624
+ * `<!--moku:head-->` (composed `<head>` inner HTML),
1625
+ * `<!--moku:assets-->` (injected `<link>`/`<script>` tags),
1626
+ * `<!--moku:body-->` (SSR body HTML).
1627
+ * When unset, the built-in shell is used (it emits charset + viewport by default).
1628
+ */
1619
1629
  template?: string;
1620
1630
  };
1621
1631
  /**
package/dist/index.mjs CHANGED
@@ -3829,7 +3829,7 @@ const DEFAULT_BODY = "<h1>404</h1><p>The page you requested could not be found.<
3829
3829
  * ```
3830
3830
  */
3831
3831
  function wrap(body) {
3832
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 — Not Found</title></head><body>${body}</body></html>`;
3832
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>404 — Not Found</title></head><body>${body}</body></html>`;
3833
3833
  }
3834
3834
  /**
3835
3835
  * Emits `outDir/404.html`. When `config.notFound` is `true`, writes the built-in
@@ -4513,6 +4513,8 @@ const HEAD_PLACEHOLDER = "<!--moku:head-->";
4513
4513
  const BODY_PLACEHOLDER = "<!--moku:body-->";
4514
4514
  /** Template placeholder for the injected asset `<link>`/`<script>` tags. */
4515
4515
  const ASSETS_PLACEHOLDER = "<!--moku:assets-->";
4516
+ /** Template placeholder for the page's locale (`<html lang>`). */
4517
+ const LANG_PLACEHOLDER = "<!--moku:lang-->";
4516
4518
  /**
4517
4519
  * Read the bundle phase's hashed asset manifest for one kind from `state.buildCache`
4518
4520
  * as a typed {@link BuildCacheEntry} (no `Map<string, unknown>` reads).
@@ -4560,14 +4562,16 @@ function buildAssetTags(ctx) {
4560
4562
  * ```
4561
4563
  */
4562
4564
  function renderDocument(parts) {
4563
- return `<!DOCTYPE html><html lang="${parts.locale}"><head>${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
4565
+ return `<!DOCTYPE html><html lang="${parts.locale}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${parts.head}${parts.assets}</head><body>${parts.body}</body></html>`;
4564
4566
  }
4565
4567
  /**
4566
- * Fill a shell template's `<!--moku:head-->` / `<!--moku:body-->` /
4567
- * `<!--moku:assets-->` placeholders deterministically at build time.
4568
+ * Fill a shell template's `<!--moku:lang-->` / `<!--moku:head-->` /
4569
+ * `<!--moku:body-->` / `<!--moku:assets-->` placeholders deterministically at build
4570
+ * time. `<!--moku:lang-->` carries the page locale (for `<html lang>`), so a single
4571
+ * shared template stays locale-correct across every locale.
4568
4572
  *
4569
4573
  * @param template - The raw shell template HTML.
4570
- * @param parts - The composed head/body/assets pieces.
4574
+ * @param parts - The composed head/body/assets/locale pieces.
4571
4575
  * @returns The filled document string.
4572
4576
  * @example
4573
4577
  * ```ts
@@ -4575,7 +4579,7 @@ function renderDocument(parts) {
4575
4579
  * ```
4576
4580
  */
4577
4581
  function fillTemplate(template, parts) {
4578
- return template.replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
4582
+ return template.replaceAll(LANG_PLACEHOLDER, parts.locale).replaceAll(HEAD_PLACEHOLDER, parts.head).replaceAll(BODY_PLACEHOLDER, parts.body).replaceAll(ASSETS_PLACEHOLDER, parts.assets);
4579
4583
  }
4580
4584
  /**
4581
4585
  * Resolve the compiled entry for a manifest definition, asserting the router
@@ -5827,7 +5831,8 @@ function buildWranglerArgs(input) {
5827
5831
  "--project-name",
5828
5832
  input.slug,
5829
5833
  "--branch",
5830
- branch
5834
+ branch,
5835
+ "--commit-dirty=true"
5831
5836
  ];
5832
5837
  }
5833
5838
  /**
@@ -6436,17 +6441,18 @@ function validateConfig$1(ctx) {
6436
6441
  ctx.require(sitePlugin);
6437
6442
  }
6438
6443
  /**
6439
- * Run wrangler for the prepared argv and surface its scrubbed result, translating
6440
- * a non-zero exit into the classified deploy error. The API token is read from env
6441
- * here so it never crosses a logging boundary; only scrubbed output is returned.
6442
- * Shared by `run()` (deploy) and `createProject()` (project create).
6444
+ * Run wrangler for the prepared argv and return its stdout, translating a non-zero
6445
+ * exit into the classified deploy error. The API token is read from env here so it
6446
+ * never crosses a logging boundary; the scrubbed stderr is used only to classify a
6447
+ * failure it is never logged (that was console noise), so nothing leaks. Shared by
6448
+ * `run()` (deploy) and `createProject()` (project create).
6443
6449
  *
6444
6450
  * @param ctx - Plugin context (provides `state.spawn`, `config`, `env`).
6445
6451
  * @param args - The fully-built, pre-validated wrangler argv.
6446
- * @returns The wrangler `stdout` plus the scrubbed `stderr` to log on success.
6452
+ * @returns The wrangler `stdout` (for URL/id parsing on a deploy).
6447
6453
  * @throws {Error} With a `code` from the deploy error taxonomy on a non-zero exit.
6448
6454
  * @example
6449
- * const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
6455
+ * const stdout = await executeWrangler(ctx, args);
6450
6456
  */
6451
6457
  async function executeWrangler(ctx, args) {
6452
6458
  const token = ctx.env.require("CLOUDFLARE_API_TOKEN");
@@ -6460,10 +6466,7 @@ async function executeWrangler(ctx, args) {
6460
6466
  const { code, message } = classifyWranglerError(exitCode, scrubbedStderr);
6461
6467
  throw deployError(code, message);
6462
6468
  }
6463
- return {
6464
- stdout,
6465
- scrubbedStderr
6466
- };
6469
+ return stdout;
6467
6470
  }
6468
6471
  /**
6469
6472
  * Assemble the public {@link DeployResult} from wrangler's stdout, parsing the
@@ -6520,9 +6523,7 @@ function createApi$2(ctx) {
6520
6523
  root
6521
6524
  });
6522
6525
  const start = Date.now();
6523
- const { stdout, scrubbedStderr } = await executeWrangler(ctx, args);
6524
- ctx.log.info(scrubbedStderr);
6525
- const result = buildDeployResult(stdout, branch, start);
6526
+ const result = buildDeployResult(await executeWrangler(ctx, args), branch, start);
6526
6527
  ctx.state.lastDeployment = result;
6527
6528
  ctx.emit("deploy:complete", {
6528
6529
  url: result.url,
@@ -6582,11 +6583,10 @@ function createApi$2(ctx) {
6582
6583
  async createProject() {
6583
6584
  const name = toSlug(ctx.require(sitePlugin).name());
6584
6585
  const branch = ctx.config.productionBranch ?? "main";
6585
- const { scrubbedStderr } = await executeWrangler(ctx, buildProjectCreateArgs({
6586
+ await executeWrangler(ctx, buildProjectCreateArgs({
6586
6587
  slug: name,
6587
6588
  branch
6588
6589
  }));
6589
- ctx.log.info(scrubbedStderr);
6590
6590
  return {
6591
6591
  name,
6592
6592
  branch
@@ -8753,16 +8753,24 @@ function createPanelRenderer(options = {}) {
8753
8753
  write(line);
8754
8754
  },
8755
8755
  /**
8756
- * Render the deploy result from a `deploy:complete` event: a `✓ DEPLOYED → url` line
8757
- * with the URL the hero value, then a dim `branch · id · time` line beneath it.
8756
+ * Render the deploy result from a `deploy:complete` event as a full-width box (matching
8757
+ * the BUILD panel): a `✓ DEPLOYED · branch` header with the elapsed time right-aligned,
8758
+ * then a `→ url · id` row. The url/id row is omitted entirely when wrangler returned no
8759
+ * URL, so a first-deploy with nothing to parse never renders a dangling `→` or `· ·`.
8758
8760
  *
8759
8761
  * @param result - The `deploy:complete` payload.
8760
8762
  * @example
8761
8763
  * render.deployed({ url: "https://x.pages.dev", deploymentId: "id", branch: "main", durationMs: 1200 });
8762
8764
  */
8763
8765
  deployed(result) {
8764
- const meta = palette.dim(`branch ${result.branch} · ${result.deploymentId} · ${result.durationMs}ms`);
8765
- writeBlock([` ${palette.green("✓")} ${palette.bold("DEPLOYED")} ${palette.dim("→")} ${palette.cyan(result.url)}`, ` ${meta}`]);
8766
+ const dot = palette.dim("·");
8767
+ const time = result.durationMs >= 1e3 ? `${(result.durationMs / 1e3).toFixed(1)}s` : `${result.durationMs}ms`;
8768
+ const lines = [railLine(`${palette.green("✓")} ${palette.bold("DEPLOYED")} ${dot} ${result.branch}`, palette.dim(time), BOX_INNER)];
8769
+ if (result.url) {
8770
+ const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
8771
+ lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
8772
+ } else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
8773
+ writeBlock(box(lines, color, BOX_INNER));
8766
8774
  },
8767
8775
  /**
8768
8776
  * Render a neutral informational line.
@@ -8857,6 +8865,29 @@ function createPanelRenderer(options = {}) {
8857
8865
  */
8858
8866
  /** Matches an explicit affirmative answer (`y`/`yes`, case-insensitive). */
8859
8867
  const YES_PATTERN = /^y(es)?$/i;
8868
+ /** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
8869
+ const PROMPT_WIDTH = 66;
8870
+ /** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
8871
+ const PROMPT_COLOR = supportsColor();
8872
+ /** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
8873
+ const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
8874
+ /**
8875
+ * Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
8876
+ * a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
8877
+ * to the plain `question [y/N] ` form off a color TTY (CI/pipes), where prompts rarely run.
8878
+ *
8879
+ * @param question - The yes/no question to display.
8880
+ * @returns The readline prompt string (the typed answer follows the caret).
8881
+ * @example
8882
+ * confirmPrompt("Deploy dist/ to Cloudflare Pages?");
8883
+ */
8884
+ function confirmPrompt(question) {
8885
+ if (!PROMPT_COLOR) return `${question} [y/N] `;
8886
+ const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
8887
+ const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
8888
+ const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
8889
+ return `${left}${" ".repeat(gap)}${right}`;
8890
+ }
8860
8891
  /**
8861
8892
  * Resolve the `Bun` runtime global, or `undefined` when not running under Bun.
8862
8893
  *
@@ -8914,7 +8945,7 @@ function defaultConfirm(question) {
8914
8945
  input: process.stdin,
8915
8946
  output: process.stdout
8916
8947
  });
8917
- readline.question(`${question} [y/N] `, (answer) => {
8948
+ readline.question(confirmPrompt(question), (answer) => {
8918
8949
  readline.close();
8919
8950
  resolve(YES_PATTERN.test(answer.trim()));
8920
8951
  });
@@ -8937,8 +8968,8 @@ function defaultSelect(question, choices) {
8937
8968
  input: process.stdin,
8938
8969
  output: process.stdout
8939
8970
  });
8940
- for (const [index, choice] of choices.entries()) console.log(` ${index + 1}) ${choice}`);
8941
- readline.question(`${question} [1-${choices.length}] `, (answer) => {
8971
+ console.log(selectChoicesBlock(question, choices));
8972
+ readline.question(selectPrompt(question, choices.length), (answer) => {
8942
8973
  readline.close();
8943
8974
  const picked = Number.parseInt(answer.trim(), 10);
8944
8975
  resolve(Number.isInteger(picked) && picked >= 1 && picked <= choices.length ? picked - 1 : 0);
@@ -8946,6 +8977,36 @@ function defaultSelect(question, choices) {
8946
8977
  });
8947
8978
  }
8948
8979
  /**
8980
+ * Render the select block: a brand `◆` marker + the question, then each choice as an
8981
+ * indented dim number + label. Off a color TTY, falls back to the plain ` N) label`
8982
+ * list (the question rides the prompt instead).
8983
+ *
8984
+ * @param question - The prompt shown above the choices (styled mode only).
8985
+ * @param choices - The selectable option labels.
8986
+ * @returns The multi-line choices block.
8987
+ * @example
8988
+ * selectChoicesBlock("Set up a workflow?", ["Auto", "Manual", "Skip"]);
8989
+ */
8990
+ function selectChoicesBlock(question, choices) {
8991
+ if (!PROMPT_COLOR) return choices.map((choice, index) => ` ${index + 1}) ${choice}`).join("\n");
8992
+ return [` ${PROMPT_PALETTE.pink("◆")} ${question}`, ...choices.map((choice, index) => ` ${PROMPT_PALETTE.dim(String(index + 1))} ${choice}`)].join("\n");
8993
+ }
8994
+ /**
8995
+ * Build the select input prompt: a dim `pick 1–N` hint + cyan `›` caret in styled mode,
8996
+ * or the plain `question [1-N] ` form off a color TTY (where the question is not printed
8997
+ * separately).
8998
+ *
8999
+ * @param question - The prompt (used only by the plain fallback).
9000
+ * @param count - The number of choices.
9001
+ * @returns The readline prompt string.
9002
+ * @example
9003
+ * selectPrompt("Set up a workflow?", 3);
9004
+ */
9005
+ function selectPrompt(question, count) {
9006
+ if (!PROMPT_COLOR) return `${question} [1-${count}] `;
9007
+ return ` ${PROMPT_PALETTE.dim(`pick 1–${count}`)} ${PROMPT_PALETTE.cyan("›")} `;
9008
+ }
9009
+ /**
8949
9010
  * Default recursive directory watcher — wraps `node:fs.watch` with `{ recursive: true }`
8950
9011
  * and adapts its handle to {@link WatchHandle}. Tests inject a fake emitter so no real
8951
9012
  * FS watch is registered.
package/package.json CHANGED
@@ -113,5 +113,5 @@
113
113
  "test:cli-e2e": "bun test src/plugins/cli/__tests__/e2e/",
114
114
  "test:coverage": "vitest run --project unit --project integration --coverage"
115
115
  },
116
- "version": "1.3.1"
116
+ "version": "1.4.0"
117
117
  }