@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.
- package/dist/cli/args.d.ts +17 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +16 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +42 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/commands/mcp.d.ts +12 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +20 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/render.d.ts +1 -1
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +149 -156
- package/dist/commands/render.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +20 -12
- package/dist/commands/serve.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +2 -2
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/resvg.wasm +0 -0
- package/man/nowline.1 +41 -3
- package/package.json +17 -12
- package/scripts/bun-entry.mjs +25 -0
- package/scripts/compile.mjs +11 -6
- package/scripts/copy-wasm.mjs +23 -0
- package/src/cli/args.ts +35 -4
- package/src/cli/help.ts +42 -2
- package/src/commands/mcp.ts +21 -0
- package/src/commands/render.ts +194 -177
- package/src/commands/serve.ts +30 -11
- package/src/generated/version.ts +2 -2
- package/src/index.ts +3 -0
package/src/commands/render.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import {
|
|
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 {
|
|
6
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
451
|
+
// Resolve the now-line date from the CLI flags.
|
|
447
452
|
//
|
|
448
453
|
// Precedence:
|
|
449
|
-
// 1. `--now -`
|
|
450
|
-
// 2. `--now
|
|
451
|
-
// 3.
|
|
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
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
470
|
-
|
|
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) => {
|
package/src/commands/serve.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
222
|
-
//
|
|
223
|
-
function resolveNowArg(args: { now?: string }): Date | undefined {
|
|
224
|
-
|
|
225
|
-
if (args.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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 {
|
package/src/generated/version.ts
CHANGED
|
@@ -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.
|
|
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": "
|
|
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
|
}
|