@moku-labs/web 1.12.4 → 1.13.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.
Files changed (3) hide show
  1. package/dist/index.cjs +32 -326
  2. package/dist/index.mjs +10 -304
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -15,6 +15,7 @@ let preact_render_to_string = require("preact-render-to-string");
15
15
  let node_child_process = require("node:child_process");
16
16
  let node_readline = require("node:readline");
17
17
  let node_url = require("node:url");
18
+ let _moku_labs_common_cli = require("@moku-labs/common/cli");
18
19
  let gray_matter = require("gray-matter");
19
20
  gray_matter = require_convention.__toESM(gray_matter, 1);
20
21
  let _shikijs_rehype = require("@shikijs/rehype");
@@ -8035,312 +8036,17 @@ function networkUrl(port, source = node_os.networkInterfaces) {
8035
8036
  return ip === null ? null : `http://${ip}:${port}`;
8036
8037
  }
8037
8038
  //#endregion
8038
- //#region src/plugins/cli/render/ansi.ts
8039
- /**
8040
- * @file cli plugin — TTY/NO_COLOR-aware ANSI color + box-drawing helpers shared by
8041
- * the Panel renderer. Modeled on the legacy `scripts/_log.ts`: color and box glyphs
8042
- * are emitted only on a real TTY with `NO_COLOR` unset; otherwise plain ASCII so
8043
- * CI logs and pipes stay readable.
8044
- */
8045
- /** The ANSI escape byte (ESC, `0x1b`), built so no literal control char is in source. */
8046
- const ESC = String.fromCodePoint(27);
8047
- /** ANSI SGR codes used by the Panel renderer (each prefixed with the ESC byte). */
8048
- const ANSI = {
8049
- reset: `${ESC}[0m`,
8050
- bold: `${ESC}[1m`,
8051
- dim: `${ESC}[2m`,
8052
- red: `${ESC}[31m`,
8053
- green: `${ESC}[32m`,
8054
- yellow: `${ESC}[33m`,
8055
- blue: `${ESC}[34m`,
8056
- magenta: `${ESC}[35m`,
8057
- cyan: `${ESC}[36m`,
8058
- gray: `${ESC}[90m`
8059
- };
8060
- /**
8061
- * The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
8062
- * Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
8063
- */
8064
- const BRAND_PINK = {
8065
- r: 255,
8066
- g: 30,
8067
- b: 111
8068
- };
8069
- /**
8070
- * Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
8071
- *
8072
- * @param r - Red channel (0–255).
8073
- * @param g - Green channel (0–255).
8074
- * @param b - Blue channel (0–255).
8075
- * @returns The `ESC[38;2;r;g;bm` foreground sequence.
8076
- * @example
8077
- * fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
8078
- */
8079
- function fg24(r, g, b) {
8080
- return `${ESC}[38;2;${r};${g};${b}m`;
8081
- }
8082
- /** ANSI: erase the entire current line, leaving the cursor where it is. */
8083
- const CLEAR_LINE = `${ESC}[2K`;
8084
- /** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
8085
- const CLEAR_BELOW = `${ESC}[0J`;
8086
- /**
8087
- * Braille spinner frames for live "working…" indicators on a TTY (advance one per tick).
8088
- * Off a TTY the Panel never animates, so this is unused in plain/CI output.
8089
- */
8090
- const SPINNER_FRAMES = [
8091
- "⠋",
8092
- "⠙",
8093
- "⠹",
8094
- "⠸",
8095
- "⠼",
8096
- "⠴",
8097
- "⠦",
8098
- "⠧",
8099
- "⠇",
8100
- "⠏"
8101
- ];
8102
- /**
8103
- * The ANSI sequence to move the cursor up `n` lines (empty string for `n <= 0`). The
8104
- * Panel uses it to repaint a live block in place — move up over the previous draw, then
8105
- * rewrite each row — so progress updates a fixed region instead of scrolling new lines.
8106
- *
8107
- * @param n - Number of lines to move the cursor up.
8108
- * @returns The cursor-up escape sequence, or `""` when `n <= 0`.
8109
- * @example
8110
- * cursorUp(3); // "\x1b[3A"
8111
- */
8112
- function cursorUp(n) {
8113
- return n > 0 ? `${ESC}[${n}A` : "";
8114
- }
8115
- /** Unicode rounded box glyphs used when output is a color-capable TTY. */
8116
- const UNICODE_BOX = {
8117
- topLeft: "╭",
8118
- topRight: "╮",
8119
- bottomLeft: "╰",
8120
- bottomRight: "╯",
8121
- horizontal: "─",
8122
- vertical: "│"
8123
- };
8124
- /** ASCII box glyphs used when output is piped/CI (plain mode). */
8125
- const ASCII_BOX = {
8126
- topLeft: "+",
8127
- topRight: "+",
8128
- bottomLeft: "+",
8129
- bottomRight: "+",
8130
- horizontal: "-",
8131
- vertical: "|"
8132
- };
8133
- /**
8134
- * Matches every ANSI SGR escape sequence (used to measure visible width). Built from
8135
- * the {@link ESC} byte so no literal control character appears in the source regex.
8136
- */
8137
- const ANSI_PATTERN = new RegExp(String.raw`${ESC}\[[0-9;]*m`, "g");
8138
- /**
8139
- * Whether ANSI color/box glyphs should be emitted: a TTY stream with `NO_COLOR`
8140
- * unset. Reads `process.stdout.isTTY` and `process.env.NO_COLOR` by default so the
8141
- * renderer auto-degrades in CI and pipes, exactly like the legacy logger.
8142
- *
8143
- * @param stream - Stream to probe for `isTTY` (defaults to `process.stdout`).
8144
- * @param noColor - The `NO_COLOR` value (defaults to `process.env.NO_COLOR`).
8145
- * @returns `true` when color should be used.
8146
- * @example
8147
- * supportsColor(); // true in an interactive terminal
8148
- */
8149
- function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR) {
8150
- return stream.isTTY === true && noColor === void 0;
8151
- }
8152
- /**
8153
- * Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
8154
- * renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
8155
- * `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
8156
- * is never used when color itself is disabled.
8157
- *
8158
- * @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
8159
- * @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
8160
- * @example
8161
- * supportsTruecolor("truecolor"); // true
8162
- */
8163
- function supportsTruecolor(colorTerm = process.env.COLORTERM) {
8164
- return colorTerm === "truecolor" || colorTerm === "24bit";
8165
- }
8166
- /**
8167
- * The braille spinner glyph for a given elapsed time, advancing one frame per
8168
- * `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
8169
- * keeps the spinner correct even when the animation ticker is briefly starved by a
8170
- * synchronous build phase and several ticks coalesce — the glyph still reflects real
8171
- * elapsed time instead of freezing on a stale frame.
8172
- *
8173
- * @param elapsedMs - Milliseconds since the live region opened.
8174
- * @param frameMs - Milliseconds per frame (defaults to `80`).
8175
- * @returns The active spinner glyph.
8176
- * @example
8177
- * spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
8178
- */
8179
- function spinnerFrameAt(elapsedMs, frameMs = 80) {
8180
- return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
8181
- }
8182
- /**
8183
- * Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
8184
- *
8185
- * @param color - Whether color/Unicode output is enabled.
8186
- * @returns The matching {@link BoxGlyphs} set.
8187
- * @example
8188
- * const glyphs = boxGlyphs(supportsColor());
8189
- */
8190
- function boxGlyphs(color) {
8191
- return color ? UNICODE_BOX : ASCII_BOX;
8192
- }
8193
- /**
8194
- * The visible width of a string, ignoring any ANSI escape sequences it contains.
8195
- *
8196
- * @param text - The (possibly colorized) text to measure.
8197
- * @returns The number of visible characters.
8198
- * @example
8199
- * visibleWidth(`${ANSI.red}hi${ANSI.reset}`); // 2
8200
- */
8201
- function visibleWidth(text) {
8202
- return text.replaceAll(ANSI_PATTERN, "").length;
8203
- }
8204
- /**
8205
- * Build a {@link Palette} bound to a fixed color mode. When `color` is `false` every
8206
- * helper returns its input unchanged, so the same render code path produces plain
8207
- * output in CI/pipes.
8208
- *
8209
- * @param color - Whether color is enabled (typically `supportsColor()`).
8210
- * @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
8211
- * only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
8212
- * @returns The bound color palette.
8213
- * @example
8214
- * const palette = makePalette(supportsColor(), supportsTruecolor());
8215
- * const line = palette.green("done");
8216
- */
8217
- function makePalette(color, truecolor = false) {
8218
- return {
8219
- enabled: color,
8220
- /**
8221
- * Wrap text in the given ANSI code (returns it unchanged when color is off).
8222
- *
8223
- * @param code - The ANSI SGR code to apply.
8224
- * @param text - The text to colorize.
8225
- * @returns The colorized (or unchanged) text.
8226
- * @example
8227
- * palette.paint(ANSI.green, "ok");
8228
- */
8229
- paint(code, text) {
8230
- return color ? `${code}${text}${ANSI.reset}` : text;
8231
- },
8232
- /**
8233
- * Bold the given text (no-op in plain mode).
8234
- *
8235
- * @param text - The text to embolden.
8236
- * @returns The bold (or unchanged) text.
8237
- * @example
8238
- * palette.bold("title");
8239
- */
8240
- bold(text) {
8241
- return this.paint(ANSI.bold, text);
8242
- },
8243
- /**
8244
- * Dim the given text (no-op in plain mode).
8245
- *
8246
- * @param text - The text to dim.
8247
- * @returns The dim (or unchanged) text.
8248
- * @example
8249
- * palette.dim("· 84ms");
8250
- */
8251
- dim(text) {
8252
- return this.paint(ANSI.dim, text);
8253
- },
8254
- /**
8255
- * Color the given text green (no-op in plain mode).
8256
- *
8257
- * @param text - The text to colorize.
8258
- * @returns The green (or unchanged) text.
8259
- * @example
8260
- * palette.green("✓");
8261
- */
8262
- green(text) {
8263
- return this.paint(ANSI.green, text);
8264
- },
8265
- /**
8266
- * Color the given text yellow (no-op in plain mode).
8267
- *
8268
- * @param text - The text to colorize.
8269
- * @returns The yellow (or unchanged) text.
8270
- * @example
8271
- * palette.yellow("~");
8272
- */
8273
- yellow(text) {
8274
- return this.paint(ANSI.yellow, text);
8275
- },
8276
- /**
8277
- * Color the given text red (no-op in plain mode).
8278
- *
8279
- * @param text - The text to colorize.
8280
- * @returns The red (or unchanged) text.
8281
- * @example
8282
- * palette.red("✗");
8283
- */
8284
- red(text) {
8285
- return this.paint(ANSI.red, text);
8286
- },
8287
- /**
8288
- * Color the given text cyan (no-op in plain mode).
8289
- *
8290
- * @param text - The text to colorize.
8291
- * @returns The cyan (or unchanged) text.
8292
- * @example
8293
- * palette.cyan("http://localhost:4173");
8294
- */
8295
- cyan(text) {
8296
- return this.paint(ANSI.cyan, text);
8297
- },
8298
- /**
8299
- * Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
8300
- * is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
8301
- *
8302
- * @param text - The text to colorize.
8303
- * @returns The pink (or unchanged) text.
8304
- * @example
8305
- * palette.pink("▟▙ moku web");
8306
- */
8307
- pink(text) {
8308
- if (!color) return text;
8309
- if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
8310
- return this.paint(ANSI.magenta, text);
8311
- }
8312
- };
8313
- }
8039
+ //#region src/plugins/cli/render/panel.ts
8314
8040
  /**
8315
- * Frame a list of already-rendered content lines in a box, padding each line to the
8316
- * widest visible line (or `minInnerWidth`, whichever is larger so several boxes can be
8317
- * forced to a shared width). Uses Unicode borders when `color` is enabled and ASCII
8318
- * otherwise. Visible width ignores embedded ANSI so colored lines align.
8319
- *
8320
- * @param lines - The content lines (may contain ANSI color codes).
8321
- * @param color - Whether to use Unicode borders (and assume color-capable output).
8322
- * @param minInnerWidth - Minimum inner (content) width to pad every row to. Defaults to `0`.
8323
- * @returns The boxed lines (top border, content rows, bottom border).
8324
- * @example
8325
- * box(["Local: http://localhost:4173"], true, 62);
8041
+ * @file cli plugin the Panel renderer (the "Velocity Lockup" CLI identity). Produces
8042
+ * the `▟▙ moku web` lockup + version/runtime banner, the live phase tree with an
8043
+ * animated indeterminate build bar, the BUILD summary + throughput sparkline, the
8044
+ * server-ready rail with a persistent breathing `◍ live` idle pulse, the compact
8045
+ * rebuild line, the deploy result, and diagnostic heading/check rows. TTY/`NO_COLOR`-
8046
+ * aware via {@link makePalette} (24-bit brand pink when truecolor is available, the
8047
+ * 16-color magenta approximation otherwise, plain text off a TTY); every line is
8048
+ * written through an injectable sink so tests can capture it.
8326
8049
  */
8327
- function box(lines, color, minInnerWidth = 0) {
8328
- const glyphs = boxGlyphs(color);
8329
- const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
8330
- const horizontal = glyphs.horizontal.repeat(inner + 2);
8331
- const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
8332
- const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
8333
- return [
8334
- top,
8335
- ...lines.map((line) => {
8336
- const pad = " ".repeat(inner - visibleWidth(line));
8337
- return `${glyphs.vertical} ${line}${pad} ${glyphs.vertical}`;
8338
- }),
8339
- bottom
8340
- ];
8341
- }
8342
- //#endregion
8343
- //#region src/plugins/cli/render/panel.ts
8344
8050
  /** Per-command label shown beside the lockup wordmark. */
8345
8051
  const COMMAND_LABEL = {
8346
8052
  build: "build",
@@ -8430,7 +8136,7 @@ function durationSuffix(palette, durationMs) {
8430
8136
  * railLine(" ├─ ✓ pages", "· 12ms");
8431
8137
  */
8432
8138
  function railLine(left, right, width = RAIL_WIDTH) {
8433
- const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
8139
+ const gap = Math.max(1, width - (0, _moku_labs_common_cli.visibleWidth)(left) - (0, _moku_labs_common_cli.visibleWidth)(right));
8434
8140
  return `${left}${" ".repeat(gap)}${right}`;
8435
8141
  }
8436
8142
  /**
@@ -8471,8 +8177,8 @@ function createPanelRenderer(options = {}) {
8471
8177
  process.stdout.write(chunk);
8472
8178
  });
8473
8179
  const now = options.now ?? Date.now;
8474
- const color = options.color ?? supportsColor();
8475
- const palette = makePalette(color, options.truecolor ?? (color && supportsTruecolor()));
8180
+ const color = options.color ?? (0, _moku_labs_common_cli.supportsColor)();
8181
+ const palette = (0, _moku_labs_common_cli.makePalette)(color, options.truecolor ?? (color && (0, _moku_labs_common_cli.supportsTruecolor)()));
8476
8182
  const version = options.version ?? "dev";
8477
8183
  const coreVersion = options.coreVersion;
8478
8184
  const g = glyphSet(color);
@@ -8499,7 +8205,7 @@ function createPanelRenderer(options = {}) {
8499
8205
  const renderPhaseRow = (row) => {
8500
8206
  const branch = palette.dim(g.tree);
8501
8207
  if (row.done) return railLine(` ${branch} ${palette.green("✓")} ${row.name}`, palette.dim(`· ${row.durationMs}ms`));
8502
- return ` ${branch} ${palette.cyan(spinnerFrameAt(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
8208
+ return ` ${branch} ${palette.cyan((0, _moku_labs_common_cli.spinnerFrameAt)(now() - blockStartedAt, SPIN_MS))} ${palette.dim(row.name)}`;
8503
8209
  };
8504
8210
  /**
8505
8211
  * Render the indeterminate "comet" build bar — a short pink fill window sweeping across
@@ -8530,10 +8236,10 @@ function createPanelRenderer(options = {}) {
8530
8236
  * paintPhaseBlock();
8531
8237
  */
8532
8238
  const paintPhaseBlock = () => {
8533
- let frame = cursorUp(phaseDrawn);
8534
- for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
8535
- frame += `${CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
8536
- writeRaw(frame + CLEAR_BELOW);
8239
+ let frame = (0, _moku_labs_common_cli.cursorUp)(phaseDrawn);
8240
+ for (const row of phaseRows) frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderPhaseRow(row)}\n`;
8241
+ frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
8242
+ writeRaw(frame + _moku_labs_common_cli.CLEAR_BELOW);
8537
8243
  phaseDrawn = phaseRows.length + 1;
8538
8244
  };
8539
8245
  /**
@@ -8543,9 +8249,9 @@ function createPanelRenderer(options = {}) {
8543
8249
  * paintRebuildLine();
8544
8250
  */
8545
8251
  const paintRebuildLine = () => {
8546
- const spinner = palette.cyan(spinnerFrameAt(now() - rebuildStartedAt, SPIN_MS));
8252
+ const spinner = palette.cyan((0, _moku_labs_common_cli.spinnerFrameAt)(now() - rebuildStartedAt, SPIN_MS));
8547
8253
  const elapsed = palette.dim(`· ${((now() - rebuildStartedAt) / 1e3).toFixed(1)}s`);
8548
- writeRaw(`\r${CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
8254
+ writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
8549
8255
  };
8550
8256
  /**
8551
8257
  * Repaint the persistent in-place `◍ live` idle pulse beneath the serve panel — the
@@ -8556,7 +8262,7 @@ function createPanelRenderer(options = {}) {
8556
8262
  * paintIdleLine();
8557
8263
  */
8558
8264
  const paintIdleLine = () => {
8559
- writeRaw(`\r${CLEAR_LINE} ${Math.floor((now() - idleStartedAt) / 450) % 2 === 0 ? palette.pink(g.liveOn) : palette.dim(g.liveOff)} ${palette.dim("live · waiting for changes…")}`);
8265
+ writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE} ${Math.floor((now() - idleStartedAt) / 450) % 2 === 0 ? palette.pink(g.liveOn) : palette.dim(g.liveOff)} ${palette.dim("live · waiting for changes…")}`);
8560
8266
  };
8561
8267
  /**
8562
8268
  * Advance whichever live region is active by one frame (driven by the shared ticker).
@@ -8683,9 +8389,9 @@ function createPanelRenderer(options = {}) {
8683
8389
  built(summary) {
8684
8390
  if (rebuilding) return;
8685
8391
  if (color && phaseOpen) {
8686
- let frame = cursorUp(phaseDrawn);
8687
- for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
8688
- writeRaw(frame + CLEAR_BELOW);
8392
+ let frame = (0, _moku_labs_common_cli.cursorUp)(phaseDrawn);
8393
+ for (const row of phaseRows) frame += `${_moku_labs_common_cli.CLEAR_LINE}${renderPhaseRow(row)}\n`;
8394
+ writeRaw(frame + _moku_labs_common_cli.CLEAR_BELOW);
8689
8395
  }
8690
8396
  const phaseDurations = phaseRows.map((row) => row.durationMs).filter((value) => value !== void 0);
8691
8397
  phaseOpen = false;
@@ -8700,7 +8406,7 @@ function createPanelRenderer(options = {}) {
8700
8406
  const rateLabel = palette.dim(`${rate} pages/s`);
8701
8407
  lines.push(railLine(spark, rateLabel, BOX_INNER));
8702
8408
  }
8703
- writeBlock(box(lines, color, BOX_INNER));
8409
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
8704
8410
  },
8705
8411
  /**
8706
8412
  * Render the server-ready rail (Local / Network URLs + watched dirs) and, on a TTY,
@@ -8714,7 +8420,7 @@ function createPanelRenderer(options = {}) {
8714
8420
  const network = info.network ? palette.cyan(info.network) : palette.dim("unavailable");
8715
8421
  const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${network}`];
8716
8422
  if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
8717
- writeBlock(box(lines, color, BOX_INNER));
8423
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
8718
8424
  if (color) {
8719
8425
  serveMode = true;
8720
8426
  idle = true;
@@ -8760,7 +8466,7 @@ function createPanelRenderer(options = {}) {
8760
8466
  rebuilding = false;
8761
8467
  const line = ` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · reloaded`)}`;
8762
8468
  if (settledRebuild && color) {
8763
- writeRaw(`\r${CLEAR_LINE}${line}\n`);
8469
+ writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}${line}\n`);
8764
8470
  resumeIdle();
8765
8471
  return;
8766
8472
  }
@@ -8785,7 +8491,7 @@ function createPanelRenderer(options = {}) {
8785
8491
  const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
8786
8492
  lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
8787
8493
  } else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
8788
- writeBlock(box(lines, color, BOX_INNER));
8494
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
8789
8495
  },
8790
8496
  /**
8791
8497
  * Render a neutral informational line.
@@ -8822,7 +8528,7 @@ function createPanelRenderer(options = {}) {
8822
8528
  const wasRebuilding = rebuilding;
8823
8529
  if (rebuilding) {
8824
8530
  rebuilding = false;
8825
- if (color) writeRaw(`\r${CLEAR_LINE}`);
8531
+ if (color) writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}`);
8826
8532
  }
8827
8533
  writeError(` ${palette.red("✗")} ${message}`);
8828
8534
  if (cause !== void 0) writeError(String(cause));
@@ -8883,9 +8589,9 @@ const YES_PATTERN = /^y(es)?$/i;
8883
8589
  /** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
8884
8590
  const PROMPT_WIDTH = 66;
8885
8591
  /** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
8886
- const PROMPT_COLOR = supportsColor();
8592
+ const PROMPT_COLOR = (0, _moku_labs_common_cli.supportsColor)();
8887
8593
  /** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
8888
- const PROMPT_PALETTE = makePalette(PROMPT_COLOR, PROMPT_COLOR && supportsTruecolor());
8594
+ const PROMPT_PALETTE = (0, _moku_labs_common_cli.makePalette)(PROMPT_COLOR, PROMPT_COLOR && (0, _moku_labs_common_cli.supportsTruecolor)());
8889
8595
  /**
8890
8596
  * Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
8891
8597
  * a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
@@ -8900,7 +8606,7 @@ function confirmPrompt(question) {
8900
8606
  if (!PROMPT_COLOR) return `${question} [y/N] `;
8901
8607
  const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
8902
8608
  const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
8903
- const gap = Math.max(1, PROMPT_WIDTH - visibleWidth(left) - visibleWidth(right));
8609
+ const gap = Math.max(1, PROMPT_WIDTH - (0, _moku_labs_common_cli.visibleWidth)(left) - (0, _moku_labs_common_cli.visibleWidth)(right));
8904
8610
  return `${left}${" ".repeat(gap)}${right}`;
8905
8611
  }
8906
8612
  /**
package/dist/index.mjs CHANGED
@@ -13,6 +13,7 @@ import { renderToString } from "preact-render-to-string";
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { createInterface } from "node:readline";
15
15
  import { fileURLToPath, fileURLToPath as urlToPath } from "node:url";
16
+ import { CLEAR_BELOW, CLEAR_LINE, box, cursorUp, makePalette, spinnerFrameAt, supportsColor, supportsTruecolor, visibleWidth } from "@moku-labs/common/cli";
16
17
  import minproc from "node:process";
17
18
  import matter from "gray-matter";
18
19
  import rehypeShiki from "@shikijs/rehype";
@@ -8022,312 +8023,17 @@ function networkUrl(port, source = networkInterfaces) {
8022
8023
  return ip === null ? null : `http://${ip}:${port}`;
8023
8024
  }
8024
8025
  //#endregion
8025
- //#region src/plugins/cli/render/ansi.ts
8026
- /**
8027
- * @file cli plugin — TTY/NO_COLOR-aware ANSI color + box-drawing helpers shared by
8028
- * the Panel renderer. Modeled on the legacy `scripts/_log.ts`: color and box glyphs
8029
- * are emitted only on a real TTY with `NO_COLOR` unset; otherwise plain ASCII so
8030
- * CI logs and pipes stay readable.
8031
- */
8032
- /** The ANSI escape byte (ESC, `0x1b`), built so no literal control char is in source. */
8033
- const ESC = String.fromCodePoint(27);
8034
- /** ANSI SGR codes used by the Panel renderer (each prefixed with the ESC byte). */
8035
- const ANSI = {
8036
- reset: `${ESC}[0m`,
8037
- bold: `${ESC}[1m`,
8038
- dim: `${ESC}[2m`,
8039
- red: `${ESC}[31m`,
8040
- green: `${ESC}[32m`,
8041
- yellow: `${ESC}[33m`,
8042
- blue: `${ESC}[34m`,
8043
- magenta: `${ESC}[35m`,
8044
- cyan: `${ESC}[36m`,
8045
- gray: `${ESC}[90m`
8046
- };
8047
- /**
8048
- * The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
8049
- * Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
8050
- */
8051
- const BRAND_PINK = {
8052
- r: 255,
8053
- g: 30,
8054
- b: 111
8055
- };
8056
- /**
8057
- * Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
8058
- *
8059
- * @param r - Red channel (0–255).
8060
- * @param g - Green channel (0–255).
8061
- * @param b - Blue channel (0–255).
8062
- * @returns The `ESC[38;2;r;g;bm` foreground sequence.
8063
- * @example
8064
- * fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
8065
- */
8066
- function fg24(r, g, b) {
8067
- return `${ESC}[38;2;${r};${g};${b}m`;
8068
- }
8069
- /** ANSI: erase the entire current line, leaving the cursor where it is. */
8070
- const CLEAR_LINE = `${ESC}[2K`;
8071
- /** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
8072
- const CLEAR_BELOW = `${ESC}[0J`;
8073
- /**
8074
- * Braille spinner frames for live "working…" indicators on a TTY (advance one per tick).
8075
- * Off a TTY the Panel never animates, so this is unused in plain/CI output.
8076
- */
8077
- const SPINNER_FRAMES = [
8078
- "⠋",
8079
- "⠙",
8080
- "⠹",
8081
- "⠸",
8082
- "⠼",
8083
- "⠴",
8084
- "⠦",
8085
- "⠧",
8086
- "⠇",
8087
- "⠏"
8088
- ];
8089
- /**
8090
- * The ANSI sequence to move the cursor up `n` lines (empty string for `n <= 0`). The
8091
- * Panel uses it to repaint a live block in place — move up over the previous draw, then
8092
- * rewrite each row — so progress updates a fixed region instead of scrolling new lines.
8093
- *
8094
- * @param n - Number of lines to move the cursor up.
8095
- * @returns The cursor-up escape sequence, or `""` when `n <= 0`.
8096
- * @example
8097
- * cursorUp(3); // "\x1b[3A"
8098
- */
8099
- function cursorUp(n) {
8100
- return n > 0 ? `${ESC}[${n}A` : "";
8101
- }
8102
- /** Unicode rounded box glyphs used when output is a color-capable TTY. */
8103
- const UNICODE_BOX = {
8104
- topLeft: "╭",
8105
- topRight: "╮",
8106
- bottomLeft: "╰",
8107
- bottomRight: "╯",
8108
- horizontal: "─",
8109
- vertical: "│"
8110
- };
8111
- /** ASCII box glyphs used when output is piped/CI (plain mode). */
8112
- const ASCII_BOX = {
8113
- topLeft: "+",
8114
- topRight: "+",
8115
- bottomLeft: "+",
8116
- bottomRight: "+",
8117
- horizontal: "-",
8118
- vertical: "|"
8119
- };
8120
- /**
8121
- * Matches every ANSI SGR escape sequence (used to measure visible width). Built from
8122
- * the {@link ESC} byte so no literal control character appears in the source regex.
8123
- */
8124
- const ANSI_PATTERN = new RegExp(String.raw`${ESC}\[[0-9;]*m`, "g");
8125
- /**
8126
- * Whether ANSI color/box glyphs should be emitted: a TTY stream with `NO_COLOR`
8127
- * unset. Reads `process.stdout.isTTY` and `process.env.NO_COLOR` by default so the
8128
- * renderer auto-degrades in CI and pipes, exactly like the legacy logger.
8129
- *
8130
- * @param stream - Stream to probe for `isTTY` (defaults to `process.stdout`).
8131
- * @param noColor - The `NO_COLOR` value (defaults to `process.env.NO_COLOR`).
8132
- * @returns `true` when color should be used.
8133
- * @example
8134
- * supportsColor(); // true in an interactive terminal
8135
- */
8136
- function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR) {
8137
- return stream.isTTY === true && noColor === void 0;
8138
- }
8139
- /**
8140
- * Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
8141
- * renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
8142
- * `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
8143
- * is never used when color itself is disabled.
8144
- *
8145
- * @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
8146
- * @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
8147
- * @example
8148
- * supportsTruecolor("truecolor"); // true
8149
- */
8150
- function supportsTruecolor(colorTerm = process.env.COLORTERM) {
8151
- return colorTerm === "truecolor" || colorTerm === "24bit";
8152
- }
8153
- /**
8154
- * The braille spinner glyph for a given elapsed time, advancing one frame per
8155
- * `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
8156
- * keeps the spinner correct even when the animation ticker is briefly starved by a
8157
- * synchronous build phase and several ticks coalesce — the glyph still reflects real
8158
- * elapsed time instead of freezing on a stale frame.
8159
- *
8160
- * @param elapsedMs - Milliseconds since the live region opened.
8161
- * @param frameMs - Milliseconds per frame (defaults to `80`).
8162
- * @returns The active spinner glyph.
8163
- * @example
8164
- * spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
8165
- */
8166
- function spinnerFrameAt(elapsedMs, frameMs = 80) {
8167
- return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
8168
- }
8169
- /**
8170
- * Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
8171
- *
8172
- * @param color - Whether color/Unicode output is enabled.
8173
- * @returns The matching {@link BoxGlyphs} set.
8174
- * @example
8175
- * const glyphs = boxGlyphs(supportsColor());
8176
- */
8177
- function boxGlyphs(color) {
8178
- return color ? UNICODE_BOX : ASCII_BOX;
8179
- }
8180
- /**
8181
- * The visible width of a string, ignoring any ANSI escape sequences it contains.
8182
- *
8183
- * @param text - The (possibly colorized) text to measure.
8184
- * @returns The number of visible characters.
8185
- * @example
8186
- * visibleWidth(`${ANSI.red}hi${ANSI.reset}`); // 2
8187
- */
8188
- function visibleWidth(text) {
8189
- return text.replaceAll(ANSI_PATTERN, "").length;
8190
- }
8191
- /**
8192
- * Build a {@link Palette} bound to a fixed color mode. When `color` is `false` every
8193
- * helper returns its input unchanged, so the same render code path produces plain
8194
- * output in CI/pipes.
8195
- *
8196
- * @param color - Whether color is enabled (typically `supportsColor()`).
8197
- * @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
8198
- * only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
8199
- * @returns The bound color palette.
8200
- * @example
8201
- * const palette = makePalette(supportsColor(), supportsTruecolor());
8202
- * const line = palette.green("done");
8203
- */
8204
- function makePalette(color, truecolor = false) {
8205
- return {
8206
- enabled: color,
8207
- /**
8208
- * Wrap text in the given ANSI code (returns it unchanged when color is off).
8209
- *
8210
- * @param code - The ANSI SGR code to apply.
8211
- * @param text - The text to colorize.
8212
- * @returns The colorized (or unchanged) text.
8213
- * @example
8214
- * palette.paint(ANSI.green, "ok");
8215
- */
8216
- paint(code, text) {
8217
- return color ? `${code}${text}${ANSI.reset}` : text;
8218
- },
8219
- /**
8220
- * Bold the given text (no-op in plain mode).
8221
- *
8222
- * @param text - The text to embolden.
8223
- * @returns The bold (or unchanged) text.
8224
- * @example
8225
- * palette.bold("title");
8226
- */
8227
- bold(text) {
8228
- return this.paint(ANSI.bold, text);
8229
- },
8230
- /**
8231
- * Dim the given text (no-op in plain mode).
8232
- *
8233
- * @param text - The text to dim.
8234
- * @returns The dim (or unchanged) text.
8235
- * @example
8236
- * palette.dim("· 84ms");
8237
- */
8238
- dim(text) {
8239
- return this.paint(ANSI.dim, text);
8240
- },
8241
- /**
8242
- * Color the given text green (no-op in plain mode).
8243
- *
8244
- * @param text - The text to colorize.
8245
- * @returns The green (or unchanged) text.
8246
- * @example
8247
- * palette.green("✓");
8248
- */
8249
- green(text) {
8250
- return this.paint(ANSI.green, text);
8251
- },
8252
- /**
8253
- * Color the given text yellow (no-op in plain mode).
8254
- *
8255
- * @param text - The text to colorize.
8256
- * @returns The yellow (or unchanged) text.
8257
- * @example
8258
- * palette.yellow("~");
8259
- */
8260
- yellow(text) {
8261
- return this.paint(ANSI.yellow, text);
8262
- },
8263
- /**
8264
- * Color the given text red (no-op in plain mode).
8265
- *
8266
- * @param text - The text to colorize.
8267
- * @returns The red (or unchanged) text.
8268
- * @example
8269
- * palette.red("✗");
8270
- */
8271
- red(text) {
8272
- return this.paint(ANSI.red, text);
8273
- },
8274
- /**
8275
- * Color the given text cyan (no-op in plain mode).
8276
- *
8277
- * @param text - The text to colorize.
8278
- * @returns The cyan (or unchanged) text.
8279
- * @example
8280
- * palette.cyan("http://localhost:4173");
8281
- */
8282
- cyan(text) {
8283
- return this.paint(ANSI.cyan, text);
8284
- },
8285
- /**
8286
- * Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
8287
- * is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
8288
- *
8289
- * @param text - The text to colorize.
8290
- * @returns The pink (or unchanged) text.
8291
- * @example
8292
- * palette.pink("▟▙ moku web");
8293
- */
8294
- pink(text) {
8295
- if (!color) return text;
8296
- if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
8297
- return this.paint(ANSI.magenta, text);
8298
- }
8299
- };
8300
- }
8026
+ //#region src/plugins/cli/render/panel.ts
8301
8027
  /**
8302
- * Frame a list of already-rendered content lines in a box, padding each line to the
8303
- * widest visible line (or `minInnerWidth`, whichever is larger so several boxes can be
8304
- * forced to a shared width). Uses Unicode borders when `color` is enabled and ASCII
8305
- * otherwise. Visible width ignores embedded ANSI so colored lines align.
8306
- *
8307
- * @param lines - The content lines (may contain ANSI color codes).
8308
- * @param color - Whether to use Unicode borders (and assume color-capable output).
8309
- * @param minInnerWidth - Minimum inner (content) width to pad every row to. Defaults to `0`.
8310
- * @returns The boxed lines (top border, content rows, bottom border).
8311
- * @example
8312
- * box(["Local: http://localhost:4173"], true, 62);
8028
+ * @file cli plugin the Panel renderer (the "Velocity Lockup" CLI identity). Produces
8029
+ * the `▟▙ moku web` lockup + version/runtime banner, the live phase tree with an
8030
+ * animated indeterminate build bar, the BUILD summary + throughput sparkline, the
8031
+ * server-ready rail with a persistent breathing `◍ live` idle pulse, the compact
8032
+ * rebuild line, the deploy result, and diagnostic heading/check rows. TTY/`NO_COLOR`-
8033
+ * aware via {@link makePalette} (24-bit brand pink when truecolor is available, the
8034
+ * 16-color magenta approximation otherwise, plain text off a TTY); every line is
8035
+ * written through an injectable sink so tests can capture it.
8313
8036
  */
8314
- function box(lines, color, minInnerWidth = 0) {
8315
- const glyphs = boxGlyphs(color);
8316
- const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
8317
- const horizontal = glyphs.horizontal.repeat(inner + 2);
8318
- const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
8319
- const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
8320
- return [
8321
- top,
8322
- ...lines.map((line) => {
8323
- const pad = " ".repeat(inner - visibleWidth(line));
8324
- return `${glyphs.vertical} ${line}${pad} ${glyphs.vertical}`;
8325
- }),
8326
- bottom
8327
- ];
8328
- }
8329
- //#endregion
8330
- //#region src/plugins/cli/render/panel.ts
8331
8037
  /** Per-command label shown beside the lockup wordmark. */
8332
8038
  const COMMAND_LABEL = {
8333
8039
  build: "build",
package/package.json CHANGED
@@ -58,7 +58,7 @@
58
58
  "bun": ">=1.3.14"
59
59
  },
60
60
  "dependencies": {
61
- "@moku-labs/common": "0.1.1",
61
+ "@moku-labs/common": "0.2.0",
62
62
  "@moku-labs/core": "0.1.4",
63
63
  "@resvg/resvg-js": "2.6.2",
64
64
  "@shikijs/rehype": "3.22.0",
@@ -126,5 +126,5 @@
126
126
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
127
127
  "test:coverage": "vitest run --project unit --project integration --coverage"
128
128
  },
129
- "version": "1.12.4"
129
+ "version": "1.13.0"
130
130
  }