@nowline/cli 0.5.0 → 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/man/nowline.1 CHANGED
@@ -10,6 +10,7 @@
10
10
  .Op Fl o Ar path
11
11
  .Op Fl t Ar theme
12
12
  .Op Fl -now Ar date
13
+ .Op Fl -timezone Ar zone
13
14
  .Op Ar options
14
15
  .Ar input
15
16
  .Nm
@@ -182,13 +183,50 @@ Default: inferred from extension; standard input defaults to
182
183
  .Sy light | dark .
183
184
  .It Fl -now Ar date
184
185
  .Sq Now
185
- anchor for the now-line and date math, in
186
+ anchor for the now-line and date math.
187
+ Accepts a bare
186
188
  .Pa YYYY-MM-DD
187
- form.
188
- Default: today (the OS calendar date in UTC).
189
+ (floating; zone-independent) or a full ISO\~8601 instant such as
190
+ .Pa 2026-06-04T23:00:00Z
191
+ or
192
+ .Pa 2026-06-05T06:00:00+07:00
193
+ (an embedded Z or \(pmHH:MM offset overrides
194
+ .Fl -timezone ) .
195
+ A bare date or an instant without an offset is floating: the written date
196
+ part is used and
197
+ .Fl -timezone
198
+ is ignored.
199
+ Default: today's civil date in the host zone (or
200
+ .Fl -timezone
201
+ if given).
189
202
  Pass
190
203
  .Fl -now Sq -
191
204
  to suppress the now-line entirely.
205
+ .It Fl -timezone Ar zone
206
+ Timezone used for the clock-based
207
+ .Dq today
208
+ default; only consulted when
209
+ .Fl -now
210
+ is omitted.
211
+ Accepted forms:
212
+ .Bl -tag -width "America/Los_Angeles" -compact
213
+ .It Sy local
214
+ Host/viewer zone (default when the flag is absent).
215
+ .It Sy UTC
216
+ UTC.
217
+ .It Sy Z , +00:00
218
+ Zero-offset shorthand (equivalent to
219
+ .Sy UTC ) .
220
+ .It Sy \(pmHH , \(pmHH:MM , \(pmHHMM
221
+ Fixed offset (DST-naive; documented caveat).
222
+ .It Sy America/Los_Angeles
223
+ Any valid IANA timezone name.
224
+ .El
225
+ Ambiguous abbreviations such as
226
+ .Sy PST
227
+ and
228
+ .Sy IST
229
+ are rejected with a clear error.
192
230
  .It Fl -locale Ar bcp47
193
231
  BCP-47 language tag for the operator's CLI message output (validator
194
232
  diagnostics on standard error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowline/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Nowline command-line interface — validate, convert, init",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -32,24 +32,28 @@
32
32
  "dependencies": {
33
33
  "@babel/code-frame": "^7.29.0",
34
34
  "@clack/prompts": "^1.4.0",
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
36
  "chalk": "^5.6.2",
36
37
  "citty": "^0.2.2",
37
38
  "consola": "^3.4.2",
38
39
  "js-yaml": "^4.1.1",
39
40
  "langium": "~4.2.4",
40
- "@nowline/config": "0.5.0",
41
- "@nowline/export-core": "0.5.0",
42
- "@nowline/core": "0.5.0",
43
- "@nowline/export-html": "0.5.0",
44
- "@nowline/export-mermaid": "0.5.0",
45
- "@nowline/export-pdf": "0.5.0",
46
- "@nowline/export-msproj": "0.5.0",
47
- "@nowline/export-png": "0.5.0",
48
- "@nowline/export-xlsx": "0.5.0",
49
- "@nowline/layout": "0.5.0",
50
- "@nowline/renderer": "0.5.0"
41
+ "@nowline/config": "0.6.0",
42
+ "@nowline/export": "0.6.0",
43
+ "@nowline/mcp": "0.6.0",
44
+ "@nowline/export-html": "0.6.0",
45
+ "@nowline/core": "0.6.0",
46
+ "@nowline/export-core": "0.6.0",
47
+ "@nowline/export-mermaid": "0.6.0",
48
+ "@nowline/export-pdf": "0.6.0",
49
+ "@nowline/export-png": "0.6.0",
50
+ "@nowline/export-xlsx": "0.6.0",
51
+ "@nowline/renderer": "0.6.0",
52
+ "@nowline/export-msproj": "0.6.0",
53
+ "@nowline/layout": "0.6.0"
51
54
  },
52
55
  "devDependencies": {
56
+ "@resvg/resvg-wasm": "^2.6.2",
53
57
  "@types/babel__code-frame": "^7.27.0",
54
58
  "@types/js-yaml": "^4.0.9",
55
59
  "@types/node": "^25.9.1",
@@ -60,6 +64,7 @@
60
64
  "bundle-templates": "node scripts/bundle-templates.mjs",
61
65
  "prebuild": "node scripts/bundle-templates.mjs",
62
66
  "build": "tsc -b tsconfig.json",
67
+ "postbuild": "node scripts/copy-wasm.mjs",
63
68
  "watch": "tsc -b tsconfig.json --watch",
64
69
  "pretest": "node scripts/bundle-templates.mjs",
65
70
  "test": "vitest run",
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bun
2
+ // Bun-only entry point for `bun build --compile`.
3
+ //
4
+ // `import ... with { type: 'file' }` is the ONLY pattern Bun's static
5
+ // analyzer recognises for embedding binary assets in a standalone binary.
6
+ // `Bun.file(new URL(..., import.meta.url))` is NOT tracked by the bundler
7
+ // and leaves the asset out of the embedded VFS.
8
+ //
9
+ // This shim:
10
+ // 1. Imports resvg.wasm via the recognised pattern, yielding a VFS path.
11
+ // 2. Stashes that path in globalThis so loadWasm() (in dist/index.js) can
12
+ // read it with Bun.file() at runtime.
13
+ // 3. Delegates immediately to dist/index.js (the normal Node entry).
14
+ //
15
+ // compile.mjs passes this file as the bun --compile entry instead of
16
+ // dist/index.js directly.
17
+
18
+ // @ts-expect-error — Bun-specific import assertion; not valid Node.js/TypeScript.
19
+ import resvgWasmPath from '../dist/resvg.wasm' with { type: 'file' };
20
+
21
+ // Make the VFS path available to loadWasm() in dist/commands/render.js.
22
+ globalThis.__RESVG_WASM_PATH__ = resvgWasmPath;
23
+
24
+ // Run the CLI.
25
+ await import('../dist/index.js');
@@ -25,9 +25,9 @@ const packageRoot = path.resolve(__dirname, '..');
25
25
  // Tight by design: a breach should trigger the conversation called out in
26
26
  // `specs/cli-distribution.md` "Size budget", not be silently absorbed.
27
27
  //
28
- // Last measured (bun 1.3.13) using --target on macOS-arm64:
29
- // darwin-arm64=70 darwin-x64=75 linux-arm64=107 linux-x64=107
30
- // windows-arm64=119 windows-x64=122
28
+ // Last measured (bun 1.3.14, @resvg/resvg-wasm, no native .node) on macOS-arm64:
29
+ // darwin-arm64=67 darwin-x64=~72 linux-arm64=~104 linux-x64=~104
30
+ // windows-arm64=~116 windows-x64=~119
31
31
  const ALL_TARGETS = [
32
32
  { id: 'bun-darwin-arm64', suffix: 'macos-arm64', maxMb: 80 },
33
33
  { id: 'bun-darwin-x64', suffix: 'macos-x64', maxMb: 85 },
@@ -80,10 +80,15 @@ function main() {
80
80
  }
81
81
  }
82
82
 
83
- const entry = path.join(packageRoot, 'dist', 'index.js');
84
- if (!safeStat(entry)) {
83
+ // Use the bun-entry shim as the compile entry point so Bun's static
84
+ // analyzer sees the `import ... with { type: 'file' }` declaration that
85
+ // embeds resvg.wasm in the binary. The shim sets __RESVG_WASM_PATH__
86
+ // and then delegates to dist/index.js.
87
+ const entry = path.join(packageRoot, 'scripts', 'bun-entry.mjs');
88
+ const distEntry = path.join(packageRoot, 'dist', 'index.js');
89
+ if (!safeStat(distEntry)) {
85
90
  console.error(
86
- `error: expected ${path.relative(packageRoot, entry)} to exist; run \`pnpm build\` first.`,
91
+ `error: expected ${path.relative(packageRoot, distEntry)} to exist; run \`pnpm build\` first.`,
87
92
  );
88
93
  process.exit(1);
89
94
  }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // Copy @resvg/resvg-wasm/index_bg.wasm → dist/resvg.wasm so the CLI can
3
+ // locate it at runtime (both uncompiled Node and bun --compile, which embeds
4
+ // files referenced via new URL('./resvg.wasm', import.meta.url)).
5
+ //
6
+ // Runs as the `postbuild` step, after tsc has created dist/.
7
+
8
+ import { copyFile, mkdir } from 'node:fs/promises';
9
+ import { createRequire } from 'node:module';
10
+ import { dirname, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const packageRoot = resolve(__dirname, '..');
15
+
16
+ const req = createRequire(import.meta.url);
17
+ const wasmEntry = req.resolve('@resvg/resvg-wasm');
18
+ const wasmSrc = resolve(dirname(wasmEntry), 'index_bg.wasm');
19
+ const wasmDest = resolve(packageRoot, 'dist', 'resvg.wasm');
20
+
21
+ await mkdir(resolve(packageRoot, 'dist'), { recursive: true });
22
+ await copyFile(wasmSrc, wasmDest);
23
+ console.log(`copied resvg.wasm → dist/resvg.wasm`);
package/src/cli/args.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ParseArgsConfig, parseArgs } from 'node:util';
2
2
  import { CliError, ExitCode } from '../io/exit-codes.js';
3
3
 
4
- export type ModeKind = 'render' | 'serve' | 'init' | 'help' | 'version';
4
+ export type ModeKind = 'render' | 'serve' | 'init' | 'mcp' | 'help' | 'version';
5
5
 
6
6
  export interface ParsedArgs {
7
7
  /** Resolved mode after dispatch (mutual-exclusivity already checked). */
@@ -23,9 +23,16 @@ export interface ParsedArgs {
23
23
  /** Now-line date string from `--now`. Undefined means "use the actual
24
24
  * current date" (the default). The literal value `"-"` means
25
25
  * "suppress the now-line entirely" (mirrors the Unix-`-` convention
26
- * used elsewhere in the CLI). Otherwise expected as YYYY-MM-DD;
27
- * parsed downstream by resolveNowArg. */
26
+ * used elsewhere in the CLI). Accepts YYYY-MM-DD (floating) or a
27
+ * full ISO 8601 instant with Z/offset; parsed downstream by resolveToday. */
28
28
  now?: string;
29
+ /**
30
+ * IANA timezone name, ISO 8601 offset, `"UTC"`, `"Z"`, or `"local"`.
31
+ * Governs which civil date is "today" when `--now` is omitted.
32
+ * Defaults to the host's local zone when absent.
33
+ * Ignored when `--now` carries an explicit date or embedded offset.
34
+ */
35
+ timezone?: string;
29
36
  noLinks: boolean;
30
37
  scale?: string;
31
38
  strict: boolean;
@@ -39,6 +46,12 @@ export interface ParsedArgs {
39
46
  fontSans?: string;
40
47
  fontMono?: string;
41
48
  headless: boolean;
49
+ /**
50
+ * Opt in to the platform font probe for raster/PDF export. Off by default
51
+ * (bundled-first): exports use the bundled DejaVu pair on every OS so the
52
+ * output is identical cross-platform and matches the live preview.
53
+ */
54
+ useSystemFonts: boolean;
42
55
  start?: string;
43
56
 
44
57
  /**
@@ -59,6 +72,9 @@ export interface ParsedArgs {
59
72
 
60
73
  // Init
61
74
  template?: string;
75
+
76
+ // MCP
77
+ root?: string;
62
78
  }
63
79
 
64
80
  /**
@@ -78,6 +94,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
78
94
  strict: false,
79
95
  open: false,
80
96
  headless: false,
97
+ useSystemFonts: false,
81
98
  };
82
99
  }
83
100
 
@@ -97,6 +114,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
97
114
 
98
115
  serve: { type: 'boolean' },
99
116
  init: { type: 'boolean' },
117
+ mcp: { type: 'boolean' },
100
118
  'dry-run': { type: 'boolean', short: 'n' },
101
119
 
102
120
  theme: { type: 'string', short: 't' },
@@ -105,6 +123,9 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
105
123
  // suppress the now-line entirely (Unix-`-` sentinel; same idea
106
124
  // as `-o -` for stdout).
107
125
  now: { type: 'string' },
126
+ // `--timezone` controls which civil date is "today" when --now is
127
+ // omitted. Accepts IANA names, ISO offsets, "UTC", "Z", or "local".
128
+ timezone: { type: 'string' },
108
129
  'no-links': { type: 'boolean' },
109
130
  scale: { type: 'string', short: 's' },
110
131
  strict: { type: 'boolean' },
@@ -119,6 +140,9 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
119
140
 
120
141
  template: { type: 'string' },
121
142
 
143
+ // MCP
144
+ root: { type: 'string' },
145
+
122
146
  // Format-specific (m2c)
123
147
  'page-size': { type: 'string' },
124
148
  orientation: { type: 'string' },
@@ -126,6 +150,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
126
150
  'font-sans': { type: 'string' },
127
151
  'font-mono': { type: 'string' },
128
152
  headless: { type: 'boolean' },
153
+ 'use-system-fonts': { type: 'boolean' },
129
154
  start: { type: 'string' },
130
155
 
131
156
  // Localization (m-loc-b)
@@ -153,6 +178,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
153
178
  strict: false,
154
179
  open: false,
155
180
  headless: false,
181
+ useSystemFonts: false,
156
182
  };
157
183
  }
158
184
  if (values.version === true) {
@@ -164,6 +190,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
164
190
  strict: false,
165
191
  open: false,
166
192
  headless: false,
193
+ useSystemFonts: false,
167
194
  };
168
195
  }
169
196
 
@@ -177,6 +204,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
177
204
  const modes: ModeKind[] = [];
178
205
  if (values.serve === true) modes.push('serve');
179
206
  if (values.init === true) modes.push('init');
207
+ if (values.mcp === true) modes.push('mcp');
180
208
  if (modes.length > 1) {
181
209
  throw new CliError(
182
210
  ExitCode.InputError,
@@ -187,7 +215,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
187
215
  const dryRun = values['dry-run'] === true;
188
216
  const mode: ModeKind = modes[0] ?? 'render';
189
217
 
190
- if (dryRun && (mode === 'serve' || mode === 'init')) {
218
+ if (dryRun && (mode === 'serve' || mode === 'init' || mode === 'mcp')) {
191
219
  throw new CliError(
192
220
  ExitCode.InputError,
193
221
  `nowline: --dry-run cannot be combined with --${mode}.`,
@@ -215,6 +243,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
215
243
  inputFormat: stringOrUndefined(values['input-format']),
216
244
  theme: stringOrUndefined(values.theme),
217
245
  now: stringOrUndefined(values.now),
246
+ timezone: stringOrUndefined(values.timezone),
218
247
  noLinks: values['no-links'] === true,
219
248
  scale: stringOrUndefined(values.scale),
220
249
  strict: values.strict === true,
@@ -231,8 +260,10 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
231
260
  fontSans: stringOrUndefined(values['font-sans']),
232
261
  fontMono: stringOrUndefined(values['font-mono']),
233
262
  headless: values.headless === true,
263
+ useSystemFonts: values['use-system-fonts'] === true,
234
264
  start: stringOrUndefined(values.start),
235
265
  locale: stringOrUndefined(values.locale),
266
+ root: stringOrUndefined(values.root),
236
267
  };
237
268
  }
238
269
 
package/src/cli/help.ts CHANGED
@@ -31,14 +31,33 @@ MODE FLAGS (mutually exclusive)
31
31
  rendered output to disk on each rebuild.
32
32
  --init [<name>] Scaffold a starter .nowline file in cwd. Positional
33
33
  becomes project name; .nowline appended if missing.
34
+ --mcp Start a Model Context Protocol (MCP) stdio server
35
+ exposing validate, render, export, and file tools.
36
+ Shares the same @nowline/mcp server code as
37
+ \`npx @nowline/mcp\`.
34
38
  -n, --dry-run Run the full pipeline (parse + validate + layout +
35
39
  format) but skip the write step. Subsumes the old
36
40
  'validate' verb. Exit 0 on success, 1 on errors.
37
41
 
38
42
  RENDER OPTIONS
39
43
  -t, --theme <name> light | dark | grayscale (greyscale alias)
40
- --now <YYYY-MM-DD> Date for the now-line. Default: today.
41
- Use --now - to suppress the now-line.
44
+ --now <date> Date for the now-line. Default: today (local civil date).
45
+ Accepts YYYY-MM-DD (floating date, zone-independent) or a
46
+ full ISO 8601 instant: YYYY-MM-DDTHH:MM:SSZ or with a
47
+ ±HH:MM offset. A bare date or ISO date without an offset is
48
+ treated as floating (written date part used, --timezone
49
+ ignored). An embedded Z or ±offset overrides --timezone.
50
+ Use --now - to suppress the now-line entirely.
51
+ --timezone <zone> Timezone for the clock-based "today" default.
52
+ Only consulted when --now is omitted; ignored when --now
53
+ carries an explicit date or embedded ISO offset.
54
+ Accepted forms:
55
+ local — host/viewer zone (default)
56
+ UTC — UTC
57
+ Z, +00:00 — zero-offset shorthand (= UTC)
58
+ ±HH, ±HH:MM, ±HHMM — fixed offset (DST-naive)
59
+ America/Los_Angeles — IANA timezone name
60
+ Ambiguous abbreviations (PST, IST, CET) are rejected.
42
61
  --no-links Omit link icons from rendered items.
43
62
  -s, --scale <n> Raster scale factor (PNG only; default 1).
44
63
  --strict Promote asset / sanitizer warnings to errors.
@@ -52,11 +71,30 @@ RENDER OPTIONS
52
71
  wins for content. Falls back to LC_ALL /
53
72
  LC_MESSAGES / LANG, then en-US.
54
73
 
74
+ FONT OPTIONS (png, pdf)
75
+ Raster/PDF export is bundled-first: by default it
76
+ renders with the bundled DejaVu pair on every OS, so
77
+ output is identical cross-platform and matches the
78
+ live preview.
79
+ --font-sans <ref> Override the sans font: a .ttf/.otf/.ttc path or a
80
+ known alias. A variable font is not rasterizable and
81
+ is replaced by bundled DejaVu (error under --strict).
82
+ --font-mono <ref> Override the mono font (path or alias; same rules).
83
+ --use-system-fonts Opt in to the platform font probe; the first static
84
+ installed font wins (variable fonts skipped), bundled
85
+ if none. Output then varies by machine.
86
+ --headless Force the bundled DejaVu pair, ignoring system fonts
87
+ and --use-system-fonts. Implied in CI without a TTY.
88
+
55
89
  SERVE OPTIONS
56
90
  -p, --port <n> Port (default: 4318).
57
91
  --host <host> Bind address (default: 127.0.0.1).
58
92
  --open Open the browser on start.
59
93
 
94
+ MCP OPTIONS
95
+ --root <dir> Allowed-root directory for file tools (default: cwd).
96
+ File paths are resolved and restricted to this root.
97
+
60
98
  LOGGING (mutually exclusive)
61
99
  -v, --verbose Print extra diagnostics to stderr.
62
100
  -q, --quiet Suppress non-error stderr.
@@ -75,6 +113,8 @@ EXAMPLES
75
113
  nowline roadmap.nowline --dry-run # validate-only
76
114
  nowline roadmap.nowline --serve -p 8080 # live preview
77
115
  nowline --init my-project # scaffold ./my-project.nowline
116
+ nowline --mcp # start MCP stdio server in cwd
117
+ nowline --mcp --root ./roadmaps # MCP server with custom root
78
118
 
79
119
  EXIT CODES
80
120
  0 Success
@@ -0,0 +1,21 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { createMcpServer } from '@nowline/mcp/server';
3
+ import type { ParsedArgs } from '../cli/args.js';
4
+
5
+ /**
6
+ * `nowline --mcp` handler.
7
+ *
8
+ * Starts a Model Context Protocol stdio server sharing the same @nowline/mcp
9
+ * server factory as `npx @nowline/mcp`. Runs until the process receives
10
+ * SIGINT/SIGTERM or the client closes stdin.
11
+ */
12
+ export async function mcpHandler({ args }: { args: ParsedArgs }): Promise<void> {
13
+ const root = args.root ?? process.cwd();
14
+ const server = createMcpServer({ allowedRoot: root });
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ // Keep alive: the transport closes when stdin closes or the process is signalled.
18
+ await new Promise<void>((resolve) => {
19
+ transport.onclose = resolve;
20
+ });
21
+ }