@nowline/cli 0.5.1 → 0.6.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.
@@ -1,9 +1,21 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { resolveIncludes } from '@nowline/core';
3
+ import {
4
+ type ExportFormat,
5
+ exportDocument,
6
+ type HostEnv,
7
+ type RenderInputs,
8
+ } from '@nowline/export';
4
9
  import { lengthToPoints, parseLength } from '@nowline/export-core';
5
- import { layoutRoadmap, normalizeThemeName, type ThemeName } from '@nowline/layout';
6
- import { type AssetResolver, renderSvg } from '@nowline/renderer';
10
+ import {
11
+ type NormalizedZone,
12
+ normalizeThemeName,
13
+ normalizeZone,
14
+ resolveToday,
15
+ type ThemeName,
16
+ TimezoneError,
17
+ } from '@nowline/layout';
18
+ import type { AssetResolver } from '@nowline/renderer';
7
19
  import type { ParsedArgs } from '../cli/args.js';
8
20
  import {
9
21
  FormatResolutionError,
@@ -15,7 +27,7 @@ import { resolveRenderOutputPath } from '../cli/output-path.js';
15
27
  import { parseNowlineJson } from '../convert/parse-json.js';
16
28
  import { printNowlineFile } from '../convert/printer.js';
17
29
  import { serializeToJson } from '../convert/schema.js';
18
- import { getServices, parseSource } from '../core/parse.js';
30
+ import { parseSource } from '../core/parse.js';
19
31
  import { type DiagnosticSource, formatDiagnostics } from '../diagnostics/index.js';
20
32
  import {
21
33
  describeContentLocaleSource,
@@ -93,7 +105,7 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
93
105
  absInputPath: input.isStdin ? path.resolve(cwd, 'stdin.nowline') : input.path,
94
106
  isStdin: input.isStdin,
95
107
  theme: parseTheme(args.theme),
96
- today: resolveNowArg(args),
108
+ today: resolveNowCli(args),
97
109
  width: parseWidthArg(args.width),
98
110
  noLinks: args.noLinks,
99
111
  strict: args.strict,
@@ -106,6 +118,7 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
106
118
  fontSans: args.fontSans ?? stringFromConfig(config, 'fontSans'),
107
119
  fontMono: args.fontMono ?? stringFromConfig(config, 'fontMono'),
108
120
  headless: args.headless || boolFromConfig(config, 'headlessFonts'),
121
+ useSystemFonts: args.useSystemFonts || boolFromConfig(config, 'useSystemFonts'),
109
122
  scale: args.scale,
110
123
  start: args.start,
111
124
  locale,
@@ -191,6 +204,8 @@ interface ProduceArgs {
191
204
  fontSans?: string;
192
205
  fontMono?: string;
193
206
  headless: boolean;
207
+ /** Opt in to system-font probing for raster/PDF export (bundled-first by default). */
208
+ useSystemFonts: boolean;
194
209
  scale?: string;
195
210
  start?: string;
196
211
  /** Resolved locale override (CLI flag or env-var fallback); undefined falls through to the directive. */
@@ -208,69 +223,67 @@ interface ProduceResult {
208
223
  isBinary: boolean;
209
224
  }
210
225
 
226
+ const BINARY_FORMATS = new Set<OutputFormat>(['png', 'pdf', 'xlsx']);
227
+ const FONT_FORMATS = new Set<OutputFormat>(['png', 'pdf']);
228
+
211
229
  async function produce(args: ProduceArgs): Promise<ProduceResult> {
212
- if (args.format === 'json') {
213
- return { rendered: await produceJson(args), isBinary: false };
214
- }
230
+ // `nowline` canonical-text format is not part of ExportFormat; handle here.
215
231
  if (args.format === 'nowline') {
216
232
  return { rendered: await produceCanonicalNowline(args), isBinary: false };
217
233
  }
218
234
 
219
- // The remaining formats all start from a positioned model + (sometimes) an
220
- // SVG. Build them once and dispatch to the format-specific exporter via
221
- // dynamic import — keeps each exporter's heavy deps off cold paths and
222
- // leaves room to re-extract a package later if a future build profile
223
- // wants to slim down.
224
- const stage = await stageRoadmap(args);
235
+ // Convert JSON-AST input to DSL text before handing to the kernel.
236
+ const sourceText =
237
+ args.inputFormat === 'json'
238
+ ? jsonToNowlineText(args.contents, args.displayPath)
239
+ : args.contents;
240
+
241
+ // Pre-validate with locale-aware diagnostic formatting so validation
242
+ // errors reach the operator in their locale. The kernel will re-parse
243
+ // the same source below — double work accepted in exchange for faithful
244
+ // stderr output.
245
+ await parseAndValidate(sourceText, args);
225
246
 
226
- if (args.format === 'svg') {
227
- return { rendered: stage.svg, isBinary: false };
247
+ const assetRoot = args.assetRoot
248
+ ? path.resolve(args.assetRoot)
249
+ : path.dirname(args.absInputPath);
250
+
251
+ const host = createNodeHostEnv(assetRoot);
252
+
253
+ // Resolve fonts lazily — only for formats that rasterize or embed them.
254
+ let fonts: RenderInputs['fonts'];
255
+ if (FONT_FORMATS.has(args.format)) {
256
+ fonts = await resolveNodeFonts(args);
228
257
  }
258
+
259
+ const kernelInputs: RenderInputs = {
260
+ sourcePath: args.absInputPath,
261
+ today: args.today,
262
+ locale: args.locale ?? 'und',
263
+ theme: args.theme,
264
+ fonts,
265
+ width: args.width,
266
+ noLinks: args.noLinks,
267
+ strict: args.strict,
268
+ pageSize: args.pageSize,
269
+ orientation: parseOrientation(args.orientation),
270
+ marginPt: parseMargin(args.margin),
271
+ pngScale: parseScale(args.scale),
272
+ msprojStart: args.start,
273
+ };
274
+
229
275
  try {
230
- if (args.format === 'html') {
231
- const mod = await import('@nowline/export-html');
232
- const html = await mod.exportHtml(stage.exportInputs, stage.svg);
233
- return { rendered: html, isBinary: false };
234
- }
235
- if (args.format === 'mermaid') {
236
- const mod = await import('@nowline/export-mermaid');
237
- const md = mod.exportMermaid(stage.exportInputs);
238
- return { rendered: md, isBinary: false };
239
- }
240
- if (args.format === 'msproj') {
241
- const mod = await import('@nowline/export-msproj');
242
- const xml = mod.exportMsProjXml(stage.exportInputs, {
243
- startDate: args.start,
244
- });
245
- return { rendered: xml, isBinary: false };
246
- }
247
- if (args.format === 'png') {
248
- const fonts = await stage.fontPair();
249
- const mod = await import('@nowline/export-png');
250
- const png = await mod.exportPng(stage.exportInputs, stage.svg, {
251
- scale: parseScale(args.scale),
252
- fonts,
253
- });
254
- return { rendered: png, isBinary: true };
255
- }
256
- if (args.format === 'pdf') {
257
- const fonts = await stage.fontPair();
258
- const mod = await import('@nowline/export-pdf');
259
- const pdf = await mod.exportPdf(stage.exportInputs, stage.svg, {
260
- pageSize: args.pageSize,
261
- orientation: parseOrientation(args.orientation),
262
- marginPt: parseMargin(args.margin),
263
- fonts,
264
- });
265
- return { rendered: pdf, isBinary: true };
266
- }
267
- if (args.format === 'xlsx') {
268
- const mod = await import('@nowline/export-xlsx');
269
- const xlsx = await mod.exportXlsx(stage.exportInputs, {
270
- generated: args.today,
271
- });
272
- return { rendered: xlsx, isBinary: true };
273
- }
276
+ const bytes = await exportDocument(
277
+ sourceText,
278
+ args.format as ExportFormat,
279
+ kernelInputs,
280
+ host,
281
+ );
282
+ const isBinary = BINARY_FORMATS.has(args.format);
283
+ const rendered: string | Uint8Array = isBinary
284
+ ? bytes
285
+ : new TextDecoder('utf-8').decode(bytes);
286
+ return { rendered, isBinary };
274
287
  } catch (err) {
275
288
  if (err instanceof CliError) throw err;
276
289
  const message = err instanceof Error ? err.message : String(err);
@@ -279,22 +292,100 @@ async function produce(args: ProduceArgs): Promise<ProduceResult> {
279
292
  `nowline: ${args.format} export failed: ${message}`,
280
293
  );
281
294
  }
295
+ }
282
296
 
283
- throw new CliError(ExitCode.InputError, `nowline: unsupported format "${args.format}".`);
297
+ // ---- Node HostEnv -----------------------------------------------------------
298
+
299
+ function createNodeHostEnv(assetRoot: string): HostEnv {
300
+ const root = path.resolve(assetRoot);
301
+ return {
302
+ readSource: async (absPath: string): Promise<string> => {
303
+ return await fs.readFile(absPath, 'utf-8');
304
+ },
305
+ readAsset: async (ref: string): Promise<Uint8Array> => {
306
+ const absPath = path.resolve(root, ref);
307
+ if (!absPath.startsWith(root + path.sep) && absPath !== root) {
308
+ throw new Error(`Asset ${ref} escapes asset-root ${assetRoot}`);
309
+ }
310
+ const bytes = await fs.readFile(absPath);
311
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
312
+ },
313
+ loadWasm: async (): Promise<ArrayBuffer> => {
314
+ // Two code paths:
315
+ //
316
+ // 1. Bun compiled binary: scripts/bun-entry.mjs imports
317
+ // dist/resvg.wasm via `with { type: 'file' }` (the only Bun
318
+ // pattern the bundler embeds) and stashes the VFS path in
319
+ // globalThis.__RESVG_WASM_PATH__. Read it with Bun.file().
320
+ //
321
+ // 2. Plain Node.js (dev, tests, uncompiled dist/index.js run):
322
+ // dist/resvg.wasm is copied by scripts/copy-wasm.mjs (postbuild).
323
+ // Use fs.readFile via a new URL relative to this module.
324
+ // biome-ignore lint/suspicious/noExplicitAny: Bun-specific global not in TS types
325
+ const bunWasmPath = (globalThis as any).__RESVG_WASM_PATH__ as string | undefined;
326
+ if (bunWasmPath) {
327
+ // biome-ignore lint/suspicious/noExplicitAny: Bun-specific global not in TS types
328
+ return (globalThis as any).Bun.file(
329
+ bunWasmPath,
330
+ ).arrayBuffer() as Promise<ArrayBuffer>;
331
+ }
332
+ const wasmUrl = new URL('../resvg.wasm', import.meta.url);
333
+ const bytes = await fs.readFile(wasmUrl);
334
+ return bytes.buffer as ArrayBuffer;
335
+ },
336
+ };
284
337
  }
285
338
 
286
- async function produceJson(args: ProduceArgs): Promise<string> {
287
- if (args.inputFormat === 'json') {
288
- // Re-parse JSON → DSL → JSON to canonicalize through @nowline/core.
289
- const { ast } = parseNowlineJson(args.contents, args.displayPath);
290
- const text = printNowlineFile(ast);
291
- const parsed = await parseAndValidate(text, args);
292
- return JSON.stringify(serializeToJson(parsed.document, text), null, 2);
339
+ // ---- Font resolution --------------------------------------------------------
340
+
341
+ async function resolveNodeFonts(
342
+ args: ProduceArgs,
343
+ ): Promise<import('@nowline/export-core').ResolvedFontPair> {
344
+ const { resolveFonts } = await import('@nowline/export-core');
345
+ const result = await resolveFonts({
346
+ fontSans: args.fontSans,
347
+ fontMono: args.fontMono,
348
+ headless: args.headless,
349
+ useSystemFonts: args.useSystemFonts,
350
+ });
351
+
352
+ // A variable font was explicitly requested but cannot be rasterized; we
353
+ // substituted bundled DejaVu. Always warn — and fail under --strict, since
354
+ // the requested font silently did not apply.
355
+ const vfSubstituted = result.sansVariableFontSubstituted || result.monoVariableFontSubstituted;
356
+ if (vfSubstituted) {
357
+ const roles = [
358
+ result.sansVariableFontSubstituted ? 'sans' : null,
359
+ result.monoVariableFontSubstituted ? 'mono' : null,
360
+ ].filter(Boolean) as string[];
361
+ const message = `requested variable font(s) for ${roles.join(
362
+ ', ',
363
+ )} cannot be rasterized; substituted bundled DejaVu`;
364
+ if (args.strict) {
365
+ throw new CliError(ExitCode.InputError, `nowline: ${message}`);
366
+ }
367
+ process.stderr.write(`warning: ${message}\n`);
293
368
  }
294
- const parsed = await parseAndValidate(args.contents, args);
295
- return JSON.stringify(serializeToJson(parsed.document, args.contents), null, 2);
369
+
370
+ // Bundled fallback after an opted-in probe found no usable system font.
371
+ // Only meaningful when --use-system-fonts is set; warn under --strict.
372
+ if (args.strict) {
373
+ if (result.sansFellBackToBundled) {
374
+ process.stderr.write(
375
+ 'warning: sans font fell back to bundled DejaVu (no platform font found)\n',
376
+ );
377
+ }
378
+ if (result.monoFellBackToBundled) {
379
+ process.stderr.write(
380
+ 'warning: mono font fell back to bundled DejaVu (no platform font found)\n',
381
+ );
382
+ }
383
+ }
384
+ return { sans: result.sans, mono: result.mono };
296
385
  }
297
386
 
387
+ // ---- nowline canonical-text format (not part of ExportFormat) ---------------
388
+
298
389
  async function produceCanonicalNowline(args: ProduceArgs): Promise<string> {
299
390
  if (args.inputFormat === 'json') {
300
391
  const { ast } = parseNowlineJson(args.contents, args.displayPath);
@@ -305,93 +396,7 @@ async function produceCanonicalNowline(args: ProduceArgs): Promise<string> {
305
396
  return printNowlineFile(doc.ast);
306
397
  }
307
398
 
308
- interface StagedRoadmap {
309
- svg: string;
310
- exportInputs: import('@nowline/export-core').ExportInputs;
311
- /** Lazy: only loads the resolved font pair when a format actually needs it. */
312
- fontPair: () => Promise<import('@nowline/export-core').ResolvedFontPair>;
313
- }
314
-
315
- async function stageRoadmap(args: ProduceArgs): Promise<StagedRoadmap> {
316
- const parsed = await parseAndValidate(
317
- args.inputFormat === 'json'
318
- ? jsonToNowlineText(args.contents, args.displayPath)
319
- : args.contents,
320
- args,
321
- );
322
- const resolved = await resolveIncludes(parsed.ast, args.absInputPath, {
323
- services: getServices().Nowline,
324
- });
325
- for (const diag of resolved.diagnostics) {
326
- if (diag.severity === 'error') {
327
- process.stderr.write(`${diag.sourcePath}: ${diag.message}\n`);
328
- }
329
- }
330
- if (resolved.diagnostics.some((d) => d.severity === 'error')) {
331
- throw new CliError(ExitCode.ValidationError, '');
332
- }
333
-
334
- const model = layoutRoadmap(parsed.ast, resolved, {
335
- theme: args.theme,
336
- today: args.today,
337
- width: args.width,
338
- locale: args.locale,
339
- });
340
-
341
- const assetRoot = args.assetRoot
342
- ? path.resolve(args.assetRoot)
343
- : path.dirname(args.absInputPath);
344
- const resolver: AssetResolver = createAssetResolver(assetRoot);
345
-
346
- const warnings: string[] = [];
347
- const svg = await renderSvg(model, {
348
- assetResolver: resolver,
349
- noLinks: args.noLinks,
350
- strict: args.strict,
351
- warn: (msg) => warnings.push(msg),
352
- });
353
-
354
- for (const w of warnings) {
355
- process.stderr.write(`warning: ${w}\n`);
356
- }
357
-
358
- let cachedFonts: import('@nowline/export-core').ResolvedFontPair | undefined;
359
- const fontPair = async () => {
360
- if (cachedFonts) return cachedFonts;
361
- const mod = await import('@nowline/export-core');
362
- const result = await mod.resolveFonts({
363
- fontSans: args.fontSans,
364
- fontMono: args.fontMono,
365
- headless: args.headless,
366
- });
367
- if (args.strict) {
368
- if (result.sansFellBackToBundled) {
369
- process.stderr.write(
370
- 'warning: sans font fell back to bundled DejaVu (no platform font found)\n',
371
- );
372
- }
373
- if (result.monoFellBackToBundled) {
374
- process.stderr.write(
375
- 'warning: mono font fell back to bundled DejaVu (no platform font found)\n',
376
- );
377
- }
378
- }
379
- cachedFonts = { sans: result.sans, mono: result.mono };
380
- return cachedFonts;
381
- };
382
-
383
- return {
384
- svg,
385
- exportInputs: {
386
- ast: parsed.ast,
387
- resolved,
388
- model,
389
- sourcePath: args.displayPath,
390
- today: args.today,
391
- },
392
- fontPair,
393
- };
394
- }
399
+ // ---- Shared helpers ---------------------------------------------------------
395
400
 
396
401
  function jsonToNowlineText(contents: string, displayPath: string): string {
397
402
  const { ast } = parseNowlineJson(contents, displayPath);
@@ -443,31 +448,41 @@ function parseTheme(raw: string | undefined): ThemeName {
443
448
  return theme;
444
449
  }
445
450
 
446
- // Resolve the now-line date from the CLI flag.
451
+ // Resolve the now-line date from the CLI flags.
447
452
  //
448
453
  // Precedence:
449
- // 1. `--now -` → undefined (suppresses the now-line)
450
- // 2. `--now <YYYY-MM-DD>` that date
451
- // 3. flag omitted today (UTC calendar date)
454
+ // 1. `--now -` → undefined (suppress now-line)
455
+ // 2. `--now YYYY-MM-DD` floating date; zone ignored
456
+ // 3. `--now YYYY-MM-DDTHH:MM:SS[±HH:MM]` civil date at embedded offset; --timezone ignored
457
+ // 4. `--now YYYY-MM-DDTHH:MM:SSZ` → civil date in UTC; --timezone ignored
458
+ // 5. `--now YYYY-MM-DDTHH:MM:SS` → floating; written date part; zone ignored
459
+ // 6. flag omitted → civil date of today in --timezone (default: local)
452
460
  //
453
- // The "default to today" behavior matches what the tool's name promises
454
- // you should see a "now" line by default. Use `--now -` to opt out (Unix
455
- // `-` sentinel, mirroring `-o -` for stdout), or `--now <date>` for
456
- // deterministic snapshots / planning a hypothetical date.
457
- function resolveNowArg(args: { now?: string }): Date | undefined {
458
- if (args.now === '-') return undefined;
459
- if (args.now) {
460
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(args.now);
461
- if (!m) {
462
- throw new CliError(
463
- ExitCode.InputError,
464
- `nowline: invalid --now "${args.now}". Expected YYYY-MM-DD or "-".`,
465
- );
461
+ // `--timezone` is only consulted for case 6. Authored dates in the roadmap
462
+ // (bars, milestones, axis) are floating and are never affected.
463
+ function resolveNowCli(args: { now?: string; timezone?: string }): Date | undefined {
464
+ let zone: NormalizedZone | undefined;
465
+ if (args.timezone) {
466
+ try {
467
+ zone = normalizeZone(args.timezone);
468
+ } catch (err) {
469
+ if (err instanceof TimezoneError) {
470
+ throw new CliError(ExitCode.InputError, err.message);
471
+ }
472
+ throw err;
466
473
  }
467
- return new Date(Date.UTC(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)));
468
474
  }
469
- const today = new Date();
470
- return new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
475
+ const result = resolveToday({ now: args.now, zone });
476
+ // resolveToday returns undefined for unrecognised strings; surface that as
477
+ // an input error so the user gets a clear message instead of a silent no-line.
478
+ if (result === undefined && args.now !== undefined && args.now !== '-') {
479
+ throw new CliError(
480
+ ExitCode.InputError,
481
+ `nowline: invalid --now "${args.now}". ` +
482
+ `Expected YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], or "-".`,
483
+ );
484
+ }
485
+ return result;
471
486
  }
472
487
 
473
488
  function parseScale(raw: string | undefined): number | undefined {
@@ -544,6 +559,8 @@ function emitDiagnostics(
544
559
  if (rendered) process.stderr.write(`${rendered}\n`);
545
560
  }
546
561
 
562
+ // ---- Asset resolver (still used by serve.ts) --------------------------------
563
+
547
564
  export function createAssetResolver(assetRoot: string): AssetResolver {
548
565
  const root = path.resolve(assetRoot);
549
566
  return async (ref: string) => {
@@ -2,7 +2,14 @@ import { type FSWatcher, promises as fs, watch as fsWatch } from 'node:fs';
2
2
  import * as http from 'node:http';
3
3
  import * as path from 'node:path';
4
4
  import { resolveIncludes } from '@nowline/core';
5
- import { layoutRoadmap, type ThemeName } from '@nowline/layout';
5
+ import {
6
+ layoutRoadmap,
7
+ type NormalizedZone,
8
+ normalizeZone,
9
+ resolveToday,
10
+ type ThemeName,
11
+ TimezoneError,
12
+ } from '@nowline/layout';
6
13
  import { renderSvg } from '@nowline/renderer';
7
14
  import type { ParsedArgs } from '../cli/args.js';
8
15
  import { getServices, parseSource } from '../core/parse.js';
@@ -218,17 +225,29 @@ function parseTheme(raw: string | undefined): ThemeName {
218
225
  return lower;
219
226
  }
220
227
 
221
- // Mirrors render.ts: `--now -` disables the line, `--now <date>` overrides
222
- // it, and the default is today's UTC calendar date.
223
- function resolveNowArg(args: { now?: string }): Date | undefined {
224
- if (args.now === '-') return undefined;
225
- if (args.now) {
226
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(args.now);
227
- if (!m) throw new CliError(ExitCode.InputError, `nowline: invalid --now "${args.now}".`);
228
- return new Date(Date.UTC(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)));
228
+ // Resolve the now-line date from the CLI flags. Mirrors render.ts.
229
+ // Delegates to the shared resolveToday; see render.ts for the full precedence table.
230
+ function resolveNowArg(args: { now?: string; timezone?: string }): Date | undefined {
231
+ let zone: NormalizedZone | undefined;
232
+ if (args.timezone) {
233
+ try {
234
+ zone = normalizeZone(args.timezone);
235
+ } catch (err) {
236
+ if (err instanceof TimezoneError) {
237
+ throw new CliError(ExitCode.InputError, err.message);
238
+ }
239
+ throw err;
240
+ }
241
+ }
242
+ const result = resolveToday({ now: args.now, zone });
243
+ if (result === undefined && args.now !== undefined && args.now !== '-') {
244
+ throw new CliError(
245
+ ExitCode.InputError,
246
+ `nowline: invalid --now "${args.now}". ` +
247
+ `Expected YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], or "-".`,
248
+ );
229
249
  }
230
- const t = new Date();
231
- return new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth(), t.getUTCDate()));
250
+ return result;
232
251
  }
233
252
 
234
253
  function sendEvent(res: http.ServerResponse, event: string, data: string): void {
@@ -1,6 +1,6 @@
1
1
  // Auto-generated by scripts/bundle-templates.mjs. Do not edit by hand.
2
2
 
3
- export const CLI_VERSION = "0.5.1";
3
+ export const CLI_VERSION = "0.6.0";
4
4
 
5
5
  export interface CliBuild {
6
6
  /** Short git SHA at build time, or empty when not in a git checkout. */
@@ -12,7 +12,7 @@ export interface CliBuild {
12
12
  }
13
13
 
14
14
  export const CLI_BUILD: CliBuild = {
15
- "sha": "fc40638",
15
+ "sha": "e2e4d9d",
16
16
  "isRelease": true,
17
17
  "isDirty": false
18
18
  };
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { parseArgv } from './cli/args.js';
3
3
  import { renderHelp, renderVersion } from './cli/help.js';
4
4
  import { initHandler } from './commands/init.js';
5
+ import { mcpHandler } from './commands/mcp.js';
5
6
  import { renderHandler } from './commands/render.js';
6
7
  import { serveHandler } from './commands/serve.js';
7
8
  import { CliError, ExitCode } from './io/exit-codes.js';
@@ -30,6 +31,8 @@ async function run(): Promise<number> {
30
31
  await initHandler({ args: parsed });
31
32
  } else if (parsed.mode === 'serve') {
32
33
  await serveHandler({ args: parsed });
34
+ } else if (parsed.mode === 'mcp') {
35
+ await mcpHandler({ args: parsed });
33
36
  } else {
34
37
  await renderHandler({ args: parsed });
35
38
  }