@zenithbuild/cli 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import { existsSync, watch } from 'node:fs';
16
16
  import { readFile, stat } from 'node:fs/promises';
17
17
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
18
18
  import { build } from './build.js';
19
+ import { createSilentLogger } from './ui/logger.js';
19
20
  import {
20
21
  executeServerRoute,
21
22
  injectSsrPayload,
@@ -41,7 +42,7 @@ const MIME_TYPES = {
41
42
  /**
42
43
  * Create and start a development server.
43
44
  *
44
- * @param {{ pagesDir: string, outDir: string, port?: number, host?: string, config?: object }} options
45
+ * @param {{ pagesDir: string, outDir: string, port?: number, host?: string, config?: object, logger?: object | null }} options
45
46
  * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
46
47
  */
47
48
  export async function createDevServer(options) {
@@ -50,8 +51,10 @@ export async function createDevServer(options) {
50
51
  outDir,
51
52
  port = 3000,
52
53
  host = '127.0.0.1',
53
- config = {}
54
+ config = {},
55
+ logger: providedLogger = null
54
56
  } = options;
57
+ const logger = providedLogger || createSilentLogger();
55
58
 
56
59
  const resolvedPagesDir = resolve(pagesDir);
57
60
  const resolvedOutDir = resolve(outDir);
@@ -83,6 +86,7 @@ export async function createDevServer(options) {
83
86
  let durationMs = 0;
84
87
  let buildError = null;
85
88
  const traceEnabled = config.devTrace === true || process.env.ZENITH_DEV_TRACE === '1';
89
+ const verboseLogging = traceEnabled || logger.mode?.logLevel === 'verbose';
86
90
 
87
91
  // Stable dev CSS endpoint points to this backing asset.
88
92
  let currentCssAssetPath = '';
@@ -104,8 +108,10 @@ export async function createDevServer(options) {
104
108
  function _trace(event, payload = {}) {
105
109
  if (!traceEnabled) return;
106
110
  try {
107
- const timestamp = new Date().toISOString();
108
- console.log(`[zenith-dev][${timestamp}] ${event} ${JSON.stringify(payload)}`);
111
+ const detail = Object.keys(payload).length > 0
112
+ ? `${event} ${JSON.stringify(payload)}`
113
+ : event;
114
+ logger.verbose('BUILD', detail);
109
115
  } catch {
110
116
  // tracing must never break the dev server
111
117
  }
@@ -243,11 +249,19 @@ export async function createDevServer(options) {
243
249
 
244
250
  // Initial build
245
251
  try {
246
- const initialBuild = await build({ pagesDir, outDir, config });
252
+ logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
253
+ const initialBuild = await build({ pagesDir, outDir, config, logger });
247
254
  await _syncCssStateFromBuild(initialBuild, buildId);
255
+ if (currentCssHref.length > 0) {
256
+ logger.css(`ready (${currentCssHref})`, { onceKey: `css-ready:${buildId}:${currentCssHref}` });
257
+ }
248
258
  } catch (err) {
249
259
  buildStatus = 'error';
250
260
  buildError = { message: err instanceof Error ? err.message : String(err) };
261
+ logger.error('initial build failed', {
262
+ hint: 'fix the error and restart dev',
263
+ error: err
264
+ });
251
265
  }
252
266
 
253
267
  const server = createServer(async (req, res) => {
@@ -265,7 +279,10 @@ export async function createDevServer(options) {
265
279
  'Connection': 'keep-alive',
266
280
  'X-Zenith-Deprecated': 'true'
267
281
  });
268
- console.warn('[zenith] Warning: /__zenith_hmr is legacy; use /__zenith_dev/events');
282
+ logger.warn('legacy HMR endpoint in use', {
283
+ hint: 'use /__zenith_dev/events',
284
+ onceKey: 'legacy-hmr-endpoint'
285
+ });
269
286
  res.write(': connected\n\n');
270
287
  hmrClients.push(res);
271
288
  req.on('close', () => {
@@ -448,7 +465,11 @@ export async function createDevServer(options) {
448
465
  let filePath = null;
449
466
 
450
467
  if (resolved.matched && resolved.route) {
451
- console.log(`[zenith] Request: ${pathname} | Route: ${resolved.route.path} | Params: ${JSON.stringify(resolved.params)}`);
468
+ if (verboseLogging) {
469
+ logger.router(
470
+ `${req.method || 'GET'} ${pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`
471
+ );
472
+ }
452
473
  const output = resolved.route.output.startsWith('/')
453
474
  ? resolved.route.output.slice(1)
454
475
  : resolved.route.output;
@@ -490,8 +511,11 @@ export async function createDevServer(options) {
490
511
 
491
512
  const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
492
513
  const routeId = resolved.route.route_id || '';
493
- console.log(`[Zenith] guard(${routeId || resolved.route.path}) -> ${trace.guard}`);
494
- console.log(`[Zenith] load(${routeId || resolved.route.path}) -> ${trace.load}`);
514
+ if (verboseLogging) {
515
+ logger.router(
516
+ `${routeId || resolved.route.path} guard=${trace.guard} load=${trace.load}`
517
+ );
518
+ }
495
519
 
496
520
  const result = routeExecution?.result;
497
521
  if (result && result.kind === 'redirect') {
@@ -609,17 +633,25 @@ export async function createDevServer(options) {
609
633
  const cycleBuildId = pendingBuildId + 1;
610
634
  pendingBuildId = cycleBuildId;
611
635
  buildStatus = 'building';
636
+ logger.build(`Rebuild (id=${cycleBuildId})`);
612
637
  _broadcastEvent('build_start', { buildId: cycleBuildId, changedFiles: changed });
613
638
 
614
639
  const startTime = Date.now();
640
+ const previousCssAssetPath = currentCssAssetPath;
641
+ const previousCssContent = currentCssContent;
615
642
  try {
616
- const buildResult = await build({ pagesDir, outDir, config });
643
+ const buildResult = await build({ pagesDir, outDir, config, logger });
617
644
  const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
645
+ const cssChanged = cssReady && (
646
+ currentCssAssetPath !== previousCssAssetPath ||
647
+ currentCssContent !== previousCssContent
648
+ );
618
649
  buildId = cycleBuildId;
619
650
  buildStatus = 'ok';
620
651
  buildError = null;
621
652
  lastBuildMs = Date.now();
622
653
  durationMs = lastBuildMs - startTime;
654
+ logger.build(`Complete (id=${cycleBuildId}, ${durationMs}ms)`);
623
655
 
624
656
  _broadcastEvent('build_complete', {
625
657
  buildId: cycleBuildId,
@@ -637,12 +669,23 @@ export async function createDevServer(options) {
637
669
  changedFiles: changed
638
670
  });
639
671
 
640
- const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
641
- if (onlyCss && cssReady && currentCssHref.length > 0) {
642
- // Let the client fetch the updated CSS automatically
672
+ if (cssChanged && currentCssHref.length > 0) {
673
+ logger.css(`ready (${currentCssHref})`);
674
+ logger.hmr(`css_update (buildId=${cycleBuildId})`);
643
675
  _broadcastEvent('css_update', { href: currentCssHref, changedFiles: changed });
644
- } else {
676
+ }
677
+
678
+ const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
679
+ if (!onlyCss) {
680
+ logger.hmr(`reload (buildId=${cycleBuildId})`);
645
681
  _broadcastEvent('reload', { changedFiles: changed });
682
+ } else {
683
+ _trace('css_only_update', {
684
+ buildId: cycleBuildId,
685
+ cssHref: currentCssHref,
686
+ cssChanged,
687
+ changedFiles: changed
688
+ });
646
689
  }
647
690
  } catch (err) {
648
691
  const fullError = err instanceof Error ? err.message : String(err);
@@ -650,6 +693,10 @@ export async function createDevServer(options) {
650
693
  buildError = { message: fullError.length > 10000 ? fullError.slice(0, 10000) + '... (truncated)' : fullError };
651
694
  lastBuildMs = Date.now();
652
695
  durationMs = lastBuildMs - startTime;
696
+ logger.error('rebuild failed', {
697
+ hint: 'fix the error and save again',
698
+ error: err
699
+ });
653
700
 
654
701
  _broadcastEvent('build_error', { buildId: cycleBuildId, ...buildError, changedFiles: changed });
655
702
  _trace('state_snapshot', {
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { resolve, join, dirname } from 'node:path';
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { createLogger } from './ui/logger.js';
16
+ import { createZenithLogger } from './ui/logger.js';
17
17
 
18
18
  const COMMANDS = ['dev', 'build', 'preview'];
19
19
  const DEFAULT_VERSION = '0.0.0';
@@ -90,7 +90,7 @@ async function loadConfig(projectRoot) {
90
90
  * @param {string} [cwd] - Working directory override
91
91
  */
92
92
  export async function cli(args, cwd) {
93
- const logger = createLogger(process);
93
+ const logger = createZenithLogger(process);
94
94
  const command = args[0];
95
95
  const cliVersion = getCliVersion();
96
96
 
@@ -118,10 +118,10 @@ export async function cli(args, cwd) {
118
118
 
119
119
  if (command === 'build') {
120
120
  const { build } = await import('./build.js');
121
- logger.info('Building...');
122
- const result = await build({ pagesDir, outDir, config });
123
- logger.success(`Built ${result.pages} page(s), ${result.assets.length} asset(s)`);
124
- logger.summary([{ label: 'Output', value: './dist' }]);
121
+ logger.build('Building');
122
+ const result = await build({ pagesDir, outDir, config, logger, showBundlerInfo: false });
123
+ logger.ok(`Built ${result.pages} page(s), ${result.assets.length} asset(s)`);
124
+ logger.summary([{ label: 'Output', value: './dist' }], 'BUILD');
125
125
  }
126
126
 
127
127
  if (command === 'dev') {
@@ -130,9 +130,9 @@ export async function cli(args, cwd) {
130
130
  ? Number.parseInt(process.env.ZENITH_DEV_PORT, 10)
131
131
  : resolvePort(args.slice(1), 3000);
132
132
  const host = process.env.ZENITH_DEV_HOST || '127.0.0.1';
133
- logger.info('Starting dev server...');
134
- const dev = await createDevServer({ pagesDir, outDir, port, host, config });
135
- logger.success(`Dev server running at http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${dev.port}`);
133
+ logger.dev('Starting dev server');
134
+ const dev = await createDevServer({ pagesDir, outDir, port, host, config, logger });
135
+ logger.ok(`http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${dev.port}`);
136
136
 
137
137
  // Graceful shutdown
138
138
  process.on('SIGINT', () => {
@@ -149,9 +149,9 @@ export async function cli(args, cwd) {
149
149
  const { createPreviewServer } = await import('./preview.js');
150
150
  const port = resolvePort(args.slice(1), 4000);
151
151
  const host = process.env.ZENITH_PREVIEW_HOST || '127.0.0.1';
152
- logger.info('Starting preview server...');
153
- const preview = await createPreviewServer({ distDir: outDir, port, host });
154
- logger.success(`Preview server running at http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${preview.port}`);
152
+ logger.dev('Starting preview server');
153
+ const preview = await createPreviewServer({ distDir: outDir, port, host, logger });
154
+ logger.ok(`http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${preview.port}`);
155
155
 
156
156
  process.on('SIGINT', () => {
157
157
  preview.close();
@@ -172,7 +172,7 @@ const isDirectRun = process.argv[1] && (
172
172
 
173
173
  if (isDirectRun) {
174
174
  cli(process.argv.slice(2)).catch((error) => {
175
- const logger = createLogger(process);
175
+ const logger = createZenithLogger(process);
176
176
  logger.error(error);
177
177
  process.exit(1);
178
178
  });
package/dist/preview.js CHANGED
@@ -14,6 +14,7 @@ import { createServer } from 'node:http';
14
14
  import { access, readFile } from 'node:fs/promises';
15
15
  import { extname, join, normalize, resolve, sep, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { createSilentLogger } from './ui/logger.js';
17
18
  import {
18
19
  compareRouteSpecificity,
19
20
  matchRoute as matchManifestRoute,
@@ -386,11 +387,13 @@ try {
386
387
  /**
387
388
  * Create and start a preview server.
388
389
  *
389
- * @param {{ distDir: string, port?: number, host?: string }} options
390
+ * @param {{ distDir: string, port?: number, host?: string, logger?: object | null }} options
390
391
  * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
391
392
  */
392
393
  export async function createPreviewServer(options) {
393
- const { distDir, port = 4000, host = '127.0.0.1' } = options;
394
+ const { distDir, port = 4000, host = '127.0.0.1', logger: providedLogger = null } = options;
395
+ const logger = providedLogger || createSilentLogger();
396
+ const verboseLogging = logger.mode?.logLevel === 'verbose';
394
397
  let actualPort = port;
395
398
 
396
399
  function publicHost() {
@@ -502,7 +505,11 @@ export async function createPreviewServer(options) {
502
505
  let htmlPath = null;
503
506
 
504
507
  if (resolved.matched && resolved.route) {
505
- console.log(`[zenith] Request: ${url.pathname} | Route: ${resolved.route.path} | Params: ${JSON.stringify(resolved.params)}`);
508
+ if (verboseLogging) {
509
+ logger.router(
510
+ `${req.method || 'GET'} ${url.pathname} -> ${resolved.route.path} params=${JSON.stringify(resolved.params)}`
511
+ );
512
+ }
506
513
  const output = resolved.route.output.startsWith('/')
507
514
  ? resolved.route.output.slice(1)
508
515
  : resolved.route.output;
@@ -541,8 +548,9 @@ export async function createPreviewServer(options) {
541
548
 
542
549
  const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
543
550
  const routeId = resolved.route.route_id || routeIdFromSourcePath(resolved.route.server_script_path || '');
544
- console.log(`[Zenith] guard(${routeId}) -> ${trace.guard}`);
545
- console.log(`[Zenith] load(${routeId}) -> ${trace.load}`);
551
+ if (verboseLogging) {
552
+ logger.router(`${routeId} guard=${trace.guard} load=${trace.load}`);
553
+ }
546
554
 
547
555
  const result = routeExecution?.result;
548
556
  if (result && result.kind === 'redirect') {
package/dist/ui/env.js CHANGED
@@ -10,6 +10,14 @@ function flagEnabled(value) {
10
10
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
11
11
  }
12
12
 
13
+ function parseLogLevel(value) {
14
+ const normalized = String(value || '').trim().toLowerCase();
15
+ if (normalized === 'quiet' || normalized === 'verbose') {
16
+ return normalized;
17
+ }
18
+ return 'normal';
19
+ }
20
+
13
21
  /**
14
22
  * @param {{ env?: Record<string, string | undefined>, stdout?: { isTTY?: boolean } }} runtime
15
23
  */
@@ -21,10 +29,17 @@ export function getUiMode(runtime = process) {
21
29
  const noColor = env.NO_COLOR !== undefined && String(env.NO_COLOR).length >= 0;
22
30
  const forceColor = flagEnabled(env.FORCE_COLOR);
23
31
  const debug = flagEnabled(env.ZENITH_DEBUG);
32
+ let logLevel = parseLogLevel(env.ZENITH_LOG_LEVEL);
24
33
 
25
34
  const plain = noUi || ci || !tty;
26
35
  const color = !plain && !noColor && (forceColor || tty);
27
36
  const spinner = tty && !plain && !ci;
37
+ if (flagEnabled(env.ZENITH_DEV_TRACE)) {
38
+ logLevel = 'verbose';
39
+ }
40
+ if (debug && logLevel !== 'quiet') {
41
+ logLevel = 'verbose';
42
+ }
28
43
 
29
44
  return {
30
45
  plain,
@@ -32,7 +47,8 @@ export function getUiMode(runtime = process) {
32
47
  tty,
33
48
  ci,
34
49
  spinner,
35
- debug
50
+ debug,
51
+ logLevel
36
52
  };
37
53
  }
38
54
 
package/dist/ui/format.js CHANGED
@@ -1,53 +1,78 @@
1
- /**
2
- * Deterministic text formatters for CLI UX.
3
- */
4
-
1
+ import pc from 'picocolors';
5
2
  import { relative, sep } from 'node:path';
6
3
 
7
- const ANSI = {
8
- reset: '\x1b[0m',
9
- bold: '\x1b[1m',
10
- dim: '\x1b[2m',
11
- red: '\x1b[31m',
12
- yellow: '\x1b[33m',
13
- green: '\x1b[32m',
14
- cyan: '\x1b[36m'
15
- };
16
4
  const DEFAULT_PHASE = 'cli';
17
5
  const DEFAULT_FILE = '.';
18
6
  const DEFAULT_HINT_BASE = 'https://github.com/zenithbuild/zenith/blob/main/zenith-cli/CLI_CONTRACT.md';
7
+ const PREFIX = '[zenith]';
8
+ const TAG_WIDTH = 6;
9
+
10
+ const TAG_COLORS = {
11
+ DEV: (colors, value) => colors.cyan(value),
12
+ BUILD: (colors, value) => colors.blue(value),
13
+ HMR: (colors, value) => colors.magenta(value),
14
+ ROUTER: (colors, value) => colors.cyan(value),
15
+ CSS: (colors, value) => colors.yellow(value),
16
+ OK: (colors, value) => colors.green(value),
17
+ WARN: (colors, value) => colors.bold(colors.yellow(value)),
18
+ ERR: (colors, value) => colors.bold(colors.red(value))
19
+ };
20
+
21
+ function getColors(mode) {
22
+ return pc.createColors(Boolean(mode?.color));
23
+ }
19
24
 
20
- function colorize(mode, token, text) {
25
+ export function formatPrefix(mode) {
26
+ return mode.color ? getColors(mode).dim(PREFIX) : PREFIX;
27
+ }
28
+
29
+ function colorizeTag(mode, tag) {
30
+ const padded = String(tag || '').padEnd(TAG_WIDTH, ' ');
21
31
  if (!mode.color) {
22
- return text;
32
+ return padded;
23
33
  }
24
- return `${ANSI[token]}${text}${ANSI.reset}`;
34
+ const colors = getColors(mode);
35
+ const colorizer = TAG_COLORS[tag] || ((_colors, value) => colors.white(value));
36
+ return colorizer(colors, padded);
25
37
  }
26
38
 
27
- export function formatHeading(mode, text) {
28
- const label = mode.plain ? 'ZENITH CLI' : colorize(mode, 'bold', 'Zenith CLI');
29
- return `${label} ${text}`.trim();
39
+ function colorizeGlyph(mode, glyph, tag) {
40
+ if (!mode.color) {
41
+ return glyph;
42
+ }
43
+ const colors = getColors(mode);
44
+ const colorizer = TAG_COLORS[tag] || ((_colors, value) => value);
45
+ return colorizer(colors, glyph);
30
46
  }
31
47
 
32
- export function formatStep(mode, text) {
33
- if (mode.plain) {
34
- return `[zenith] INFO: ${text}`;
35
- }
36
- const bullet = colorize(mode, 'cyan', '•');
37
- return `[zenith] ${bullet} ${text}`;
48
+ export function formatLine(mode, { glyph = '•', tag = 'DEV', text = '' }) {
49
+ return `${formatPrefix(mode)} ${colorizeGlyph(mode, glyph, tag)} ${colorizeTag(mode, tag)} ${String(text || '')}`;
38
50
  }
39
51
 
40
- export function formatSummaryTable(mode, rows) {
52
+ export function formatStep(mode, text, tag = 'BUILD') {
53
+ return formatLine(mode, { glyph: '•', tag, text });
54
+ }
55
+
56
+ export function formatHint(mode, text) {
57
+ const body = ` hint: ${String(text || '').trim()}`;
58
+ return mode.color ? getColors(mode).dim(body) : body;
59
+ }
60
+
61
+ export function formatHeading(mode, text) {
62
+ const label = mode.color ? getColors(mode).bold('Zenith CLI') : 'Zenith CLI';
63
+ return `${label} ${String(text || '').trim()}`.trim();
64
+ }
65
+
66
+ export function formatSummaryTable(mode, rows, tag = 'BUILD') {
41
67
  if (!Array.isArray(rows) || rows.length === 0) {
42
68
  return '';
43
69
  }
44
- const maxLabel = rows.reduce((acc, row) => Math.max(acc, String(row.label || '').length), 0);
45
70
  return rows
46
- .map((row) => {
47
- const label = String(row.label || '').padEnd(maxLabel, ' ');
48
- const value = String(row.value || '');
49
- return `[zenith] ${label} : ${value}`;
50
- })
71
+ .map((row) => formatLine(mode, {
72
+ glyph: '',
73
+ tag,
74
+ text: `${String(row.label || '')}: ${String(row.value || '')}`
75
+ }))
51
76
  .join('\n');
52
77
  }
53
78
 
@@ -131,36 +156,49 @@ export function normalizeError(err) {
131
156
  return new Error(sanitizeErrorMessage(err));
132
157
  }
133
158
 
159
+ function firstMeaningfulLine(text) {
160
+ return String(text || '')
161
+ .split('\n')
162
+ .map((line) => line.trim())
163
+ .find((line) => line.length > 0) || '';
164
+ }
165
+
134
166
  /**
135
167
  * @param {unknown} err
136
- * @param {{ plain: boolean, color: boolean, debug: boolean }} mode
168
+ * @param {{ plain: boolean, color: boolean, debug?: boolean, logLevel?: string }} mode
137
169
  */
138
170
  export function formatErrorBlock(err, mode) {
139
171
  const normalized = normalizeError(err);
140
172
  const maybe = /** @type {{ code?: unknown, phase?: unknown, kind?: unknown, file?: unknown, hint?: unknown }} */ (normalized);
141
- const kind = sanitizeErrorMessage(maybe.kind || maybe.code || 'CLI_ERROR');
142
173
  const phase = maybe.phase ? sanitizeErrorMessage(maybe.phase) : inferPhaseFromArgv();
143
174
  const code = maybe.code
144
175
  ? sanitizeErrorMessage(maybe.code)
145
176
  : `${phase.toUpperCase().replace(/[^A-Z0-9]+/g, '_') || 'CLI'}_FAILED`;
146
177
  const rawMessage = sanitizeErrorMessage(normalized.message || String(normalized));
147
178
  const message = normalizeErrorMessagePaths(rawMessage);
179
+ const compactMessage = firstMeaningfulLine(message) || 'Command failed';
148
180
  const file = normalizePathForDisplay(
149
181
  sanitizeErrorMessage(maybe.file || extractFileFromMessage(message) || DEFAULT_FILE)
150
182
  );
151
183
  const hint = sanitizeErrorMessage(maybe.hint || formatHintUrl(code));
152
184
 
185
+ if (mode.logLevel !== 'verbose' && !mode.debug) {
186
+ return [
187
+ formatLine(mode, { glyph: '✖', tag: 'ERR', text: compactMessage }),
188
+ formatHint(mode, hint)
189
+ ].join('\n');
190
+ }
191
+
153
192
  const lines = [];
154
- lines.push('[zenith] ERROR: Command failed');
155
- lines.push(`[zenith] Error Kind: ${kind}`);
156
- lines.push(`[zenith] Phase: ${phase || DEFAULT_PHASE}`);
157
- lines.push(`[zenith] Code: ${code || 'CLI_FAILED'}`);
158
- lines.push(`[zenith] File: ${file || DEFAULT_FILE}`);
159
- lines.push(`[zenith] Hint: ${hint || formatHintUrl(code)}`);
160
- lines.push(`[zenith] Message: ${message}`);
193
+ lines.push(formatLine(mode, { glyph: '✖', tag: 'ERR', text: compactMessage }));
194
+ lines.push(formatHint(mode, hint || formatHintUrl(code)));
195
+ lines.push(`${formatPrefix(mode)} code: ${code || 'CLI_FAILED'}`);
196
+ lines.push(`${formatPrefix(mode)} phase: ${phase || DEFAULT_PHASE}`);
197
+ lines.push(`${formatPrefix(mode)} file: ${file || DEFAULT_FILE}`);
198
+ lines.push(`${formatPrefix(mode)} detail: ${message}`);
161
199
 
162
200
  if (mode.debug && normalized.stack) {
163
- lines.push('[zenith] Stack:');
201
+ lines.push(`${formatPrefix(mode)} stack:`);
164
202
  lines.push(...String(normalized.stack).split('\n').slice(0, 20));
165
203
  }
166
204