@moku-labs/web 1.12.3 → 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.
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_convention = require("./convention-BpDfzX7e.cjs");
3
+ let _moku_labs_common = require("@moku-labs/common");
3
4
  let _moku_labs_core = require("@moku-labs/core");
4
5
  let node_fs = require("node:fs");
5
6
  let node_crypto = require("node:crypto");
@@ -14,6 +15,7 @@ let preact_render_to_string = require("preact-render-to-string");
14
15
  let node_child_process = require("node:child_process");
15
16
  let node_readline = require("node:readline");
16
17
  let node_url = require("node:url");
18
+ let _moku_labs_common_cli = require("@moku-labs/common/cli");
17
19
  let gray_matter = require("gray-matter");
18
20
  gray_matter = require_convention.__toESM(gray_matter, 1);
19
21
  let _shikijs_rehype = require("@shikijs/rehype");
@@ -41,831 +43,6 @@ let hast_util_sanitize = require("hast-util-sanitize");
41
43
  let reading_time = require("reading-time");
42
44
  reading_time = require_convention.__toESM(reading_time, 1);
43
45
  let node_process = require("node:process");
44
- //#region src/plugins/env/api.ts
45
- /** Error prefix for all env API failures. */
46
- const ERROR_PREFIX$16 = "[web]";
47
- /**
48
- * Creates the env plugin API surface mounted at `ctx.env`. Closes over
49
- * `ctx.state` ({@link EnvState}) and reads the frozen `resolved` / `publicMap`
50
- * maps; closures never return a raw `ctx.state` reference.
51
- *
52
- * @param ctx - Core plugin context carrying the frozen env state.
53
- * @param ctx.state - The resolved + public {@link EnvState} maps.
54
- * @returns The {@link EnvApi} accessor surface mounted at `ctx.env`.
55
- * @example
56
- * ```ts
57
- * const api = createEnvApi(ctx);
58
- * api.get("PUBLIC_API_URL");
59
- * ```
60
- */
61
- function createEnvApi(ctx) {
62
- const { resolved, publicMap } = ctx.state;
63
- return {
64
- /**
65
- * Reads a resolved variable.
66
- *
67
- * @param key - Variable name.
68
- * @returns The value, or `undefined` if not present.
69
- * @example
70
- * ```ts
71
- * api.get("PUBLIC_API_URL");
72
- * ```
73
- */
74
- get(key) {
75
- return resolved.get(key);
76
- },
77
- /**
78
- * Reads a variable that must exist.
79
- *
80
- * @param key - Variable name.
81
- * @returns The value.
82
- * @throws {Error} If the variable is undefined.
83
- * @example
84
- * ```ts
85
- * api.require("DEPLOY_TOKEN");
86
- * ```
87
- */
88
- require(key) {
89
- const value = resolved.get(key);
90
- if (value === void 0) throw new Error(`${ERROR_PREFIX$16} env: required variable "${key}" is not defined.`);
91
- return value;
92
- },
93
- /**
94
- * Tests presence of a resolved variable.
95
- *
96
- * @param key - Variable name.
97
- * @returns `true` if a value is present.
98
- * @example
99
- * ```ts
100
- * api.has("PUBLIC_API_URL");
101
- * ```
102
- */
103
- has(key) {
104
- return resolved.has(key);
105
- },
106
- /**
107
- * Returns all public variables as a frozen plain object — a fresh copy,
108
- * never the raw state map.
109
- *
110
- * @returns A frozen `Record` of public variable names to values.
111
- * @example
112
- * ```ts
113
- * const payload = { ...api.getPublic() };
114
- * ```
115
- */
116
- getPublic() {
117
- return Object.freeze(Object.fromEntries(publicMap));
118
- },
119
- /**
120
- * Returns the already-frozen map of public variables.
121
- *
122
- * @returns The frozen public map.
123
- * @example
124
- * ```ts
125
- * [...api.getPublicMap()];
126
- * ```
127
- */
128
- getPublicMap() {
129
- return publicMap;
130
- }
131
- };
132
- }
133
- //#endregion
134
- //#region src/plugins/env/state.ts
135
- /**
136
- * Creates initial env plugin state: two empty, mutable maps that are populated
137
- * and frozen by `validateSchema` (the `onInit`) at `createApp` time.
138
- *
139
- * @returns A fresh `EnvState` with empty `resolved` and `publicMap` maps.
140
- * @example
141
- * ```ts
142
- * const state = createEnvState();
143
- * state.resolved.size; // 0
144
- * ```
145
- */
146
- function createEnvState() {
147
- return {
148
- resolved: /* @__PURE__ */ new Map(),
149
- publicMap: /* @__PURE__ */ new Map()
150
- };
151
- }
152
- //#endregion
153
- //#region src/plugins/env/validate.ts
154
- /** Error message thrown by every frozen-map mutator. */
155
- const FROZEN_MESSAGE = "env: map is frozen and cannot be mutated";
156
- /** Error prefix for all resolution-pipeline failures. */
157
- const ERROR_PREFIX$15 = "[web]";
158
- /** The `Map` mutators redefined as throwers when a map is frozen. */
159
- const FROZEN_METHODS = [
160
- "set",
161
- "clear",
162
- "delete"
163
- ];
164
- /**
165
- * Throws the canonical frozen-map error; installed as a map's `set`/`clear`/`delete`.
166
- *
167
- * @throws {TypeError} Always, signalling the map is frozen.
168
- * @example
169
- * ```ts
170
- * frozenThrower(); // throws TypeError
171
- * ```
172
- */
173
- function frozenThrower() {
174
- throw new TypeError(FROZEN_MESSAGE);
175
- }
176
- /**
177
- * Coerces a raw provider value to its effective presence: an empty string counts
178
- * as "absent" so a `KEY=""` falls through to later providers.
179
- *
180
- * @param raw - The raw value a provider supplied for a key (possibly `undefined`).
181
- * @returns The value, or `undefined` when it is missing or an empty string.
182
- * @example
183
- * ```ts
184
- * coerceEmpty(""); // => undefined
185
- * coerceEmpty("3000"); // => "3000"
186
- * ```
187
- */
188
- function coerceEmpty(raw) {
189
- return raw === "" ? void 0 : raw;
190
- }
191
- /**
192
- * Merges providers in array order, coercing empty strings to `undefined` before
193
- * precedence so a `KEY=""` falls through to later providers. First non-empty
194
- * value wins.
195
- *
196
- * @param config - The resolved env config carrying the ordered providers.
197
- * @returns A flat record of the first defined value found per key.
198
- * @example
199
- * ```ts
200
- * mergeProviders({ providers: [a, b], schema: {}, publicPrefix: "PUBLIC_" });
201
- * ```
202
- */
203
- function mergeProviders$1(config) {
204
- const merged = {};
205
- for (const provider of config.providers) for (const [key, raw] of Object.entries(provider.load())) {
206
- const value = coerceEmpty(raw);
207
- if (value !== void 0 && merged[key] === void 0) merged[key] = value;
208
- }
209
- return merged;
210
- }
211
- /**
212
- * Bidirectionally enforces the `PUBLIC_` naming convention against each schema
213
- * entry's `public` flag. Throws on either violation direction.
214
- *
215
- * @param config - The resolved env config carrying `schema` + `publicPrefix`.
216
- * @throws {Error} If a public var lacks the prefix, or a prefixed var is not public.
217
- * @example
218
- * ```ts
219
- * crossCheckPublicPrefix(config); // throws if PUBLIC_X is not public:true
220
- * ```
221
- */
222
- function crossCheckPublicPrefix(config) {
223
- const { schema, publicPrefix } = config;
224
- for (const [key, spec] of Object.entries(schema)) {
225
- const hasPrefix = key.startsWith(publicPrefix);
226
- if (spec.public === true && !hasPrefix) throw new Error(`${ERROR_PREFIX$15} env: "${key}" is marked public but does not start with "${publicPrefix}".`);
227
- if (hasPrefix && spec.public !== true) throw new Error(`${ERROR_PREFIX$15} env: "${key}" starts with "${publicPrefix}" but is not marked public:true.`);
228
- }
229
- }
230
- /**
231
- * Seals a map so `set`, `clear`, and `delete` throw, then `Object.freeze`s it
232
- * for defense in depth. Closes the `Object.freeze`-on-`Map` mutability hole by
233
- * redefining the mutators as non-writable, non-configurable throwers.
234
- *
235
- * @param map - The map to freeze in place.
236
- * @example
237
- * ```ts
238
- * freezeMap(state.resolved); // resolved.set(...) now throws
239
- * ```
240
- */
241
- function freezeMap(map) {
242
- for (const method of FROZEN_METHODS) Object.defineProperty(map, method, {
243
- value: frozenThrower,
244
- writable: false,
245
- configurable: false,
246
- enumerable: false
247
- });
248
- Object.freeze(map);
249
- }
250
- /**
251
- * Populates `state.publicMap` with the schema-driven public subset: every
252
- * `public:true` schema key that resolved to a defined value. This map is the only
253
- * sanctioned input to a browser-facing `define`, so it stays schema-scoped (never
254
- * includes non-schema provider keys).
255
- *
256
- * @param schema - The per-variable schema from {@link EnvConfig}.
257
- * @param merged - The merged provider values keyed by variable name.
258
- * @param publicMap - The mutable public map to fill in place.
259
- * @example
260
- * ```ts
261
- * populatePublicMap(config.schema, merged, state.publicMap);
262
- * ```
263
- */
264
- function populatePublicMap(schema, merged, publicMap) {
265
- for (const [key, spec] of Object.entries(schema)) {
266
- const value = merged[key];
267
- if (spec.public === true && value !== void 0) publicMap.set(key, value);
268
- }
269
- }
270
- /**
271
- * Populates `state.resolved` with EVERY merged key that carries a defined value
272
- * (spec/02 Lifecycle §5), including non-schema provider keys so
273
- * `ctx.env.require()` works for dynamic keys.
274
- *
275
- * @param merged - The merged provider values keyed by variable name.
276
- * @param resolved - The mutable resolved map to fill in place.
277
- * @example
278
- * ```ts
279
- * populateResolved(merged, state.resolved);
280
- * ```
281
- */
282
- function populateResolved(merged, resolved) {
283
- for (const [key, value] of Object.entries(merged)) resolved.set(key, value);
284
- }
285
- /**
286
- * Resolves, validates, and freezes the environment table at `onInit`.
287
- *
288
- * Pipeline order: merge providers (with empty-string → undefined coercion) →
289
- * `PUBLIC_` bidirectional cross-check → apply defaults → assert required →
290
- * populate `state.resolved` / `state.publicMap` → freeze both via
291
- * {@link freezeMap}. Fail-fast: any violation throws at `createApp` time.
292
- *
293
- * @param ctx - Core plugin context (`{ config, state }`).
294
- * @param ctx.config - The resolved {@link EnvConfig}.
295
- * @param ctx.state - The mutable {@link EnvState} to populate and freeze.
296
- * @throws {Error} On a `PUBLIC_` cross-check violation or a missing required variable.
297
- * @example
298
- * ```ts
299
- * validateSchema(ctx); // throws on missing required / PUBLIC_ violation
300
- * ```
301
- */
302
- function validateSchema(ctx) {
303
- const { config, state } = ctx;
304
- const { schema } = config;
305
- const merged = mergeProviders$1(config);
306
- crossCheckPublicPrefix(config);
307
- for (const [key, spec] of Object.entries(schema)) {
308
- if (merged[key] === void 0 && spec.default !== void 0) merged[key] = spec.default;
309
- if (merged[key] === void 0 && spec.required === true) throw new Error(`${ERROR_PREFIX$15} env: required variable "${key}" is not defined by any provider or default.`);
310
- }
311
- populatePublicMap(schema, merged, state.publicMap);
312
- populateResolved(merged, state.resolved);
313
- freezeMap(state.resolved);
314
- freezeMap(state.publicMap);
315
- }
316
- //#endregion
317
- //#region src/plugins/env/providers.browser.ts
318
- /** Default `globalThis` property holding a runtime-injected public-env snapshot. */
319
- const DEFAULT_GLOBAL_KEY = "__ENV__";
320
- /**
321
- * A browser-safe {@link EnvProvider} that reads `import.meta.env` and an optional
322
- * `globalThis[globalKey]` snapshot, merging them with the runtime global winning.
323
- * Contains zero `node:*` imports, so it is safe to include in the client bundle.
324
- * Never throws on missing sources — each absent source resolves to `{}`.
325
- *
326
- * @param options - Optional settings.
327
- * @param options.globalKey - `globalThis` key to read a public-env snapshot from. Defaults to `"__ENV__"`.
328
- * @returns An {@link EnvProvider} named `browser-env`.
329
- * @example
330
- * ```ts
331
- * const provider = browserEnv();
332
- * provider.load(); // { PUBLIC_API_URL: "/api", ... }
333
- * ```
334
- */
335
- function browserEnv(options) {
336
- const globalKey = options?.globalKey ?? DEFAULT_GLOBAL_KEY;
337
- return {
338
- name: "browser-env",
339
- /**
340
- * Merges `import.meta.env` with `globalThis[globalKey]`, the runtime global
341
- * winning. Each absent source resolves to `{}`; never throws.
342
- *
343
- * @returns The merged environment record.
344
- * @example
345
- * ```ts
346
- * browserEnv().load();
347
- * ```
348
- */
349
- load() {
350
- const importEnv = {}.env ?? {};
351
- const globalObject = globalThis[globalKey] ?? {};
352
- return {
353
- ...importEnv,
354
- ...globalObject
355
- };
356
- }
357
- };
358
- }
359
- /**
360
- * Core plugin that resolves, validates, and freezes the environment at `onInit`,
361
- * exposing a read-only accessor at `ctx.env`. No `onStart`/`onStop` — holds no resource.
362
- *
363
- * @example
364
- * ```ts
365
- * createApp({ pluginConfigs: { env: { schema: { PUBLIC_API_URL: { public: true } } } } });
366
- * ```
367
- */
368
- const envPlugin = (0, _moku_labs_core.createCorePlugin)("env", {
369
- config: {
370
- schema: {},
371
- providers: [],
372
- publicPrefix: "PUBLIC_"
373
- },
374
- createState: createEnvState,
375
- api: createEnvApi,
376
- onInit: validateSchema
377
- });
378
- //#endregion
379
- //#region src/plugins/log/expect.ts
380
- /**
381
- * Named error thrown by `expect()` assertions when a trace condition fails.
382
- *
383
- * @example
384
- * ```ts
385
- * throw new LogExpectAssertionError("missing event build:complete");
386
- * ```
387
- */
388
- var LogExpectAssertionError = class extends Error {
389
- /**
390
- * Construct a new assertion error with a descriptive failure message.
391
- *
392
- * @param message - Descriptive failure message (event name, partial, index).
393
- * @example
394
- * ```ts
395
- * throw new LogExpectAssertionError("missing event build:complete");
396
- * ```
397
- */
398
- constructor(message) {
399
- super(message);
400
- this.name = "LogExpectAssertionError";
401
- }
402
- };
403
- /**
404
- * Tests whether a value is a non-null, non-array plain object.
405
- *
406
- * @param value - The value to test.
407
- * @returns `true` when `value` is a non-null object that is not an array.
408
- * @example
409
- * ```ts
410
- * isPlainObject({ a: 1 }); // true
411
- * isPlainObject([1]); // false
412
- * ```
413
- */
414
- function isPlainObject$1(value) {
415
- return typeof value === "object" && value !== null && !Array.isArray(value);
416
- }
417
- /**
418
- * Tests whether `actual` is an array that recursively matches every element of
419
- * the `partial` array (element-wise, with equal length).
420
- *
421
- * @param actual - The value to test against (must be an array of equal length).
422
- * @param partial - The expected partial array shape.
423
- * @returns `true` when `actual` is an equal-length array matching `partial` element-wise.
424
- * @example
425
- * ```ts
426
- * matchesPartialArray([1, 2], [1, 2]); // true
427
- * matchesPartialArray([1], [1, 2]); // false (length mismatch)
428
- * ```
429
- */
430
- function matchesPartialArray(actual, partial) {
431
- if (!Array.isArray(actual) || actual.length !== partial.length) return false;
432
- return partial.every((value, index) => matchesPartial(actual[index], value));
433
- }
434
- /**
435
- * Tests whether `actual` is a plain object in which every `partial` key
436
- * recursively matches (extra `actual` keys are ignored).
437
- *
438
- * @param actual - The value to test against (must be a plain object).
439
- * @param partial - The expected partial object shape.
440
- * @returns `true` when every `partial` key exists in `actual` and matches recursively.
441
- * @example
442
- * ```ts
443
- * matchesPartialObject({ a: 1, b: 2 }, { a: 1 }); // true
444
- * matchesPartialObject({ a: 1 }, { b: 1 }); // false (missing key)
445
- * ```
446
- */
447
- function matchesPartialObject(actual, partial) {
448
- if (!isPlainObject$1(actual)) return false;
449
- return Object.keys(partial).every((key) => key in actual && matchesPartial(actual[key], partial[key]));
450
- }
451
- /**
452
- * Subset-equality matcher: is `partial` a recursive subset of `actual`?
453
- *
454
- * Fast path via `Object.is` (covers identical primitives/references and
455
- * `null`/`NaN`); primitives compare with `Object.is`; arrays match element-wise
456
- * with equal length; plain objects require every `partial` key to recursively
457
- * match (extra `actual` keys ignored).
458
- *
459
- * @param actual - The value to test against (typically `entry.data`).
460
- * @param partial - The expected partial shape.
461
- * @returns `true` when `partial` is a recursive subset of `actual`.
462
- * @example
463
- * ```ts
464
- * matchesPartial({ a: 1, b: 2 }, { a: 1 }); // true
465
- * matchesPartial([1, 2], [1]); // false (length mismatch)
466
- * ```
467
- */
468
- function matchesPartial(actual, partial) {
469
- if (Object.is(actual, partial)) return true;
470
- if (Array.isArray(partial)) return matchesPartialArray(actual, partial);
471
- if (isPlainObject$1(partial)) return matchesPartialObject(actual, partial);
472
- return false;
473
- }
474
- /**
475
- * Tests whether an entry matches `event` and (when provided) `partial`.
476
- *
477
- * @param entry - The candidate trace entry.
478
- * @param event - Required event name.
479
- * @param partial - Optional partial data shape (subset-matched against `entry.data`).
480
- * @returns `true` when the entry matches the event and optional partial.
481
- * @example
482
- * ```ts
483
- * entryMatches({ level: "info", event: "a", data: { x: 1 }, ts: 0 }, "a", { x: 1 }); // true
484
- * ```
485
- */
486
- function entryMatches(entry, event, partial) {
487
- if (entry.event !== event) return false;
488
- return partial === void 0 ? true : matchesPartial(entry.data, partial);
489
- }
490
- /**
491
- * Render a `partial` for an error message, prefixed with a space when present.
492
- *
493
- * @param partial - Optional partial data shape.
494
- * @returns A ` matching <json>` suffix, or an empty string when absent.
495
- * @example
496
- * ```ts
497
- * describePartial({ ok: true }); // ' matching {"ok":true}'
498
- * ```
499
- */
500
- function describePartial(partial) {
501
- return partial === void 0 ? "" : ` matching ${JSON.stringify(partial)}`;
502
- }
503
- /**
504
- * Find the first entry with `event` at or after `startIndex`, scanning forward.
505
- *
506
- * @param entries - The trace array to scan.
507
- * @param event - Event name to find.
508
- * @param startIndex - Index to begin scanning from (inclusive).
509
- * @returns The index of the first match, or `-1` when none exists from `startIndex` on.
510
- * @example
511
- * ```ts
512
- * findEventAtOrAfter([{ event: "a" }, { event: "b" }] as LogEntry[], "b", 0); // 1
513
- * ```
514
- */
515
- function findEventAtOrAfter(entries, event, startIndex) {
516
- for (let index = startIndex; index < entries.length; index++) if (entries[index]?.event === event) return index;
517
- return -1;
518
- }
519
- /**
520
- * Create a fluent assertion chain bound to the live `entries` array. Each method
521
- * reads `entries` at call time, so assertions reflect later logging.
522
- *
523
- * @param entries - The live trace array (read on each assertion call).
524
- * @returns A fresh {@link ExpectChain} backed by `entries`.
525
- * @example
526
- * ```ts
527
- * createExpectChain(state.entries).toHaveEvent("build:complete");
528
- * ```
529
- */
530
- function createExpectChain(entries) {
531
- const chain = {
532
- /**
533
- * Assert at least one entry has `event`, optionally matching `partial`.
534
- *
535
- * @param event - Event name to find.
536
- * @param partial - Optional partial data shape (subset-matched).
537
- * @returns The same chain for chaining.
538
- * @throws {LogExpectAssertionError} When no matching entry exists.
539
- * @example
540
- * ```ts
541
- * chain.toHaveEvent("build:phase", { status: "start" });
542
- * ```
543
- */
544
- toHaveEvent(event, partial) {
545
- if (!entries.some((entry) => entryMatches(entry, event, partial))) throw new LogExpectAssertionError(`Expected trace to contain event "${event}"${describePartial(partial)}, but none was found.`);
546
- return chain;
547
- },
548
- /**
549
- * Assert all of `events` appear in the trace in the given relative order.
550
- *
551
- * @param events - Ordered list of event names (gaps allowed).
552
- * @returns The same chain for chaining.
553
- * @throws {LogExpectAssertionError} When the ordering cannot be satisfied.
554
- * @example
555
- * ```ts
556
- * chain.toHaveEventInOrder(["build:phase", "build:complete"]);
557
- * ```
558
- */
559
- toHaveEventInOrder(events) {
560
- let cursor = 0;
561
- for (const [position, event] of events.entries()) {
562
- const matchIndex = findEventAtOrAfter(entries, event, cursor);
563
- if (matchIndex === -1) throw new LogExpectAssertionError(`Expected events in order ${JSON.stringify(events)}, but "${event}" (index ${position}) was not found at or after position ${cursor}.`);
564
- cursor = matchIndex + 1;
565
- }
566
- return chain;
567
- },
568
- /**
569
- * Assert NO entry has `event` (optionally narrowed by `partial`).
570
- *
571
- * @param event - Event name that must be absent.
572
- * @param partial - Optional partial data shape; only matching entries violate.
573
- * @returns The same chain for chaining.
574
- * @throws {LogExpectAssertionError} When a matching entry exists.
575
- * @example
576
- * ```ts
577
- * chain.toNotHaveEvent("deploy:failed");
578
- * ```
579
- */
580
- toNotHaveEvent(event, partial) {
581
- const offending = entries.findIndex((entry) => entryMatches(entry, event, partial));
582
- if (offending !== -1) throw new LogExpectAssertionError(`Expected trace to NOT contain event "${event}"${describePartial(partial)}, but found one at index ${offending}.`);
583
- return chain;
584
- }
585
- };
586
- return chain;
587
- }
588
- //#endregion
589
- //#region src/plugins/log/api.ts
590
- /**
591
- * @file log plugin — API factory.
592
- *
593
- * Builds the `LogApi` over the plugin's `{ config, state }` core context:
594
- * the leveled loggers (via a shared `append`), the frozen `trace()` snapshot,
595
- * the live `expect()` chain, `addSink`, and `reset`.
596
- */
597
- /**
598
- * Append a new entry to the trace and fan it out to every sink in order.
599
- *
600
- * @param state - The mutable log state to append to.
601
- * @param level - Severity level for the entry.
602
- * @param event - Event identifier.
603
- * @param data - Optional structured payload.
604
- * @example
605
- * ```ts
606
- * append(state, "info", "content:ready", { count: 12 });
607
- * ```
608
- */
609
- function append(state, level, event, data) {
610
- const entry = {
611
- level,
612
- event,
613
- data,
614
- ts: Date.now()
615
- };
616
- state.entries.push(entry);
617
- for (const sink of state.sinks) sink.write(entry);
618
- }
619
- /**
620
- * Tests whether a value is a non-null, non-array plain object.
621
- *
622
- * @param value - The value to test.
623
- * @returns `true` when `value` is a non-null object that is not an array.
624
- * @example
625
- * ```ts
626
- * isPlainObject({ a: 1 }); // true
627
- * isPlainObject([1]); // false
628
- * ```
629
- */
630
- function isPlainObject(value) {
631
- return typeof value === "object" && value !== null && !Array.isArray(value);
632
- }
633
- /**
634
- * Merge an `Error`'s `message`/`stack` into `data` under an `error` key. The
635
- * `error` field is always preserved; only a plain object `data` contributes its
636
- * keys. Non-plain-object `data` (arrays and primitives) is replaced by `{}` —
637
- * its original value is not retained — so the merge target is always a record.
638
- *
639
- * @param data - Original payload (any shape).
640
- * @param error - The originating error to merge.
641
- * @returns A new object carrying any plain-object keys plus the `error` field.
642
- * @example
643
- * ```ts
644
- * mergeError({ target: "cf" }, new Error("boom"));
645
- * // { target: "cf", error: { message: "boom", stack: "..." } }
646
- * ```
647
- */
648
- function mergeError(data, error) {
649
- return {
650
- ...isPlainObject(data) ? data : {},
651
- error: {
652
- message: error.message,
653
- stack: error.stack
654
- }
655
- };
656
- }
657
- /**
658
- * Create the log plugin API surface injected as `ctx.log` / `app.log`.
659
- *
660
- * @param ctx - Core plugin context (`{ config, state }`).
661
- * @returns The {@link LogApi} bound to `ctx.state`.
662
- * @example
663
- * ```ts
664
- * const log = createLogApi(ctx);
665
- * log.info("content:ready", { articleCount: 12 });
666
- * ```
667
- */
668
- function createLogApi(ctx) {
669
- const { state } = ctx;
670
- return {
671
- /**
672
- * Append an `info` entry and fan it out to every sink.
673
- *
674
- * @param event - Event identifier (convention: `domain:action`).
675
- * @param data - Optional structured payload.
676
- * @example
677
- * ```ts
678
- * log.info("content:ready", { count: 12 });
679
- * ```
680
- */
681
- info(event, data) {
682
- append(state, "info", event, data);
683
- },
684
- /**
685
- * Append a `debug` entry and fan it out to every sink.
686
- *
687
- * @param event - Event identifier (convention: `domain:action`).
688
- * @param data - Optional structured payload.
689
- * @example
690
- * ```ts
691
- * log.debug("router:match", { path: "/blog/" });
692
- * ```
693
- */
694
- debug(event, data) {
695
- append(state, "debug", event, data);
696
- },
697
- /**
698
- * Append a `warn` entry and fan it out to every sink.
699
- *
700
- * @param event - Event identifier (convention: `domain:action`).
701
- * @param data - Optional structured payload.
702
- * @example
703
- * ```ts
704
- * log.warn("build:skip", { reason: "no sitemap" });
705
- * ```
706
- */
707
- warn(event, data) {
708
- append(state, "warn", event, data);
709
- },
710
- /**
711
- * Append an `error` entry. When `error` is provided, its `message`/`stack`
712
- * are merged into `data` under an `error` key (existing keys preserved);
713
- * otherwise `data` is recorded as-is.
714
- *
715
- * @param event - Event identifier (convention: `domain:action`).
716
- * @param data - Optional structured payload.
717
- * @param error - Optional originating Error to merge into `data`.
718
- * @example
719
- * ```ts
720
- * log.error("deploy:failed", { target: "cf" }, err);
721
- * ```
722
- */
723
- error(event, data, error) {
724
- append(state, "error", event, error === void 0 ? data : mergeError(data, error));
725
- },
726
- /**
727
- * Return a frozen snapshot (fresh copy) of the entries recorded so far.
728
- *
729
- * @returns A readonly, frozen copy of the recorded entries.
730
- * @example
731
- * ```ts
732
- * const entries = log.trace();
733
- * ```
734
- */
735
- trace() {
736
- return Object.freeze([...state.entries]);
737
- },
738
- /**
739
- * Return a fluent assertion chain bound to the live entries array.
740
- *
741
- * @returns A fresh {@link ExpectChain} reading `state.entries` live.
742
- * @example
743
- * ```ts
744
- * log.expect().toHaveEvent("build:complete");
745
- * ```
746
- */
747
- expect() {
748
- return createExpectChain(state.entries);
749
- },
750
- /**
751
- * Register an additional output sink at runtime.
752
- *
753
- * @param sink - The sink to add to the fan-out list.
754
- * @example
755
- * ```ts
756
- * log.addSink({ write: (e) => stream.write(JSON.stringify(e)) });
757
- * ```
758
- */
759
- addSink(sink) {
760
- state.sinks.push(sink);
761
- },
762
- /**
763
- * Clear all recorded entries while keeping registered sinks.
764
- *
765
- * @example
766
- * ```ts
767
- * log.reset();
768
- * ```
769
- */
770
- reset() {
771
- state.entries.length = 0;
772
- }
773
- };
774
- }
775
- //#endregion
776
- //#region src/plugins/log/sinks.ts
777
- /** Severity rank for threshold comparison (higher = more severe). */
778
- const LEVEL_RANK = {
779
- debug: 10,
780
- info: 20,
781
- warn: 30,
782
- error: 40
783
- };
784
- /**
785
- * Build the console sink: routes entries by channel — `error` → `console.error`,
786
- * `warn` → `console.warn`, and `debug`/`info` → `console.log`. The full entry
787
- * object is forwarded so the console serializes its `event` and `data`. Entries
788
- * below `minLevel` are dropped (the in-memory trace still records everything).
789
- *
790
- * @param minLevel - Lowest severity to print. Defaults to `"debug"` (print all).
791
- * @returns A {@link LogSink} that writes to the matching `console` channel.
792
- * @example
793
- * ```ts
794
- * state.sinks.push(consoleSink("info")); // suppress debug spam
795
- * ```
796
- */
797
- function consoleSink(minLevel = "debug") {
798
- const threshold = LEVEL_RANK[minLevel];
799
- return {
800
- /**
801
- * Route a single entry to the console channel matching its level.
802
- *
803
- * @param entry - The entry to emit.
804
- * @example
805
- * ```ts
806
- * sink.write({ level: "warn", event: "build:skip", ts: Date.now() });
807
- * ```
808
- */
809
- write(entry) {
810
- if (LEVEL_RANK[entry.level] < threshold) return;
811
- if (entry.level === "error") console.error(entry);
812
- else if (entry.level === "warn") console.warn(entry);
813
- else console.log(entry);
814
- } };
815
- }
816
- /**
817
- * Install mode-selected default sinks at onInit. The in-memory trace is always
818
- * on (`state.entries`); the console sink is added only in dev/production. `dev`
819
- * prints everything (debug+); `production` prints `info`+ only, so the per-phase
820
- * `debug` events (build:bundle, build:pages, …) don't spam a prod build. Both
821
- * modes still record all levels in the in-memory trace.
822
- *
823
- * @param ctx - Core plugin context (`{ config, state }`).
824
- * @param ctx.config - Resolved log config (`{ mode }`).
825
- * @param ctx.state - Mutable log state (`{ entries, sinks }`).
826
- * @example
827
- * ```ts
828
- * // "dev" -> [consoleSink("debug")]; "production" -> [consoleSink("info")]; "test"/"silent" -> []
829
- * ```
830
- */
831
- function installDefaultSinks(ctx) {
832
- if (ctx.config.mode === "dev") ctx.state.sinks.push(consoleSink("debug"));
833
- else if (ctx.config.mode === "production") ctx.state.sinks.push(consoleSink("info"));
834
- }
835
- //#endregion
836
- //#region src/plugins/log/state.ts
837
- /**
838
- * Create fresh log state: an empty append-only trace and an empty sink list.
839
- * No module-level singletons — guarantees per-`createApp` isolation (two
840
- * `createApp` calls never share `entries` or `sinks`).
841
- *
842
- * @param _ctx - Core plugin context (`{ config }`); unused at construction.
843
- * @returns A fresh `LogState` with empty `entries` and `sinks` arrays.
844
- * @example
845
- * ```ts
846
- * const state = createLogState({ config: { mode: "test" } }); // { entries: [], sinks: [] }
847
- * ```
848
- */
849
- function createLogState(_ctx) {
850
- return {
851
- entries: [],
852
- sinks: []
853
- };
854
- }
855
- /**
856
- * Core logging plugin — always-on in-memory trace + `expect()` event-trace DSL.
857
- * API injected as `ctx.log` on every regular plugin and surfaced as `app.log`.
858
- * No depends / events / hooks (core plugin per spec/03 §5).
859
- *
860
- * @see README.md
861
- */
862
- const logPlugin = (0, _moku_labs_core.createCorePlugin)("log", {
863
- config: { mode: "production" },
864
- createState: createLogState,
865
- api: createLogApi,
866
- onInit: installDefaultSinks
867
- });
868
- //#endregion
869
46
  //#region src/config.ts
870
47
  /**
871
48
  * @file Framework configuration — Config + Events types, core plugin registration.
@@ -887,7 +64,7 @@ const coreConfig = (0, _moku_labs_core.createCoreConfig)("web", {
887
64
  stage: "production",
888
65
  mode: "hybrid"
889
66
  },
890
- plugins: [logPlugin, envPlugin],
67
+ plugins: [_moku_labs_common.logPlugin, _moku_labs_common.envPlugin],
891
68
  pluginConfigs: { log: { mode: "production" } }
892
69
  });
893
70
  /**
@@ -8859,312 +8036,17 @@ function networkUrl(port, source = node_os.networkInterfaces) {
8859
8036
  return ip === null ? null : `http://${ip}:${port}`;
8860
8037
  }
8861
8038
  //#endregion
8862
- //#region src/plugins/cli/render/ansi.ts
8863
- /**
8864
- * @file cli plugin — TTY/NO_COLOR-aware ANSI color + box-drawing helpers shared by
8865
- * the Panel renderer. Modeled on the legacy `scripts/_log.ts`: color and box glyphs
8866
- * are emitted only on a real TTY with `NO_COLOR` unset; otherwise plain ASCII so
8867
- * CI logs and pipes stay readable.
8868
- */
8869
- /** The ANSI escape byte (ESC, `0x1b`), built so no literal control char is in source. */
8870
- const ESC = String.fromCodePoint(27);
8871
- /** ANSI SGR codes used by the Panel renderer (each prefixed with the ESC byte). */
8872
- const ANSI = {
8873
- reset: `${ESC}[0m`,
8874
- bold: `${ESC}[1m`,
8875
- dim: `${ESC}[2m`,
8876
- red: `${ESC}[31m`,
8877
- green: `${ESC}[32m`,
8878
- yellow: `${ESC}[33m`,
8879
- blue: `${ESC}[34m`,
8880
- magenta: `${ESC}[35m`,
8881
- cyan: `${ESC}[36m`,
8882
- gray: `${ESC}[90m`
8883
- };
8884
- /**
8885
- * The Moku brand pink (`#FF1E6F`) as an RGB triple, used for 24-bit truecolor output.
8886
- * Degrades to {@link ANSI.magenta} on a 16-color TTY and to plain text off a TTY.
8887
- */
8888
- const BRAND_PINK = {
8889
- r: 255,
8890
- g: 30,
8891
- b: 111
8892
- };
8893
- /**
8894
- * Build a 24-bit (truecolor) SGR foreground escape for the given RGB triple.
8895
- *
8896
- * @param r - Red channel (0–255).
8897
- * @param g - Green channel (0–255).
8898
- * @param b - Blue channel (0–255).
8899
- * @returns The `ESC[38;2;r;g;bm` foreground sequence.
8900
- * @example
8901
- * fg24(255, 30, 111); // "\x1b[38;2;255;30;111m"
8902
- */
8903
- function fg24(r, g, b) {
8904
- return `${ESC}[38;2;${r};${g};${b}m`;
8905
- }
8906
- /** ANSI: erase the entire current line, leaving the cursor where it is. */
8907
- const CLEAR_LINE = `${ESC}[2K`;
8908
- /** ANSI: erase from the cursor to the end of the screen (drops stale trailing rows). */
8909
- const CLEAR_BELOW = `${ESC}[0J`;
8910
- /**
8911
- * Braille spinner frames for live "working…" indicators on a TTY (advance one per tick).
8912
- * Off a TTY the Panel never animates, so this is unused in plain/CI output.
8913
- */
8914
- const SPINNER_FRAMES = [
8915
- "⠋",
8916
- "⠙",
8917
- "⠹",
8918
- "⠸",
8919
- "⠼",
8920
- "⠴",
8921
- "⠦",
8922
- "⠧",
8923
- "⠇",
8924
- "⠏"
8925
- ];
8926
- /**
8927
- * The ANSI sequence to move the cursor up `n` lines (empty string for `n <= 0`). The
8928
- * Panel uses it to repaint a live block in place — move up over the previous draw, then
8929
- * rewrite each row — so progress updates a fixed region instead of scrolling new lines.
8930
- *
8931
- * @param n - Number of lines to move the cursor up.
8932
- * @returns The cursor-up escape sequence, or `""` when `n <= 0`.
8933
- * @example
8934
- * cursorUp(3); // "\x1b[3A"
8935
- */
8936
- function cursorUp(n) {
8937
- return n > 0 ? `${ESC}[${n}A` : "";
8938
- }
8939
- /** Unicode rounded box glyphs used when output is a color-capable TTY. */
8940
- const UNICODE_BOX = {
8941
- topLeft: "╭",
8942
- topRight: "╮",
8943
- bottomLeft: "╰",
8944
- bottomRight: "╯",
8945
- horizontal: "─",
8946
- vertical: "│"
8947
- };
8948
- /** ASCII box glyphs used when output is piped/CI (plain mode). */
8949
- const ASCII_BOX = {
8950
- topLeft: "+",
8951
- topRight: "+",
8952
- bottomLeft: "+",
8953
- bottomRight: "+",
8954
- horizontal: "-",
8955
- vertical: "|"
8956
- };
8957
- /**
8958
- * Matches every ANSI SGR escape sequence (used to measure visible width). Built from
8959
- * the {@link ESC} byte so no literal control character appears in the source regex.
8960
- */
8961
- const ANSI_PATTERN = new RegExp(String.raw`${ESC}\[[0-9;]*m`, "g");
8962
- /**
8963
- * Whether ANSI color/box glyphs should be emitted: a TTY stream with `NO_COLOR`
8964
- * unset. Reads `process.stdout.isTTY` and `process.env.NO_COLOR` by default so the
8965
- * renderer auto-degrades in CI and pipes, exactly like the legacy logger.
8966
- *
8967
- * @param stream - Stream to probe for `isTTY` (defaults to `process.stdout`).
8968
- * @param noColor - The `NO_COLOR` value (defaults to `process.env.NO_COLOR`).
8969
- * @returns `true` when color should be used.
8970
- * @example
8971
- * supportsColor(); // true in an interactive terminal
8972
- */
8973
- function supportsColor(stream = process.stdout, noColor = process.env.NO_COLOR) {
8974
- return stream.isTTY === true && noColor === void 0;
8975
- }
8976
- /**
8977
- * Whether the terminal advertises 24-bit (truecolor) support via `COLORTERM`, so the
8978
- * renderer may emit the exact brand pink ({@link BRAND_PINK}) instead of the 16-color
8979
- * `magenta` approximation. Always layered on top of {@link supportsColor} — truecolor
8980
- * is never used when color itself is disabled.
8981
- *
8982
- * @param colorTerm - The `COLORTERM` value (defaults to `process.env.COLORTERM`).
8983
- * @returns `true` when `COLORTERM` is `truecolor` or `24bit`.
8984
- * @example
8985
- * supportsTruecolor("truecolor"); // true
8986
- */
8987
- function supportsTruecolor(colorTerm = process.env.COLORTERM) {
8988
- return colorTerm === "truecolor" || colorTerm === "24bit";
8989
- }
8990
- /**
8991
- * The braille spinner glyph for a given elapsed time, advancing one frame per
8992
- * `frameMs`. Deriving the frame from wall-clock elapsed (rather than a tick counter)
8993
- * keeps the spinner correct even when the animation ticker is briefly starved by a
8994
- * synchronous build phase and several ticks coalesce — the glyph still reflects real
8995
- * elapsed time instead of freezing on a stale frame.
8996
- *
8997
- * @param elapsedMs - Milliseconds since the live region opened.
8998
- * @param frameMs - Milliseconds per frame (defaults to `80`).
8999
- * @returns The active spinner glyph.
9000
- * @example
9001
- * spinnerFrameAt(240); // "⠹" (the 4th frame at 80ms/frame)
9002
- */
9003
- function spinnerFrameAt(elapsedMs, frameMs = 80) {
9004
- return SPINNER_FRAMES[Math.floor(Math.max(0, elapsedMs) / frameMs) % SPINNER_FRAMES.length] ?? "⠋";
9005
- }
9006
- /**
9007
- * Select the box glyph set for the given color mode (Unicode on a TTY, ASCII off it).
9008
- *
9009
- * @param color - Whether color/Unicode output is enabled.
9010
- * @returns The matching {@link BoxGlyphs} set.
9011
- * @example
9012
- * const glyphs = boxGlyphs(supportsColor());
9013
- */
9014
- function boxGlyphs(color) {
9015
- return color ? UNICODE_BOX : ASCII_BOX;
9016
- }
9017
- /**
9018
- * The visible width of a string, ignoring any ANSI escape sequences it contains.
9019
- *
9020
- * @param text - The (possibly colorized) text to measure.
9021
- * @returns The number of visible characters.
9022
- * @example
9023
- * visibleWidth(`${ANSI.red}hi${ANSI.reset}`); // 2
9024
- */
9025
- function visibleWidth(text) {
9026
- return text.replaceAll(ANSI_PATTERN, "").length;
9027
- }
9028
- /**
9029
- * Build a {@link Palette} bound to a fixed color mode. When `color` is `false` every
9030
- * helper returns its input unchanged, so the same render code path produces plain
9031
- * output in CI/pipes.
9032
- *
9033
- * @param color - Whether color is enabled (typically `supportsColor()`).
9034
- * @param truecolor - Whether 24-bit output is enabled (typically `supportsTruecolor()`);
9035
- * only consulted by {@link Palette.pink}. Defaults to `false` (16-color magenta).
9036
- * @returns The bound color palette.
9037
- * @example
9038
- * const palette = makePalette(supportsColor(), supportsTruecolor());
9039
- * const line = palette.green("done");
9040
- */
9041
- function makePalette(color, truecolor = false) {
9042
- return {
9043
- enabled: color,
9044
- /**
9045
- * Wrap text in the given ANSI code (returns it unchanged when color is off).
9046
- *
9047
- * @param code - The ANSI SGR code to apply.
9048
- * @param text - The text to colorize.
9049
- * @returns The colorized (or unchanged) text.
9050
- * @example
9051
- * palette.paint(ANSI.green, "ok");
9052
- */
9053
- paint(code, text) {
9054
- return color ? `${code}${text}${ANSI.reset}` : text;
9055
- },
9056
- /**
9057
- * Bold the given text (no-op in plain mode).
9058
- *
9059
- * @param text - The text to embolden.
9060
- * @returns The bold (or unchanged) text.
9061
- * @example
9062
- * palette.bold("title");
9063
- */
9064
- bold(text) {
9065
- return this.paint(ANSI.bold, text);
9066
- },
9067
- /**
9068
- * Dim the given text (no-op in plain mode).
9069
- *
9070
- * @param text - The text to dim.
9071
- * @returns The dim (or unchanged) text.
9072
- * @example
9073
- * palette.dim("· 84ms");
9074
- */
9075
- dim(text) {
9076
- return this.paint(ANSI.dim, text);
9077
- },
9078
- /**
9079
- * Color the given text green (no-op in plain mode).
9080
- *
9081
- * @param text - The text to colorize.
9082
- * @returns The green (or unchanged) text.
9083
- * @example
9084
- * palette.green("✓");
9085
- */
9086
- green(text) {
9087
- return this.paint(ANSI.green, text);
9088
- },
9089
- /**
9090
- * Color the given text yellow (no-op in plain mode).
9091
- *
9092
- * @param text - The text to colorize.
9093
- * @returns The yellow (or unchanged) text.
9094
- * @example
9095
- * palette.yellow("~");
9096
- */
9097
- yellow(text) {
9098
- return this.paint(ANSI.yellow, text);
9099
- },
9100
- /**
9101
- * Color the given text red (no-op in plain mode).
9102
- *
9103
- * @param text - The text to colorize.
9104
- * @returns The red (or unchanged) text.
9105
- * @example
9106
- * palette.red("✗");
9107
- */
9108
- red(text) {
9109
- return this.paint(ANSI.red, text);
9110
- },
9111
- /**
9112
- * Color the given text cyan (no-op in plain mode).
9113
- *
9114
- * @param text - The text to colorize.
9115
- * @returns The cyan (or unchanged) text.
9116
- * @example
9117
- * palette.cyan("http://localhost:4173");
9118
- */
9119
- cyan(text) {
9120
- return this.paint(ANSI.cyan, text);
9121
- },
9122
- /**
9123
- * Color the given text the Moku brand pink: exact `#FF1E6F` (24-bit) when truecolor
9124
- * is enabled, the 16-color `magenta` approximation otherwise, unchanged in plain mode.
9125
- *
9126
- * @param text - The text to colorize.
9127
- * @returns The pink (or unchanged) text.
9128
- * @example
9129
- * palette.pink("▟▙ moku web");
9130
- */
9131
- pink(text) {
9132
- if (!color) return text;
9133
- if (truecolor) return `${fg24(BRAND_PINK.r, BRAND_PINK.g, BRAND_PINK.b)}${text}${ANSI.reset}`;
9134
- return this.paint(ANSI.magenta, text);
9135
- }
9136
- };
9137
- }
8039
+ //#region src/plugins/cli/render/panel.ts
9138
8040
  /**
9139
- * Frame a list of already-rendered content lines in a box, padding each line to the
9140
- * widest visible line (or `minInnerWidth`, whichever is larger so several boxes can be
9141
- * forced to a shared width). Uses Unicode borders when `color` is enabled and ASCII
9142
- * otherwise. Visible width ignores embedded ANSI so colored lines align.
9143
- *
9144
- * @param lines - The content lines (may contain ANSI color codes).
9145
- * @param color - Whether to use Unicode borders (and assume color-capable output).
9146
- * @param minInnerWidth - Minimum inner (content) width to pad every row to. Defaults to `0`.
9147
- * @returns The boxed lines (top border, content rows, bottom border).
9148
- * @example
9149
- * 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.
9150
8049
  */
9151
- function box(lines, color, minInnerWidth = 0) {
9152
- const glyphs = boxGlyphs(color);
9153
- const inner = Math.max(0, minInnerWidth, ...lines.map((line) => visibleWidth(line)));
9154
- const horizontal = glyphs.horizontal.repeat(inner + 2);
9155
- const top = `${glyphs.topLeft}${horizontal}${glyphs.topRight}`;
9156
- const bottom = `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`;
9157
- return [
9158
- top,
9159
- ...lines.map((line) => {
9160
- const pad = " ".repeat(inner - visibleWidth(line));
9161
- return `${glyphs.vertical} ${line}${pad} ${glyphs.vertical}`;
9162
- }),
9163
- bottom
9164
- ];
9165
- }
9166
- //#endregion
9167
- //#region src/plugins/cli/render/panel.ts
9168
8050
  /** Per-command label shown beside the lockup wordmark. */
9169
8051
  const COMMAND_LABEL = {
9170
8052
  build: "build",
@@ -9254,7 +8136,7 @@ function durationSuffix(palette, durationMs) {
9254
8136
  * railLine(" ├─ ✓ pages", "· 12ms");
9255
8137
  */
9256
8138
  function railLine(left, right, width = RAIL_WIDTH) {
9257
- 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));
9258
8140
  return `${left}${" ".repeat(gap)}${right}`;
9259
8141
  }
9260
8142
  /**
@@ -9295,8 +8177,8 @@ function createPanelRenderer(options = {}) {
9295
8177
  process.stdout.write(chunk);
9296
8178
  });
9297
8179
  const now = options.now ?? Date.now;
9298
- const color = options.color ?? supportsColor();
9299
- 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)()));
9300
8182
  const version = options.version ?? "dev";
9301
8183
  const coreVersion = options.coreVersion;
9302
8184
  const g = glyphSet(color);
@@ -9323,7 +8205,7 @@ function createPanelRenderer(options = {}) {
9323
8205
  const renderPhaseRow = (row) => {
9324
8206
  const branch = palette.dim(g.tree);
9325
8207
  if (row.done) return railLine(` ${branch} ${palette.green("✓")} ${row.name}`, palette.dim(`· ${row.durationMs}ms`));
9326
- 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)}`;
9327
8209
  };
9328
8210
  /**
9329
8211
  * Render the indeterminate "comet" build bar — a short pink fill window sweeping across
@@ -9354,10 +8236,10 @@ function createPanelRenderer(options = {}) {
9354
8236
  * paintPhaseBlock();
9355
8237
  */
9356
8238
  const paintPhaseBlock = () => {
9357
- let frame = cursorUp(phaseDrawn);
9358
- for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
9359
- frame += `${CLEAR_LINE}${renderBuildBar(now() - blockStartedAt)}\n`;
9360
- 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);
9361
8243
  phaseDrawn = phaseRows.length + 1;
9362
8244
  };
9363
8245
  /**
@@ -9367,9 +8249,9 @@ function createPanelRenderer(options = {}) {
9367
8249
  * paintRebuildLine();
9368
8250
  */
9369
8251
  const paintRebuildLine = () => {
9370
- const spinner = palette.cyan(spinnerFrameAt(now() - rebuildStartedAt, SPIN_MS));
8252
+ const spinner = palette.cyan((0, _moku_labs_common_cli.spinnerFrameAt)(now() - rebuildStartedAt, SPIN_MS));
9371
8253
  const elapsed = palette.dim(`· ${((now() - rebuildStartedAt) / 1e3).toFixed(1)}s`);
9372
- writeRaw(`\r${CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
8254
+ writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE} ${spinner} rebuilding ${rebuildLabel} ${elapsed}`);
9373
8255
  };
9374
8256
  /**
9375
8257
  * Repaint the persistent in-place `◍ live` idle pulse beneath the serve panel — the
@@ -9380,7 +8262,7 @@ function createPanelRenderer(options = {}) {
9380
8262
  * paintIdleLine();
9381
8263
  */
9382
8264
  const paintIdleLine = () => {
9383
- 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…")}`);
9384
8266
  };
9385
8267
  /**
9386
8268
  * Advance whichever live region is active by one frame (driven by the shared ticker).
@@ -9507,9 +8389,9 @@ function createPanelRenderer(options = {}) {
9507
8389
  built(summary) {
9508
8390
  if (rebuilding) return;
9509
8391
  if (color && phaseOpen) {
9510
- let frame = cursorUp(phaseDrawn);
9511
- for (const row of phaseRows) frame += `${CLEAR_LINE}${renderPhaseRow(row)}\n`;
9512
- 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);
9513
8395
  }
9514
8396
  const phaseDurations = phaseRows.map((row) => row.durationMs).filter((value) => value !== void 0);
9515
8397
  phaseOpen = false;
@@ -9524,7 +8406,7 @@ function createPanelRenderer(options = {}) {
9524
8406
  const rateLabel = palette.dim(`${rate} pages/s`);
9525
8407
  lines.push(railLine(spark, rateLabel, BOX_INNER));
9526
8408
  }
9527
- writeBlock(box(lines, color, BOX_INNER));
8409
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
9528
8410
  },
9529
8411
  /**
9530
8412
  * Render the server-ready rail (Local / Network URLs + watched dirs) and, on a TTY,
@@ -9538,7 +8420,7 @@ function createPanelRenderer(options = {}) {
9538
8420
  const network = info.network ? palette.cyan(info.network) : palette.dim("unavailable");
9539
8421
  const lines = [`${palette.green("➜")} ${palette.bold("Local")} ${palette.cyan(info.local)}`, `${palette.green("➜")} ${palette.bold("Network")} ${network}`];
9540
8422
  if (info.watching && info.watching.length > 0) lines.push(`${palette.dim("watching")} ${palette.dim(info.watching.join(", "))}`);
9541
- writeBlock(box(lines, color, BOX_INNER));
8423
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
9542
8424
  if (color) {
9543
8425
  serveMode = true;
9544
8426
  idle = true;
@@ -9584,7 +8466,7 @@ function createPanelRenderer(options = {}) {
9584
8466
  rebuilding = false;
9585
8467
  const line = ` ${palette.green("✓")} rebuilt ${palette.bold(String(info.pageCount))} pages ${palette.dim(`· ${info.durationMs}ms · reloaded`)}`;
9586
8468
  if (settledRebuild && color) {
9587
- writeRaw(`\r${CLEAR_LINE}${line}\n`);
8469
+ writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}${line}\n`);
9588
8470
  resumeIdle();
9589
8471
  return;
9590
8472
  }
@@ -9609,7 +8491,7 @@ function createPanelRenderer(options = {}) {
9609
8491
  const id = result.deploymentId ? ` ${dot} ${palette.dim(result.deploymentId)}` : "";
9610
8492
  lines.push(`${palette.dim("→")} ${palette.cyan(result.url)}${id}`);
9611
8493
  } else if (result.deploymentId) lines.push(palette.dim(`id ${result.deploymentId}`));
9612
- writeBlock(box(lines, color, BOX_INNER));
8494
+ writeBlock((0, _moku_labs_common_cli.box)(lines, color, BOX_INNER));
9613
8495
  },
9614
8496
  /**
9615
8497
  * Render a neutral informational line.
@@ -9646,7 +8528,7 @@ function createPanelRenderer(options = {}) {
9646
8528
  const wasRebuilding = rebuilding;
9647
8529
  if (rebuilding) {
9648
8530
  rebuilding = false;
9649
- if (color) writeRaw(`\r${CLEAR_LINE}`);
8531
+ if (color) writeRaw(`\r${_moku_labs_common_cli.CLEAR_LINE}`);
9650
8532
  }
9651
8533
  writeError(` ${palette.red("✗")} ${message}`);
9652
8534
  if (cause !== void 0) writeError(String(cause));
@@ -9707,9 +8589,9 @@ const YES_PATTERN = /^y(es)?$/i;
9707
8589
  /** Prompt rail width — matches the renderer's `RAIL_WIDTH` so the hint aligns with other rows. */
9708
8590
  const PROMPT_WIDTH = 66;
9709
8591
  /** Whether the interactive prompts render with the MOKU marker styling (color/TTY only). */
9710
- const PROMPT_COLOR = supportsColor();
8592
+ const PROMPT_COLOR = (0, _moku_labs_common_cli.supportsColor)();
9711
8593
  /** Shared palette for the interactive prompts (same brand colors as the Panel renderer). */
9712
- 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)());
9713
8595
  /**
9714
8596
  * Build the styled y/N confirm prompt: a brand `◆` marker + the question on the left,
9715
8597
  * a dim `y / N` hint + cyan `›` caret right-aligned to {@link PROMPT_WIDTH}. Falls back
@@ -9724,7 +8606,7 @@ function confirmPrompt(question) {
9724
8606
  if (!PROMPT_COLOR) return `${question} [y/N] `;
9725
8607
  const left = ` ${PROMPT_PALETTE.pink("◆")} ${question}`;
9726
8608
  const right = `${PROMPT_PALETTE.dim("y / N")} ${PROMPT_PALETTE.cyan("›")} `;
9727
- 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));
9728
8610
  return `${left}${" ".repeat(gap)}${right}`;
9729
8611
  }
9730
8612
  /**
@@ -10148,7 +9030,7 @@ function spaEvents(register) {
10148
9030
  }
10149
9031
  //#endregion
10150
9032
  //#region src/plugins/spa/types.ts
10151
- var types_exports$9 = /* @__PURE__ */ require_convention.__exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
9033
+ var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({ COMPONENT_HOOK_NAMES: () => COMPONENT_HOOK_NAMES });
10152
9034
  /** Allowed hook names — single source of truth for fail-fast validation. */
10153
9035
  const COMPONENT_HOOK_NAMES = [
10154
9036
  "onCreate",
@@ -11484,176 +10366,11 @@ var types_exports$3 = /* @__PURE__ */ require_convention.__exportAll({});
11484
10366
  //#region src/plugins/deploy/types.ts
11485
10367
  var types_exports$4 = /* @__PURE__ */ require_convention.__exportAll({});
11486
10368
  //#endregion
11487
- //#region src/plugins/env/types.ts
11488
- var types_exports$5 = /* @__PURE__ */ require_convention.__exportAll({});
11489
- //#endregion
11490
10369
  //#region src/plugins/head/types.ts
11491
- var types_exports$6 = /* @__PURE__ */ require_convention.__exportAll({});
11492
- //#endregion
11493
- //#region src/plugins/log/types.ts
11494
- var types_exports$7 = /* @__PURE__ */ require_convention.__exportAll({});
10370
+ var types_exports$5 = /* @__PURE__ */ require_convention.__exportAll({});
11495
10371
  //#endregion
11496
10372
  //#region src/plugins/router/types.ts
11497
- var types_exports$8 = /* @__PURE__ */ require_convention.__exportAll({});
11498
- //#endregion
11499
- //#region src/plugins/env/providers.ts
11500
- /**
11501
- * @file env plugin — built-in providers: dotenv, processEnv, cloudflareBindings.
11502
- */
11503
- /** Default dotenv file path: optional local overrides. */
11504
- const DEFAULT_DOTENV_PATH = ".env.local";
11505
- /** Property on `globalThis` that the consumer sets per Cloudflare request. */
11506
- const CLOUDFLARE_GLOBAL = "__CLOUDFLARE_ENV__";
11507
- /** `String.indexOf` sentinel meaning "no `=` separator on this line". */
11508
- const NO_SEPARATOR = -1;
11509
- /**
11510
- * Strips a single matching pair of surrounding double or single quotes from a
11511
- * value. Leaves unquoted values (and trailing inline comments) untouched.
11512
- *
11513
- * @param value - The already-trimmed raw value.
11514
- * @returns The value with one outer quote pair removed, if present.
11515
- * @example
11516
- * ```ts
11517
- * stripQuotes('"a"'); // "a"
11518
- * stripQuotes("plain # c"); // "plain # c"
11519
- * ```
11520
- */
11521
- function stripQuotes(value) {
11522
- if (value.length < 2) return value;
11523
- const first = value[0];
11524
- const last = value.at(-1);
11525
- if ((first === "\"" || first === "'") && first === last) return value.slice(1, -1);
11526
- return value;
11527
- }
11528
- /**
11529
- * Reports whether a trimmed line carries no assignment — a blank line or a
11530
- * full-line `#` comment — and should be skipped by the parser.
11531
- *
11532
- * @param trimmed - A whitespace-trimmed line from the dotenv text.
11533
- * @returns `true` when the line is empty or a comment.
11534
- * @example
11535
- * ```ts
11536
- * isIgnoredLine(""); // true
11537
- * isIgnoredLine("# note"); // true
11538
- * isIgnoredLine("A=1"); // false
11539
- * ```
11540
- */
11541
- function isIgnoredLine(trimmed) {
11542
- return trimmed === "" || trimmed.startsWith("#");
11543
- }
11544
- /**
11545
- * Parses `.env`-style text into a flat record. Handles CRLF/LF, blank lines,
11546
- * full-line `#` comments, first-`=` splitting, key/value trimming, and a single
11547
- * outer quote pair. Does not strip trailing inline comments on unquoted values.
11548
- *
11549
- * @param text - The raw file contents.
11550
- * @returns A flat record of parsed key/value pairs.
11551
- * @example
11552
- * ```ts
11553
- * parseDotenv('A=1\nB="two"'); // { A: "1", B: "two" }
11554
- * ```
11555
- */
11556
- function parseDotenv(text) {
11557
- const out = {};
11558
- for (const line of text.split(/\r?\n/)) {
11559
- const trimmed = line.trim();
11560
- if (isIgnoredLine(trimmed)) continue;
11561
- const eq = trimmed.indexOf("=");
11562
- if (eq === NO_SEPARATOR) continue;
11563
- const key = trimmed.slice(0, eq).trim();
11564
- out[key] = stripQuotes(trimmed.slice(eq + 1).trim());
11565
- }
11566
- return out;
11567
- }
11568
- /**
11569
- * A zero-dependency `.env`-style provider that re-reads and re-parses the file
11570
- * from disk on every `load()`. Missing file resolves to `{}` (optional
11571
- * overrides). Strips a single outer quote pair; does not strip trailing inline
11572
- * comments on unquoted values.
11573
- *
11574
- * @param path - Path to the dotenv file. Defaults to `.env.local`.
11575
- * @returns An {@link EnvProvider} named `dotenv:<path>` that reads fresh per call.
11576
- * @example
11577
- * ```ts
11578
- * const provider = dotenv(".env.local");
11579
- * provider.load(); // { PUBLIC_API_URL: "/api", ... }
11580
- * ```
11581
- */
11582
- function dotenv(path = DEFAULT_DOTENV_PATH) {
11583
- return {
11584
- name: `dotenv:${path}`,
11585
- /**
11586
- * Reads and parses the dotenv file fresh from disk; `{}` if it is missing.
11587
- *
11588
- * @returns The parsed environment record, or `{}` when the file is absent.
11589
- * @example
11590
- * ```ts
11591
- * dotenv(".env.local").load();
11592
- * ```
11593
- */
11594
- load() {
11595
- if (!(0, node_fs.existsSync)(path)) return {};
11596
- return parseDotenv((0, node_fs.readFileSync)(path, "utf8"));
11597
- }
11598
- };
11599
- }
11600
- /**
11601
- * A provider that returns a shallow copy of `process.env` at `load()` time.
11602
- *
11603
- * @returns An {@link EnvProvider} named `process-env`.
11604
- * @example
11605
- * ```ts
11606
- * const provider = processEnv();
11607
- * provider.load().HOME; // current process value
11608
- * ```
11609
- */
11610
- function processEnv() {
11611
- return {
11612
- name: "process-env",
11613
- /**
11614
- * Returns a shallow copy of `process.env` at call time.
11615
- *
11616
- * @returns A fresh shallow copy of `process.env`.
11617
- * @example
11618
- * ```ts
11619
- * processEnv().load();
11620
- * ```
11621
- */
11622
- load() {
11623
- return { ...process.env };
11624
- }
11625
- };
11626
- }
11627
- /**
11628
- * A provider that reads live, per-request Cloudflare bindings from
11629
- * `globalThis.__CLOUDFLARE_ENV__` at `load()` time (`?? {}` when absent). Never
11630
- * caches the binding object; the consumer owns the global's request lifecycle.
11631
- *
11632
- * @returns An {@link EnvProvider} named `cloudflare`.
11633
- * @example
11634
- * ```ts
11635
- * globalThis.__CLOUDFLARE_ENV__ = env; // set by the request handler
11636
- * const provider = cloudflareBindings();
11637
- * provider.load(); // reads the current request's bindings
11638
- * ```
11639
- */
11640
- function cloudflareBindings() {
11641
- return {
11642
- name: "cloudflare",
11643
- /**
11644
- * Reads `globalThis.__CLOUDFLARE_ENV__` fresh, never caching the bindings.
11645
- *
11646
- * @returns The current Cloudflare bindings, or `{}` when the global is unset.
11647
- * @example
11648
- * ```ts
11649
- * cloudflareBindings().load();
11650
- * ```
11651
- */
11652
- load() {
11653
- return globalThis[CLOUDFLARE_GLOBAL] ?? {};
11654
- }
11655
- };
11656
- }
10373
+ var types_exports$6 = /* @__PURE__ */ require_convention.__exportAll({});
11657
10374
  //#endregion
11658
10375
  //#region node_modules/unist-util-stringify-position/lib/index.js
11659
10376
  /**
@@ -13884,43 +12601,41 @@ Object.defineProperty(exports, "Deploy", {
13884
12601
  }
13885
12602
  });
13886
12603
  exports.EmbedFacadeButton = EmbedFacadeButton;
13887
- Object.defineProperty(exports, "Env", {
12604
+ exports.GalleryTrack = GalleryTrack;
12605
+ Object.defineProperty(exports, "Head", {
13888
12606
  enumerable: true,
13889
12607
  get: function() {
13890
12608
  return types_exports$5;
13891
12609
  }
13892
12610
  });
13893
- exports.GalleryTrack = GalleryTrack;
13894
- Object.defineProperty(exports, "Head", {
12611
+ Object.defineProperty(exports, "Router", {
13895
12612
  enumerable: true,
13896
12613
  get: function() {
13897
12614
  return types_exports$6;
13898
12615
  }
13899
12616
  });
13900
- Object.defineProperty(exports, "Log", {
12617
+ Object.defineProperty(exports, "Spa", {
13901
12618
  enumerable: true,
13902
12619
  get: function() {
13903
12620
  return types_exports$7;
13904
12621
  }
13905
12622
  });
13906
- Object.defineProperty(exports, "Router", {
13907
- enumerable: true,
13908
- get: function() {
13909
- return types_exports$8;
13910
- }
13911
- });
13912
- Object.defineProperty(exports, "Spa", {
12623
+ Object.defineProperty(exports, "browserEnv", {
13913
12624
  enumerable: true,
13914
12625
  get: function() {
13915
- return types_exports$9;
12626
+ return _moku_labs_common.browserEnv;
13916
12627
  }
13917
12628
  });
13918
- exports.browserEnv = browserEnv;
13919
12629
  exports.buildArticleHead = buildArticleHead;
13920
12630
  exports.buildPlugin = buildPlugin;
13921
12631
  exports.canonical = canonical;
13922
12632
  exports.cliPlugin = cliPlugin;
13923
- exports.cloudflareBindings = cloudflareBindings;
12633
+ Object.defineProperty(exports, "cloudflareBindings", {
12634
+ enumerable: true,
12635
+ get: function() {
12636
+ return _moku_labs_common.cloudflareBindings;
12637
+ }
12638
+ });
13924
12639
  exports.contentPlugin = contentPlugin;
13925
12640
  exports.createApp = createApp;
13926
12641
  exports.createComponent = createComponent;
@@ -13929,8 +12644,18 @@ exports.createUrls = createUrls;
13929
12644
  exports.dataPlugin = dataPlugin;
13930
12645
  exports.defineRoutes = defineRoutes;
13931
12646
  exports.deployPlugin = deployPlugin;
13932
- exports.dotenv = dotenv;
13933
- exports.envPlugin = envPlugin;
12647
+ Object.defineProperty(exports, "dotenv", {
12648
+ enumerable: true,
12649
+ get: function() {
12650
+ return _moku_labs_common.dotenv;
12651
+ }
12652
+ });
12653
+ Object.defineProperty(exports, "envPlugin", {
12654
+ enumerable: true,
12655
+ get: function() {
12656
+ return _moku_labs_common.envPlugin;
12657
+ }
12658
+ });
13934
12659
  exports.feedLink = feedLink;
13935
12660
  exports.fileSystemContent = fileSystemContent;
13936
12661
  exports.headPlugin = headPlugin;
@@ -13938,10 +12663,20 @@ exports.hreflang = hreflang;
13938
12663
  exports.i18nPlugin = i18nPlugin;
13939
12664
  exports.jsonLd = jsonLd;
13940
12665
  exports.lazyEmbed = lazyEmbed;
13941
- exports.logPlugin = logPlugin;
12666
+ Object.defineProperty(exports, "logPlugin", {
12667
+ enumerable: true,
12668
+ get: function() {
12669
+ return _moku_labs_common.logPlugin;
12670
+ }
12671
+ });
13942
12672
  exports.meta = meta;
13943
12673
  exports.og = og;
13944
- exports.processEnv = processEnv;
12674
+ Object.defineProperty(exports, "processEnv", {
12675
+ enumerable: true,
12676
+ get: function() {
12677
+ return _moku_labs_common.processEnv;
12678
+ }
12679
+ });
13945
12680
  exports.route = route;
13946
12681
  exports.routerPlugin = routerPlugin;
13947
12682
  exports.sitePlugin = sitePlugin;