@nowline/cli 0.8.6 → 0.10.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/README.md +32 -9
- package/dist/cli/args.d.ts +20 -4
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +20 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +31 -3
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/options.d.ts +74 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +209 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp-install.d.ts +18 -0
- package/dist/commands/mcp-install.d.ts.map +1 -0
- package/dist/commands/mcp-install.js +151 -0
- package/dist/commands/mcp-install.js.map +1 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +7 -12
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/render.d.ts +7 -0
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +161 -155
- package/dist/commands/render.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +87 -60
- package/dist/commands/serve.js.map +1 -1
- package/dist/diagnostics/adapt.d.ts +12 -1
- package/dist/diagnostics/adapt.d.ts.map +1 -1
- package/dist/diagnostics/adapt.js +53 -1
- package/dist/diagnostics/adapt.js.map +1 -1
- package/dist/diagnostics/model.d.ts +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.d.ts.map +1 -1
- package/dist/generated/version.js +2 -2
- package/dist/generated/version.js.map +1 -1
- package/dist/i18n/locale.d.ts.map +1 -1
- package/dist/i18n/locale.js +2 -5
- package/dist/i18n/locale.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/io/config.d.ts +1 -1
- package/dist/io/config.d.ts.map +1 -1
- package/dist/io/config.js +1 -1
- package/dist/io/config.js.map +1 -1
- package/dist/io/log.d.ts +17 -0
- package/dist/io/log.d.ts.map +1 -0
- package/dist/io/log.js +16 -0
- package/dist/io/log.js.map +1 -0
- package/dist/mcp-install/clients.d.ts +58 -0
- package/dist/mcp-install/clients.d.ts.map +1 -0
- package/dist/mcp-install/clients.js +304 -0
- package/dist/mcp-install/clients.js.map +1 -0
- package/dist/mcp-install/host.d.ts +29 -0
- package/dist/mcp-install/host.d.ts.map +1 -0
- package/dist/mcp-install/host.js +72 -0
- package/dist/mcp-install/host.js.map +1 -0
- package/dist/mcp-install/json-config.d.ts +29 -0
- package/dist/mcp-install/json-config.d.ts.map +1 -0
- package/dist/mcp-install/json-config.js +139 -0
- package/dist/mcp-install/json-config.js.map +1 -0
- package/dist/mcp-install/server-command.d.ts +26 -0
- package/dist/mcp-install/server-command.d.ts.map +1 -0
- package/dist/mcp-install/server-command.js +52 -0
- package/dist/mcp-install/server-command.js.map +1 -0
- package/man/fr/nowline.1 +248 -19
- package/man/fr/nowline.5 +819 -45
- package/man/nowline.1 +193 -18
- package/man/nowline.5 +764 -36
- package/package.json +19 -19
- package/src/cli/args.ts +55 -5
- package/src/cli/help.ts +31 -3
- package/src/cli/options.ts +286 -0
- package/src/commands/init.ts +2 -3
- package/src/commands/mcp-install.ts +211 -0
- package/src/commands/mcp.ts +10 -14
- package/src/commands/render.ts +198 -195
- package/src/commands/serve.ts +102 -74
- package/src/diagnostics/adapt.ts +62 -0
- package/src/diagnostics/model.ts +1 -1
- package/src/generated/version.ts +2 -2
- package/src/i18n/locale.ts +2 -6
- package/src/index.ts +3 -0
- package/src/io/config.ts +0 -1
- package/src/io/log.ts +33 -0
- package/src/mcp-install/clients.ts +443 -0
- package/src/mcp-install/host.ts +106 -0
- package/src/mcp-install/json-config.ts +179 -0
- package/src/mcp-install/server-command.ts +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowline/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Command-line tool to check, convert, and create Nowline roadmaps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -32,33 +32,33 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/code-frame": "^8.0.6",
|
|
34
34
|
"@clack/prompts": "^1.8.1",
|
|
35
|
-
"@modelcontextprotocol/
|
|
36
|
-
"@
|
|
37
|
-
"@nowline/
|
|
38
|
-
"@nowline/
|
|
39
|
-
"@nowline/export": "0.
|
|
40
|
-
"@nowline/export-
|
|
41
|
-
"@nowline/export-
|
|
42
|
-
"@nowline/export-
|
|
43
|
-
"@nowline/export-
|
|
44
|
-
"@nowline/export-
|
|
45
|
-
"@nowline/export-
|
|
46
|
-
"@nowline/
|
|
47
|
-
"@nowline/
|
|
48
|
-
"@nowline/
|
|
49
|
-
"@nowline/renderer": "0.8.6",
|
|
35
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
36
|
+
"@nowline/config": "0.10.0",
|
|
37
|
+
"@nowline/core": "0.10.0",
|
|
38
|
+
"@nowline/export": "0.10.0",
|
|
39
|
+
"@nowline/export-core": "0.10.0",
|
|
40
|
+
"@nowline/export-html": "0.10.0",
|
|
41
|
+
"@nowline/export-mermaid": "0.10.0",
|
|
42
|
+
"@nowline/export-msproj": "0.10.0",
|
|
43
|
+
"@nowline/export-pdf": "0.10.0",
|
|
44
|
+
"@nowline/export-png": "0.10.0",
|
|
45
|
+
"@nowline/export-xlsx": "0.10.0",
|
|
46
|
+
"@nowline/layout": "0.10.0",
|
|
47
|
+
"@nowline/mcp": "0.10.0",
|
|
48
|
+
"@nowline/renderer": "0.10.0",
|
|
50
49
|
"chalk": "^6.0.1",
|
|
51
50
|
"citty": "^0.2.2",
|
|
52
51
|
"consola": "^3.4.2",
|
|
53
|
-
"js-yaml": "^5.
|
|
52
|
+
"js-yaml": "^5.5.0",
|
|
53
|
+
"jsonc-parser": "^3.3.1",
|
|
54
54
|
"langium": "~4.4.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
58
58
|
"@types/babel__code-frame": "^7.27.0",
|
|
59
|
-
"@types/node": "^26.6.
|
|
59
|
+
"@types/node": "^26.6.5",
|
|
60
60
|
"typescript": "^7.0.2",
|
|
61
|
-
"vitest": "^5.0.
|
|
61
|
+
"vitest": "^5.0.5"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"bundle-templates": "node scripts/bundle-templates.mjs",
|
package/src/cli/args.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { type ParseArgsConfig, parseArgs } from 'node:util';
|
|
2
2
|
import { isDiagnosticFormat } from '../diagnostics/format.js';
|
|
3
3
|
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
4
|
+
import type { LogLevel } from '../io/log.js';
|
|
4
5
|
|
|
5
|
-
export type ModeKind =
|
|
6
|
+
export type ModeKind =
|
|
7
|
+
| 'render'
|
|
8
|
+
| 'serve'
|
|
9
|
+
| 'init'
|
|
10
|
+
| 'mcp'
|
|
11
|
+
| 'mcp-install'
|
|
12
|
+
| 'mcp-uninstall'
|
|
13
|
+
| 'help'
|
|
14
|
+
| 'version';
|
|
15
|
+
|
|
16
|
+
export type McpScope = 'user' | 'project';
|
|
17
|
+
export type McpServerKind = 'self' | 'npx';
|
|
6
18
|
|
|
7
19
|
export interface ParsedArgs {
|
|
8
20
|
/** Resolved mode after dispatch (mutual-exclusivity already checked). */
|
|
9
21
|
mode: ModeKind;
|
|
10
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Positional argument, or undefined. Render = input path. Init = project name. Serve = input path.
|
|
24
|
+
* MCP install / uninstall = client id.
|
|
25
|
+
*/
|
|
11
26
|
positional?: string;
|
|
12
|
-
/** True if `--dry-run` / `-n` was passed (
|
|
27
|
+
/** True if `--dry-run` / `-n` was passed (render and MCP install / uninstall). */
|
|
13
28
|
dryRun: boolean;
|
|
14
29
|
/** Logging level. Verbose and quiet are mutually exclusive. */
|
|
15
|
-
logLevel:
|
|
30
|
+
logLevel: LogLevel;
|
|
16
31
|
|
|
17
32
|
// I/O
|
|
18
33
|
output?: string;
|
|
@@ -38,6 +53,12 @@ export interface ParsedArgs {
|
|
|
38
53
|
scale?: string;
|
|
39
54
|
strict: boolean;
|
|
40
55
|
width?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Raw `--non-working` value (`hide` or `show`). Undefined means unset, so
|
|
58
|
+
* the file's `default roadmap non-working:` key still applies. Validated
|
|
59
|
+
* by the render and serve commands, like `--width`.
|
|
60
|
+
*/
|
|
61
|
+
nonWorking?: string;
|
|
41
62
|
assetRoot?: string;
|
|
42
63
|
|
|
43
64
|
// Format-specific options (m2c)
|
|
@@ -78,6 +99,10 @@ export interface ParsedArgs {
|
|
|
78
99
|
|
|
79
100
|
// MCP
|
|
80
101
|
root?: string;
|
|
102
|
+
/** `--scope` for --mcp-install / --mcp-uninstall; validated in parseArgv. */
|
|
103
|
+
scope?: McpScope;
|
|
104
|
+
/** `--server` for --mcp-install; validated in parseArgv. */
|
|
105
|
+
server?: McpServerKind;
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
/**
|
|
@@ -118,6 +143,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
118
143
|
serve: { type: 'boolean' },
|
|
119
144
|
init: { type: 'boolean' },
|
|
120
145
|
mcp: { type: 'boolean' },
|
|
146
|
+
'mcp-install': { type: 'boolean' },
|
|
147
|
+
'mcp-uninstall': { type: 'boolean' },
|
|
121
148
|
'dry-run': { type: 'boolean', short: 'n' },
|
|
122
149
|
|
|
123
150
|
theme: { type: 'string', short: 't' },
|
|
@@ -133,6 +160,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
133
160
|
scale: { type: 'string', short: 's' },
|
|
134
161
|
strict: { type: 'boolean' },
|
|
135
162
|
width: { type: 'string', short: 'w' },
|
|
163
|
+
'non-working': { type: 'string' },
|
|
136
164
|
'asset-root': { type: 'string' },
|
|
137
165
|
|
|
138
166
|
port: { type: 'string', short: 'p' },
|
|
@@ -145,6 +173,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
145
173
|
|
|
146
174
|
// MCP
|
|
147
175
|
root: { type: 'string' },
|
|
176
|
+
scope: { type: 'string' },
|
|
177
|
+
server: { type: 'string' },
|
|
148
178
|
|
|
149
179
|
// Format-specific (m2c)
|
|
150
180
|
'page-size': { type: 'string' },
|
|
@@ -208,6 +238,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
208
238
|
if (values.serve === true) modes.push('serve');
|
|
209
239
|
if (values.init === true) modes.push('init');
|
|
210
240
|
if (values.mcp === true) modes.push('mcp');
|
|
241
|
+
if (values['mcp-install'] === true) modes.push('mcp-install');
|
|
242
|
+
if (values['mcp-uninstall'] === true) modes.push('mcp-uninstall');
|
|
211
243
|
if (modes.length > 1) {
|
|
212
244
|
throw new CliError(
|
|
213
245
|
ExitCode.InputError,
|
|
@@ -233,6 +265,21 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
233
265
|
throw new CliError(ExitCode.InputError, `nowline: unexpected extra arguments: ${extras}.`);
|
|
234
266
|
}
|
|
235
267
|
|
|
268
|
+
const scope = stringOrUndefined(values.scope);
|
|
269
|
+
if (scope !== undefined && scope !== 'user' && scope !== 'project') {
|
|
270
|
+
throw new CliError(
|
|
271
|
+
ExitCode.InputError,
|
|
272
|
+
`nowline: invalid --scope "${scope}". Expected user or project.`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const server = stringOrUndefined(values.server);
|
|
276
|
+
if (server !== undefined && server !== 'self' && server !== 'npx') {
|
|
277
|
+
throw new CliError(
|
|
278
|
+
ExitCode.InputError,
|
|
279
|
+
`nowline: invalid --server "${server}". Expected self or npx.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
236
283
|
const rawDiagnosticFormat = stringOrUndefined(values['diagnostic-format']);
|
|
237
284
|
if (rawDiagnosticFormat !== undefined && !isDiagnosticFormat(rawDiagnosticFormat)) {
|
|
238
285
|
throw new CliError(
|
|
@@ -241,7 +288,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
241
288
|
);
|
|
242
289
|
}
|
|
243
290
|
|
|
244
|
-
const logLevel:
|
|
291
|
+
const logLevel: LogLevel =
|
|
245
292
|
values.verbose === true ? 'verbose' : values.quiet === true ? 'quiet' : 'normal';
|
|
246
293
|
|
|
247
294
|
return {
|
|
@@ -259,6 +306,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
259
306
|
scale: stringOrUndefined(values.scale),
|
|
260
307
|
strict: values.strict === true,
|
|
261
308
|
width: stringOrUndefined(values.width),
|
|
309
|
+
nonWorking: stringOrUndefined(values['non-working']),
|
|
262
310
|
assetRoot: stringOrUndefined(values['asset-root']),
|
|
263
311
|
port: stringOrUndefined(values.port),
|
|
264
312
|
host: stringOrUndefined(values.host),
|
|
@@ -275,6 +323,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgs {
|
|
|
275
323
|
start: stringOrUndefined(values.start),
|
|
276
324
|
locale: stringOrUndefined(values.locale),
|
|
277
325
|
root: stringOrUndefined(values.root),
|
|
326
|
+
scope,
|
|
327
|
+
server,
|
|
278
328
|
};
|
|
279
329
|
}
|
|
280
330
|
|
package/src/cli/help.ts
CHANGED
|
@@ -9,6 +9,7 @@ USAGE
|
|
|
9
9
|
nowline <input> [options]
|
|
10
10
|
nowline --serve <input> [options]
|
|
11
11
|
nowline --init [<name>]
|
|
12
|
+
nowline --mcp-install [<client>] [options]
|
|
12
13
|
nowline --version
|
|
13
14
|
nowline --help
|
|
14
15
|
|
|
@@ -35,6 +36,14 @@ MODE FLAGS (mutually exclusive)
|
|
|
35
36
|
exposing validate, render, export, and file tools.
|
|
36
37
|
Shares the same @nowline/mcp server code as
|
|
37
38
|
\`npx @nowline/mcp\`.
|
|
39
|
+
--mcp-install [<client>]
|
|
40
|
+
Register the MCP server with an agent: claude-code,
|
|
41
|
+
claude-desktop, cursor, vscode, codex, gemini,
|
|
42
|
+
antigravity, muse, opencode, copilot, windsurf, zed,
|
|
43
|
+
amp, goose, or all. Omitted: every agent found on this machine.
|
|
44
|
+
Re-running is safe; -n previews the change.
|
|
45
|
+
--mcp-uninstall [<client>]
|
|
46
|
+
Remove the server from the same configs.
|
|
38
47
|
-n, --dry-run Run the full pipeline (parse + validate + layout +
|
|
39
48
|
format) but skip the write step. Subsumes the old
|
|
40
49
|
'validate' verb. Exit 0 on success, 1 on errors.
|
|
@@ -66,8 +75,18 @@ RENDER OPTIONS
|
|
|
66
75
|
--no-links Omit link icons from rendered items.
|
|
67
76
|
-s, --scale <n> Raster scale factor (PNG only; default 1).
|
|
68
77
|
--strict Promote asset / sanitizer warnings to errors.
|
|
69
|
-
-w, --width <px>
|
|
70
|
-
|
|
78
|
+
-w, --width <px> Maximum canvas width in pixels. Default: fit the
|
|
79
|
+
content. A wider roadmap shrinks its timeline to
|
|
80
|
+
fit; long captions can still overflow it.
|
|
81
|
+
--non-working <hide|show>
|
|
82
|
+
How a business calendar's non-working days appear:
|
|
83
|
+
hide collapses them (the default), show draws them
|
|
84
|
+
as shaded bands. Overrides the file's
|
|
85
|
+
'default roadmap non-working:' key; omit it to let
|
|
86
|
+
the key apply. Mermaid, MS Project, and XLSX output
|
|
87
|
+
is the same in both views.
|
|
88
|
+
--asset-root <dir> Root for logo / image assets (default: the
|
|
89
|
+
.nowlinerc assetRoot key, then the input dir).
|
|
71
90
|
--locale <bcp47> BCP-47 locale (e.g. fr, fr-CA) for CLI message
|
|
72
91
|
output (validator diagnostics, --help, errors).
|
|
73
92
|
Used as a fallback for the rendered drawing only
|
|
@@ -99,6 +118,13 @@ SERVE OPTIONS
|
|
|
99
118
|
MCP OPTIONS
|
|
100
119
|
--root <dir> Allowed-root directory for file tools (default: cwd).
|
|
101
120
|
File paths are resolved and restricted to this root.
|
|
121
|
+
With --mcp-install, written into the registered
|
|
122
|
+
command.
|
|
123
|
+
--scope <s> --mcp-install target: user (default, every
|
|
124
|
+
project) or project (config files in cwd).
|
|
125
|
+
--server <s> What --mcp-install registers: self (default, this
|
|
126
|
+
nowline binary; needs no Node) or npx
|
|
127
|
+
(npx -y @nowline/mcp, tracks npm).
|
|
102
128
|
|
|
103
129
|
LOGGING (mutually exclusive)
|
|
104
130
|
-v, --verbose Print extra diagnostics to stderr.
|
|
@@ -118,8 +144,10 @@ EXAMPLES
|
|
|
118
144
|
nowline roadmap.nowline --dry-run # validate-only
|
|
119
145
|
nowline roadmap.nowline --serve -p 8080 # live preview
|
|
120
146
|
nowline --init my-project # scaffold ./my-project.nowline
|
|
121
|
-
nowline --mcp # start MCP stdio server
|
|
147
|
+
nowline --mcp # start MCP stdio server
|
|
122
148
|
nowline --mcp --root ./roadmaps # MCP server with custom root
|
|
149
|
+
nowline --mcp-install # register with every agent found
|
|
150
|
+
nowline --mcp-install cursor --scope project -n # preview .cursor/mcp.json
|
|
123
151
|
|
|
124
152
|
EXIT CODES
|
|
125
153
|
0 Success
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// How the CLI turns flags and `.nowlinerc` keys into render options. The five
|
|
2
|
+
// shared options (theme, width, now, page size, non-working display) come from
|
|
3
|
+
// `@nowline/config`'s schema; the CLI-only keys are registered here as option
|
|
4
|
+
// specs over the same two layers, so adding an rc key is one entry in
|
|
5
|
+
// `CLI_OPTION_SPECS` rather than a hand-written merge in each command
|
|
6
|
+
// (specs/surface-options.md §3.1).
|
|
7
|
+
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
ASSET_ROOT_SPEC,
|
|
11
|
+
type NowlineRc,
|
|
12
|
+
type OptionError,
|
|
13
|
+
type OptionLayer,
|
|
14
|
+
type OptionParse,
|
|
15
|
+
type OptionSpec,
|
|
16
|
+
rcLayer,
|
|
17
|
+
resolveOptions,
|
|
18
|
+
resolveSurfaceOptions,
|
|
19
|
+
SURFACE_OPTION_SPECS,
|
|
20
|
+
type SurfaceOptions,
|
|
21
|
+
} from '@nowline/config';
|
|
22
|
+
import { lengthToPoints, parseLength } from '@nowline/export-core';
|
|
23
|
+
import { type NormalizedZone, normalizeZone, TimezoneError } from '@nowline/layout';
|
|
24
|
+
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
25
|
+
import type { ParsedArgs } from './args.js';
|
|
26
|
+
|
|
27
|
+
// ---- CLI-only option specs --------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export type PdfOrientation = 'portrait' | 'landscape' | 'auto';
|
|
30
|
+
|
|
31
|
+
const ORIENTATIONS: readonly PdfOrientation[] = ['portrait', 'landscape', 'auto'];
|
|
32
|
+
|
|
33
|
+
/** An rc key the CLI reads as a string; any other type is ignored, as it always was. */
|
|
34
|
+
function lenientString(raw: unknown): OptionParse<string> {
|
|
35
|
+
return { ok: true, value: typeof raw === 'string' ? raw : undefined };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An rc or flag switch: only a literal `true` turns it on. */
|
|
39
|
+
function lenientSwitch(raw: unknown): OptionParse<boolean> {
|
|
40
|
+
return { ok: true, value: raw === true ? true : undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseOrientation(raw: unknown): OptionParse<PdfOrientation> {
|
|
44
|
+
if (raw === '') return { ok: true, value: undefined };
|
|
45
|
+
const lower = typeof raw === 'string' ? raw.toLowerCase() : undefined;
|
|
46
|
+
const found = ORIENTATIONS.find((orientation) => orientation === lower);
|
|
47
|
+
if (found) return { ok: true, value: found };
|
|
48
|
+
return { ok: false, detail: 'must be an orientation', expected: ORIENTATIONS };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseMargin(raw: unknown): OptionParse<number> {
|
|
52
|
+
if (raw === '') return { ok: true, value: undefined };
|
|
53
|
+
// A bare number is points; YAML and JSON hand the rc one over as a number.
|
|
54
|
+
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) {
|
|
55
|
+
return { ok: true, value: raw };
|
|
56
|
+
}
|
|
57
|
+
const text = String(raw);
|
|
58
|
+
if (/^\d+(?:\.\d+)?$/.test(text)) return { ok: true, value: Number(text) };
|
|
59
|
+
try {
|
|
60
|
+
return { ok: true, value: lengthToPoints(parseLength(text)) };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Every option the CLI resolves over its flag and rc layers: the shared five, then its own. */
|
|
67
|
+
export const CLI_OPTION_SPECS = Object.freeze({
|
|
68
|
+
...SURFACE_OPTION_SPECS,
|
|
69
|
+
orientation: { rcKey: 'pdfOrientation', parse: parseOrientation } as OptionSpec<PdfOrientation>,
|
|
70
|
+
margin: { rcKey: 'pdfMargin', parse: parseMargin } as OptionSpec<number>,
|
|
71
|
+
defaultFormat: { rcKey: 'defaultFormat', parse: lenientString } as OptionSpec<string>,
|
|
72
|
+
fontSans: { rcKey: 'fontSans', parse: lenientString } as OptionSpec<string>,
|
|
73
|
+
fontMono: { rcKey: 'fontMono', parse: lenientString } as OptionSpec<string>,
|
|
74
|
+
headless: { rcKey: 'headlessFonts', parse: lenientSwitch } as OptionSpec<boolean>,
|
|
75
|
+
useSystemFonts: { rcKey: 'useSystemFonts', parse: lenientSwitch } as OptionSpec<boolean>,
|
|
76
|
+
locale: { rcKey: 'locale', parse: lenientString } as OptionSpec<string>,
|
|
77
|
+
assetRoot: ASSET_ROOT_SPEC as OptionSpec<string>,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
type CliSpecs = typeof CLI_OPTION_SPECS;
|
|
81
|
+
export type CliOptionKey = Extract<keyof CliSpecs, string>;
|
|
82
|
+
|
|
83
|
+
/** The flag each option is passed as, for an error that names its source. */
|
|
84
|
+
const FLAG_NAMES: Readonly<Partial<Record<CliOptionKey, string>>> = {
|
|
85
|
+
theme: '--theme',
|
|
86
|
+
width: '--width',
|
|
87
|
+
now: '--now',
|
|
88
|
+
pageSize: '--page-size',
|
|
89
|
+
nonWorking: '--non-working',
|
|
90
|
+
orientation: '--orientation',
|
|
91
|
+
margin: '--margin',
|
|
92
|
+
fontSans: '--font-sans',
|
|
93
|
+
fontMono: '--font-mono',
|
|
94
|
+
headless: '--headless',
|
|
95
|
+
useSystemFonts: '--use-system-fonts',
|
|
96
|
+
assetRoot: '--asset-root',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---- Resolution -------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export interface CliOptions {
|
|
102
|
+
/** Theme, width, now-line date and non-working display; page size when set. */
|
|
103
|
+
surface: SurfaceOptions;
|
|
104
|
+
orientation?: PdfOrientation;
|
|
105
|
+
/** Page margin in points. */
|
|
106
|
+
margin?: number;
|
|
107
|
+
/** The rc `defaultFormat`; the `--format` flag is resolved by `resolveFormat`. */
|
|
108
|
+
defaultFormat?: string;
|
|
109
|
+
fontSans?: string;
|
|
110
|
+
fontMono?: string;
|
|
111
|
+
headless: boolean;
|
|
112
|
+
useSystemFonts: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Where `logo:` assets are read from, as an absolute path: `--asset-root`,
|
|
115
|
+
* else the rc `assetRoot`. Unset means the input file's directory.
|
|
116
|
+
*/
|
|
117
|
+
assetRoot?: string;
|
|
118
|
+
/** The rc `locale`; the flag and `NOWLINE_LOCALE` rank above it (`resolveLocaleOverride`). */
|
|
119
|
+
rcLocale?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Throw the first invalid option as a usage error, in the order the CLI
|
|
122
|
+
* has always reported them: theme, timezone, now, width, non-working.
|
|
123
|
+
*/
|
|
124
|
+
assertValid(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Throw the first invalid PDF setting (page size, orientation, margin).
|
|
127
|
+
* Run after validation, so these errors follow any diagnostics.
|
|
128
|
+
*/
|
|
129
|
+
assertPdfSettingsValid(): void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the CLI's options over the flag layer, then the `.nowlinerc` layer.
|
|
134
|
+
* `render` and `--serve` both call this, so they read the same keys the same
|
|
135
|
+
* way. `cwd` is what a relative `--asset-root` resolves against (default: the
|
|
136
|
+
* process's). Nothing throws here: a caller reads `defaultFormat` early and reports bad
|
|
137
|
+
* values at the point it always did, through `assertValid`.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveCliOptions(
|
|
140
|
+
args: ParsedArgs,
|
|
141
|
+
config: NowlineRc | null,
|
|
142
|
+
context: { rcPath?: string; cwd?: string; clock?: () => Date } = {},
|
|
143
|
+
): CliOptions {
|
|
144
|
+
const { rcPath, cwd, clock } = context;
|
|
145
|
+
const layers = [flagLayer(args), rcLayer(config ?? {}, CLI_OPTION_SPECS)];
|
|
146
|
+
|
|
147
|
+
// `--timezone` is validated even when `--now` is explicit, as it always was.
|
|
148
|
+
const zone = readZone(args.timezone);
|
|
149
|
+
const { options, errors: surfaceErrors } = resolveSurfaceOptions(
|
|
150
|
+
layers,
|
|
151
|
+
{ theme: 'light' },
|
|
152
|
+
{ zone: zone.zone, clock },
|
|
153
|
+
);
|
|
154
|
+
const { values, errors: cliErrors } = resolveOptions(CLI_OPTION_SPECS, layers);
|
|
155
|
+
const failures = new Map<string, OptionError<CliOptionKey>>(
|
|
156
|
+
[...surfaceErrors, ...cliErrors].map((error) => [error.key, error]),
|
|
157
|
+
);
|
|
158
|
+
const failure = (key: CliOptionKey): CliError | undefined => {
|
|
159
|
+
const error = failures.get(key);
|
|
160
|
+
return error && new CliError(ExitCode.InputError, formatOptionError(error));
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
surface: options,
|
|
165
|
+
orientation: values.orientation,
|
|
166
|
+
margin: values.margin,
|
|
167
|
+
defaultFormat: values.defaultFormat,
|
|
168
|
+
fontSans: values.fontSans,
|
|
169
|
+
fontMono: values.fontMono,
|
|
170
|
+
headless: values.headless ?? false,
|
|
171
|
+
useSystemFonts: values.useSystemFonts ?? false,
|
|
172
|
+
assetRoot: absoluteAssetRoot(values.assetRoot, args.assetRoot !== undefined, rcPath, cwd),
|
|
173
|
+
rcLocale: values.locale,
|
|
174
|
+
assertValid: () =>
|
|
175
|
+
throwFirst([
|
|
176
|
+
failure('theme'),
|
|
177
|
+
zone.error,
|
|
178
|
+
failure('now'),
|
|
179
|
+
failure('width'),
|
|
180
|
+
failure('nonWorking'),
|
|
181
|
+
failure('assetRoot'),
|
|
182
|
+
]),
|
|
183
|
+
assertPdfSettingsValid: () =>
|
|
184
|
+
throwFirst([failure('pageSize'), failure('orientation'), failure('margin')]),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function throwFirst(candidates: readonly (CliError | undefined)[]): void {
|
|
189
|
+
for (const candidate of candidates) {
|
|
190
|
+
if (candidate) throw candidate;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function flagLayer(args: ParsedArgs): OptionLayer<CliOptionKey> {
|
|
195
|
+
return {
|
|
196
|
+
source: (key) => ({ kind: 'flag', name: FLAG_NAMES[key] ?? `--${key}` }),
|
|
197
|
+
values: {
|
|
198
|
+
// An empty `--theme ""` has always meant "not passed", so the rc theme applies.
|
|
199
|
+
theme: args.theme || undefined,
|
|
200
|
+
width: args.width,
|
|
201
|
+
now: args.now,
|
|
202
|
+
pageSize: args.pageSize,
|
|
203
|
+
nonWorking: args.nonWorking,
|
|
204
|
+
orientation: args.orientation,
|
|
205
|
+
margin: args.margin,
|
|
206
|
+
fontSans: args.fontSans,
|
|
207
|
+
fontMono: args.fontMono,
|
|
208
|
+
assetRoot: args.assetRoot,
|
|
209
|
+
// A switch flag is absent or on; absent leaves the rc to decide.
|
|
210
|
+
headless: args.headless || undefined,
|
|
211
|
+
useSystemFonts: args.useSystemFonts || undefined,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readZone(raw: string | undefined): { zone?: NormalizedZone; error?: CliError } {
|
|
217
|
+
if (!raw) return {};
|
|
218
|
+
try {
|
|
219
|
+
return { zone: normalizeZone(raw) };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof TimezoneError) {
|
|
222
|
+
return { error: new CliError(ExitCode.InputError, err.message) };
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- Asset root -------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The asset root as an absolute path. A flag resolves against `cwd` (the
|
|
232
|
+
* process's, unless a caller passes its own); an rc value against the
|
|
233
|
+
* directory of the `.nowlinerc` it came from, so a checked-in `assetRoot:
|
|
234
|
+
* assets` means the same directory wherever `nowline` is run from.
|
|
235
|
+
*/
|
|
236
|
+
function absoluteAssetRoot(
|
|
237
|
+
value: string | undefined,
|
|
238
|
+
fromFlag: boolean,
|
|
239
|
+
rcPath: string | undefined,
|
|
240
|
+
cwd?: string,
|
|
241
|
+
): string | undefined {
|
|
242
|
+
if (value === undefined) return undefined;
|
|
243
|
+
if (fromFlag || rcPath === undefined) return path.resolve(cwd ?? '.', value);
|
|
244
|
+
return path.resolve(path.dirname(rcPath), value);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Error text -------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* An {@link OptionError} in the CLI's wording, naming the flag or `.nowlinerc`
|
|
251
|
+
* key the value came from: `nowline: invalid --width "abc". Must be an integer
|
|
252
|
+
* ≥ 320.` The templates are the ones the CLI used before the schema, so
|
|
253
|
+
* scripts that match them keep working.
|
|
254
|
+
*/
|
|
255
|
+
export function formatOptionError(error: OptionError<string>): string {
|
|
256
|
+
const lead = `nowline: invalid ${describeSource(error.source)} "${String(error.raw)}"`;
|
|
257
|
+
if (error.expected) return `${lead}. Expected ${formatChoices(error.expected)}.`;
|
|
258
|
+
switch (error.key) {
|
|
259
|
+
// Detail reads "must be YYYY-MM-DD, ...": the CLI says "Expected".
|
|
260
|
+
case 'now':
|
|
261
|
+
return `${lead}. Expected ${error.detail.replace(/^must be /, '')}.`;
|
|
262
|
+
// These two carry the parser's own message after a colon.
|
|
263
|
+
case 'pageSize':
|
|
264
|
+
case 'margin':
|
|
265
|
+
return `${lead}: ${error.detail}`;
|
|
266
|
+
default:
|
|
267
|
+
return `${lead}. ${error.detail.charAt(0).toUpperCase()}${error.detail.slice(1)}.`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function describeSource(source: OptionError['source']): string {
|
|
272
|
+
switch (source.kind) {
|
|
273
|
+
case 'flag':
|
|
274
|
+
case 'argument':
|
|
275
|
+
case 'setting':
|
|
276
|
+
return source.name;
|
|
277
|
+
case 'rc':
|
|
278
|
+
return `.nowlinerc ${source.key}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// `a`, `a or b`, `a, b, or c`.
|
|
283
|
+
export function formatChoices(names: readonly string[]): string {
|
|
284
|
+
if (names.length <= 2) return names.join(' or ');
|
|
285
|
+
return `${names.slice(0, -1).join(', ')}, or ${names[names.length - 1]}`;
|
|
286
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ParsedArgs } from '../cli/args.js';
|
|
|
2
2
|
import { defaultInitOutputPath, initNameHasIncompatibleExtension } from '../cli/output-path.js';
|
|
3
3
|
import { TEMPLATE_NAMES, TEMPLATES, type TemplateName } from '../generated/templates.js';
|
|
4
4
|
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
5
|
+
import { createLog } from '../io/log.js';
|
|
5
6
|
import { writeOutput } from '../io/write.js';
|
|
6
7
|
|
|
7
8
|
export interface InitHandlerOptions {
|
|
@@ -47,9 +48,7 @@ export async function initHandler(options: InitHandlerOptions): Promise<void> {
|
|
|
47
48
|
|
|
48
49
|
await writeOutput(outputPath, rendered, 'text', { cwd });
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
process.stderr.write(`nowline: wrote ${outputPath}\n`);
|
|
52
|
-
}
|
|
51
|
+
createLog(args.logLevel).verbose(`nowline: wrote ${outputPath}`);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
function parseTemplateName(raw: string | undefined): TemplateName {
|