@nowline/cli 0.8.5 → 0.9.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 +42 -12
- package/dist/cli/args.d.ts +21 -5
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +26 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/formats.d.ts.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +35 -2
- 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 +6 -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 +176 -152
- 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/core/parse.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/format.js.map +1 -1
- package/dist/diagnostics/model.d.ts +1 -1
- package/dist/diagnostics/text.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +2 -2
- package/dist/i18n/locale.d.ts +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/exit-codes.d.ts.map +1 -1
- package/dist/io/exit-codes.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/io/read.d.ts +3 -0
- package/dist/io/read.d.ts.map +1 -1
- package/dist/io/read.js +32 -5
- package/dist/io/read.js.map +1 -1
- package/dist/io/write.js.map +1 -1
- 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 +246 -19
- package/man/fr/nowline.5 +819 -45
- package/man/nowline.1 +191 -18
- package/man/nowline.5 +764 -36
- package/package.json +24 -24
- package/src/cli/args.ts +69 -8
- package/src/cli/help.ts +35 -2
- 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 +9 -14
- package/src/commands/render.ts +219 -186
- 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/io/read.ts +39 -5
- 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/src/commands/render.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import type { ResolveDiagnostic } from '@nowline/core';
|
|
3
4
|
import {
|
|
5
|
+
acceptStagedRoadmap,
|
|
4
6
|
type ExportFormat,
|
|
5
|
-
|
|
7
|
+
exportStaged,
|
|
6
8
|
type HostEnv,
|
|
7
9
|
type RenderInputs,
|
|
10
|
+
type StageResult,
|
|
11
|
+
stageForExport,
|
|
8
12
|
} from '@nowline/export';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
type NormalizedZone,
|
|
12
|
-
normalizeThemeName,
|
|
13
|
-
normalizeZone,
|
|
14
|
-
resolveToday,
|
|
15
|
-
type ThemeName,
|
|
16
|
-
TimezoneError,
|
|
17
|
-
} from '@nowline/layout';
|
|
13
|
+
import type { PdfPageSize } from '@nowline/export-core';
|
|
14
|
+
import type { NonWorkingDisplay, ThemeName } from '@nowline/layout';
|
|
18
15
|
import type { AssetResolver } from '@nowline/renderer';
|
|
19
16
|
import type { ParsedArgs } from '../cli/args.js';
|
|
20
17
|
import {
|
|
@@ -23,20 +20,28 @@ import {
|
|
|
23
20
|
type OutputFormat,
|
|
24
21
|
resolveFormat,
|
|
25
22
|
} from '../cli/formats.js';
|
|
23
|
+
import { type PdfOrientation, resolveCliOptions } from '../cli/options.js';
|
|
26
24
|
import { resolveRenderOutputPath } from '../cli/output-path.js';
|
|
27
25
|
import { parseNowlineJson } from '../convert/parse-json.js';
|
|
28
26
|
import { printNowlineFile } from '../convert/printer.js';
|
|
29
27
|
import { serializeToJson } from '../convert/schema.js';
|
|
30
|
-
import { parseSource } from '../core/parse.js';
|
|
31
|
-
import {
|
|
28
|
+
import { getServices, parseSource } from '../core/parse.js';
|
|
29
|
+
import {
|
|
30
|
+
adaptResolveDiagnostic,
|
|
31
|
+
type CliDiagnostic,
|
|
32
|
+
type DiagnosticFormat,
|
|
33
|
+
type DiagnosticSource,
|
|
34
|
+
formatDiagnostics,
|
|
35
|
+
} from '../diagnostics/index.js';
|
|
32
36
|
import {
|
|
33
37
|
describeContentLocaleSource,
|
|
34
38
|
operatorLocale,
|
|
35
39
|
readDirectiveLocale,
|
|
36
40
|
resolveLocaleOverride,
|
|
37
41
|
} from '../i18n/locale.js';
|
|
38
|
-
import { loadConfig } from '../io/config.js';
|
|
42
|
+
import { type LoadConfigResult, loadConfig } from '../io/config.js';
|
|
39
43
|
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
44
|
+
import { createLog, type Log } from '../io/log.js';
|
|
40
45
|
import { readInput } from '../io/read.js';
|
|
41
46
|
import { writeOutput } from '../io/write.js';
|
|
42
47
|
|
|
@@ -63,18 +68,20 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
const isStdoutOutput = args.output === '-';
|
|
66
|
-
const
|
|
71
|
+
const log = createLog(args.logLevel);
|
|
72
|
+
const { config, path: rcPath } = await loadConfigFor(args.positional, cwd);
|
|
73
|
+
// Every rc-backed setting resolves flag > .nowlinerc > default; a bad
|
|
74
|
+
// value is reported by `assertValid` below, where it always has been.
|
|
75
|
+
const cliOptions = resolveCliOptions(args, config, { rcPath });
|
|
67
76
|
|
|
68
77
|
const format = resolveFormatOrThrow({
|
|
69
78
|
flag: args.format,
|
|
70
79
|
outputPath: args.output,
|
|
71
|
-
configFormat:
|
|
80
|
+
configFormat: cliOptions.defaultFormat,
|
|
72
81
|
isStdout: isStdoutOutput,
|
|
73
82
|
});
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
process.stderr.write(`nowline: format=${format} (resolved)\n`);
|
|
77
|
-
}
|
|
84
|
+
log.verbose(`nowline: format=${format} (resolved)`);
|
|
78
85
|
|
|
79
86
|
const resolvedOutput = resolveRenderOutputPath({
|
|
80
87
|
outputArg: args.output,
|
|
@@ -92,10 +99,11 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
|
|
|
92
99
|
const resolved = resolveLocaleOverride({
|
|
93
100
|
flag: args.locale,
|
|
94
101
|
env: process.env,
|
|
95
|
-
rc:
|
|
102
|
+
rc: cliOptions.rcLocale,
|
|
96
103
|
});
|
|
97
104
|
const locale = resolved.tag;
|
|
98
105
|
const opLocale = operatorLocale(resolved);
|
|
106
|
+
cliOptions.assertValid();
|
|
99
107
|
|
|
100
108
|
const { rendered, isBinary } = await produce({
|
|
101
109
|
format,
|
|
@@ -104,33 +112,34 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
|
|
|
104
112
|
displayPath: input.displayPath,
|
|
105
113
|
absInputPath: input.isStdin ? path.resolve(cwd, 'stdin.nowline') : input.path,
|
|
106
114
|
isStdin: input.isStdin,
|
|
107
|
-
theme:
|
|
108
|
-
today:
|
|
109
|
-
width:
|
|
115
|
+
theme: cliOptions.surface.theme,
|
|
116
|
+
today: cliOptions.surface.today,
|
|
117
|
+
width: cliOptions.surface.width,
|
|
118
|
+
nonWorking: cliOptions.surface.nonWorking,
|
|
110
119
|
noLinks: args.noLinks,
|
|
111
120
|
strict: args.strict,
|
|
112
|
-
assetRoot:
|
|
113
|
-
// m2c format-specific options.
|
|
114
|
-
//
|
|
115
|
-
pageSize:
|
|
116
|
-
orientation:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
assetRoot: cliOptions.assetRoot,
|
|
122
|
+
// m2c format-specific options. The PDF settings parse in `produce`,
|
|
123
|
+
// after validation, for every format.
|
|
124
|
+
pageSize: cliOptions.surface.pageSize,
|
|
125
|
+
orientation: cliOptions.orientation,
|
|
126
|
+
marginPt: cliOptions.margin,
|
|
127
|
+
assertPdfSettingsValid: () => cliOptions.assertPdfSettingsValid(),
|
|
128
|
+
fontSans: cliOptions.fontSans,
|
|
129
|
+
fontMono: cliOptions.fontMono,
|
|
130
|
+
headless: cliOptions.headless,
|
|
131
|
+
useSystemFonts: cliOptions.useSystemFonts,
|
|
122
132
|
scale: args.scale,
|
|
123
133
|
start: args.start,
|
|
124
134
|
locale,
|
|
125
135
|
operatorLocale: opLocale,
|
|
126
136
|
resolvedLocale: resolved,
|
|
127
|
-
|
|
137
|
+
log,
|
|
138
|
+
diagnosticFormat: resolveDiagnosticFormat(args),
|
|
128
139
|
});
|
|
129
140
|
|
|
130
141
|
if (args.dryRun) {
|
|
131
|
-
|
|
132
|
-
process.stderr.write('nowline: --dry-run; skipping write\n');
|
|
133
|
-
}
|
|
142
|
+
log.verbose('nowline: --dry-run; skipping write');
|
|
134
143
|
return;
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -141,8 +150,8 @@ export async function renderHandler(options: RenderHandlerOptions): Promise<void
|
|
|
141
150
|
{ cwd },
|
|
142
151
|
);
|
|
143
152
|
|
|
144
|
-
if (
|
|
145
|
-
|
|
153
|
+
if (!resolvedOutput.isStdout) {
|
|
154
|
+
log.verbose(`nowline: wrote ${resolvedOutput.path}`);
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -194,13 +203,17 @@ interface ProduceArgs {
|
|
|
194
203
|
theme: ThemeName;
|
|
195
204
|
today?: Date;
|
|
196
205
|
width?: number;
|
|
206
|
+
nonWorking?: NonWorkingDisplay;
|
|
197
207
|
noLinks: boolean;
|
|
198
208
|
strict: boolean;
|
|
199
209
|
assetRoot?: string;
|
|
200
210
|
// m2c format-specific
|
|
201
|
-
pageSize?:
|
|
202
|
-
orientation?:
|
|
203
|
-
margin
|
|
211
|
+
pageSize?: PdfPageSize;
|
|
212
|
+
orientation?: PdfOrientation;
|
|
213
|
+
/** Page margin in points. */
|
|
214
|
+
marginPt?: number;
|
|
215
|
+
/** Reports a bad PDF setting; run after validation so its error follows any diagnostics. */
|
|
216
|
+
assertPdfSettingsValid: () => void;
|
|
204
217
|
fontSans?: string;
|
|
205
218
|
fontMono?: string;
|
|
206
219
|
headless: boolean;
|
|
@@ -214,8 +227,10 @@ interface ProduceArgs {
|
|
|
214
227
|
operatorLocale: string;
|
|
215
228
|
/** Resolved locale override metadata, used for the verbose-mode source line. */
|
|
216
229
|
resolvedLocale: import('../i18n/locale.js').ResolvedLocale;
|
|
217
|
-
/**
|
|
218
|
-
|
|
230
|
+
/** `-v` / `-q` gate: verbose lines, warnings, and text-mode warning diagnostics. */
|
|
231
|
+
log: Log;
|
|
232
|
+
/** How diagnostics are written to stderr. See `resolveDiagnosticFormat`. */
|
|
233
|
+
diagnosticFormat: DiagnosticFormat;
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
interface ProduceResult {
|
|
@@ -238,17 +253,38 @@ async function produce(args: ProduceArgs): Promise<ProduceResult> {
|
|
|
238
253
|
? jsonToNowlineText(args.contents, args.displayPath)
|
|
239
254
|
: args.contents;
|
|
240
255
|
|
|
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);
|
|
246
|
-
|
|
247
256
|
const assetRoot = args.assetRoot
|
|
248
257
|
? path.resolve(args.assetRoot)
|
|
249
258
|
: path.dirname(args.absInputPath);
|
|
250
259
|
|
|
251
|
-
|
|
260
|
+
// Included files' text, kept for the code frames of routed diagnostics.
|
|
261
|
+
const included = new Map<string, string>();
|
|
262
|
+
const nodeHost = createNodeHostEnv(assetRoot);
|
|
263
|
+
const host: HostEnv = {
|
|
264
|
+
...nodeHost,
|
|
265
|
+
readSource: async (absPath) => {
|
|
266
|
+
const text = await nodeHost.readSource(absPath);
|
|
267
|
+
included.set(absPath, text);
|
|
268
|
+
return text;
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// What the stage lays out with; the export below must ask for the same.
|
|
273
|
+
const layoutInputs: RenderInputs = {
|
|
274
|
+
sourcePath: args.absInputPath,
|
|
275
|
+
today: args.today,
|
|
276
|
+
locale: args.locale ?? 'und',
|
|
277
|
+
theme: args.theme,
|
|
278
|
+
width: args.width,
|
|
279
|
+
nonWorking: args.nonWorking,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Parse and stage once. Validation diagnostics come out in the operator's
|
|
283
|
+
// locale, and routed include-resolver diagnostics join the validator's
|
|
284
|
+
// here, so they are reported at the same point and in the same report.
|
|
285
|
+
const { stage, stageFailure } = await parseAndValidate(sourceText, args, {
|
|
286
|
+
stage: { inputs: layoutInputs, host, included },
|
|
287
|
+
});
|
|
252
288
|
|
|
253
289
|
// Resolve fonts lazily — only for formats that rasterize or embed them.
|
|
254
290
|
let fonts: RenderInputs['fonts'];
|
|
@@ -256,29 +292,28 @@ async function produce(args: ProduceArgs): Promise<ProduceResult> {
|
|
|
256
292
|
fonts = await resolveNodeFonts(args);
|
|
257
293
|
}
|
|
258
294
|
|
|
295
|
+
args.assertPdfSettingsValid();
|
|
296
|
+
|
|
259
297
|
const kernelInputs: RenderInputs = {
|
|
260
|
-
|
|
261
|
-
today: args.today,
|
|
262
|
-
locale: args.locale ?? 'und',
|
|
263
|
-
theme: args.theme,
|
|
298
|
+
...layoutInputs,
|
|
264
299
|
fonts,
|
|
265
|
-
width: args.width,
|
|
266
300
|
noLinks: args.noLinks,
|
|
267
301
|
strict: args.strict,
|
|
268
302
|
pageSize: args.pageSize,
|
|
269
|
-
orientation:
|
|
270
|
-
marginPt:
|
|
303
|
+
orientation: args.orientation,
|
|
304
|
+
marginPt: args.marginPt,
|
|
271
305
|
pngScale: parseScale(args.scale),
|
|
272
306
|
msprojStart: args.start,
|
|
307
|
+
onNotice: (message) => args.log.notice(message),
|
|
308
|
+
onWarning: (message) => args.log.notice(`warning: ${message}`),
|
|
273
309
|
};
|
|
274
310
|
|
|
275
311
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
312
|
+
// A stage failure or an uncoded include error was held back until
|
|
313
|
+
// here, so a bad font or PDF setting is still reported first.
|
|
314
|
+
if (!stage) throw stageFailure;
|
|
315
|
+
const staged = acceptStagedRoadmap(stage, kernelInputs);
|
|
316
|
+
const bytes = await exportStaged(staged, args.format as ExportFormat, kernelInputs, host);
|
|
282
317
|
const isBinary = BINARY_FORMATS.has(args.format);
|
|
283
318
|
const rendered: string | Uint8Array = isBinary
|
|
284
319
|
? bytes
|
|
@@ -294,6 +329,18 @@ async function produce(args: ProduceArgs): Promise<ProduceResult> {
|
|
|
294
329
|
}
|
|
295
330
|
}
|
|
296
331
|
|
|
332
|
+
/**
|
|
333
|
+
* How an included file is named in diagnostics: relative to the input's
|
|
334
|
+
* display path, so `examples/a.nowline` including `./teams/b.nowline` prints
|
|
335
|
+
* `examples/teams/b.nowline` (absolute when the input was given absolute).
|
|
336
|
+
*/
|
|
337
|
+
function includeDisplayPath(absPath: string, args: ProduceArgs): string {
|
|
338
|
+
const rel = path.relative(path.dirname(args.absInputPath), absPath);
|
|
339
|
+
if (path.isAbsolute(rel)) return rel;
|
|
340
|
+
const base = args.isStdin ? '' : path.dirname(args.displayPath);
|
|
341
|
+
return path.join(base, rel);
|
|
342
|
+
}
|
|
343
|
+
|
|
297
344
|
// ---- Node HostEnv -----------------------------------------------------------
|
|
298
345
|
|
|
299
346
|
function createNodeHostEnv(assetRoot: string): HostEnv {
|
|
@@ -364,20 +411,20 @@ async function resolveNodeFonts(
|
|
|
364
411
|
if (args.strict) {
|
|
365
412
|
throw new CliError(ExitCode.InputError, `nowline: ${message}`);
|
|
366
413
|
}
|
|
367
|
-
|
|
414
|
+
args.log.notice(`warning: ${message}`);
|
|
368
415
|
}
|
|
369
416
|
|
|
370
417
|
// Bundled fallback after an opted-in probe found no usable system font.
|
|
371
418
|
// Only meaningful when --use-system-fonts is set; warn under --strict.
|
|
372
419
|
if (args.strict) {
|
|
373
420
|
if (result.sansFellBackToBundled) {
|
|
374
|
-
|
|
375
|
-
'warning: sans font fell back to bundled DejaVu (no platform font found)
|
|
421
|
+
args.log.notice(
|
|
422
|
+
'warning: sans font fell back to bundled DejaVu (no platform font found)',
|
|
376
423
|
);
|
|
377
424
|
}
|
|
378
425
|
if (result.monoFellBackToBundled) {
|
|
379
|
-
|
|
380
|
-
'warning: mono font fell back to bundled DejaVu (no platform font found)
|
|
426
|
+
args.log.notice(
|
|
427
|
+
'warning: mono font fell back to bundled DejaVu (no platform font found)',
|
|
381
428
|
);
|
|
382
429
|
}
|
|
383
430
|
}
|
|
@@ -403,86 +450,99 @@ function jsonToNowlineText(contents: string, displayPath: string): string {
|
|
|
403
450
|
return printNowlineFile(ast);
|
|
404
451
|
}
|
|
405
452
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const directive = readDirectiveLocale(result.ast);
|
|
414
|
-
const { tag, source } = describeContentLocaleSource(directive, args.resolvedLocale);
|
|
415
|
-
process.stderr.write(`nowline: locale=${tag} (${source})\n`);
|
|
416
|
-
}
|
|
417
|
-
return result;
|
|
453
|
+
/** What `parseAndValidate` stages with, once the source validates. */
|
|
454
|
+
interface StageRequest {
|
|
455
|
+
/** The layout inputs the export that follows will ask for. */
|
|
456
|
+
inputs: RenderInputs;
|
|
457
|
+
host: HostEnv;
|
|
458
|
+
/** Collects the text of each file the stage reads, for code frames. */
|
|
459
|
+
included: Map<string, string>;
|
|
418
460
|
}
|
|
419
461
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Parse and validate; on errors, report them and throw. With `stage`, the
|
|
464
|
+
* validated document is also staged (include resolution, logo, layout) and a
|
|
465
|
+
* file that has includes reports the routed (wave-rule) diagnostics of that
|
|
466
|
+
* stage with the validator's, in the same report (specs/waves.md §6.1). The
|
|
467
|
+
* stage is returned for the export to reuse. Every other include error, and a
|
|
468
|
+
* stage that throws, is left for the caller to report where the kernel always
|
|
469
|
+
* did (exit 3).
|
|
470
|
+
*/
|
|
471
|
+
async function parseAndValidate(
|
|
472
|
+
contents: string,
|
|
473
|
+
args: ProduceArgs,
|
|
474
|
+
options: { stage?: StageRequest } = {},
|
|
475
|
+
) {
|
|
476
|
+
const result = await parseSource(contents, args.displayPath, { validate: true });
|
|
477
|
+
const diagnostics = [...result.diagnostics];
|
|
478
|
+
const sources = new Map<string, DiagnosticSource>([[args.displayPath, result.source]]);
|
|
479
|
+
let failed = result.hasErrors;
|
|
480
|
+
let stage: StageResult | undefined;
|
|
481
|
+
let stageFailure: unknown;
|
|
482
|
+
if (!failed && options.stage) {
|
|
483
|
+
const { inputs, host, included } = options.stage;
|
|
484
|
+
try {
|
|
485
|
+
stage = await stageForExport(contents, inputs, host, {
|
|
486
|
+
document: result.document,
|
|
487
|
+
services: getServices(),
|
|
488
|
+
});
|
|
489
|
+
} catch (err) {
|
|
490
|
+
stageFailure = err;
|
|
491
|
+
}
|
|
492
|
+
if (stage && result.ast.includes.length > 0) {
|
|
493
|
+
for (const d of routedDiagnostics(stage)) {
|
|
494
|
+
const isRoot = d.sourcePath === args.absInputPath;
|
|
495
|
+
const file = isRoot ? args.displayPath : includeDisplayPath(d.sourcePath, args);
|
|
496
|
+
const text = isRoot ? result.source.contents : included.get(d.sourcePath);
|
|
497
|
+
if (text !== undefined) sources.set(file, { file, contents: text });
|
|
498
|
+
diagnostics.push(adaptResolveDiagnostic(d, file, text));
|
|
499
|
+
if (d.severity === 'error') failed = true;
|
|
500
|
+
}
|
|
428
501
|
}
|
|
429
|
-
const abs = path.resolve(cwd, inputArg);
|
|
430
|
-
const dir = path.dirname(abs);
|
|
431
|
-
const { config } = await loadConfig(dir);
|
|
432
|
-
return config;
|
|
433
|
-
} catch {
|
|
434
|
-
return null;
|
|
435
502
|
}
|
|
503
|
+
// Text mode prints only when the run fails. JSON mode prints one document
|
|
504
|
+
// whenever any diagnostic exists, warnings included, so a warnings-only
|
|
505
|
+
// run still exits 0 with the document on stderr.
|
|
506
|
+
if (failed || (args.diagnosticFormat === 'json' && diagnostics.length > 0)) {
|
|
507
|
+
emitDiagnostics(diagnostics, sources, args.operatorLocale, args.diagnosticFormat, args.log);
|
|
508
|
+
}
|
|
509
|
+
if (failed) {
|
|
510
|
+
throw new CliError(ExitCode.ValidationError, '');
|
|
511
|
+
}
|
|
512
|
+
const directive = readDirectiveLocale(result.ast);
|
|
513
|
+
const { tag, source } = describeContentLocaleSource(directive, args.resolvedLocale);
|
|
514
|
+
args.log.verbose(`nowline: locale=${tag} (${source})`);
|
|
515
|
+
return { ...result, stage, stageFailure };
|
|
436
516
|
}
|
|
437
517
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
);
|
|
518
|
+
/**
|
|
519
|
+
* The routed (wave-rule) diagnostics of a stage's include resolution. Empty
|
|
520
|
+
* when the resolver reports any other include error: that one is more
|
|
521
|
+
* fundamental, and the export reports it as before (exit 3).
|
|
522
|
+
*/
|
|
523
|
+
function routedDiagnostics(stage: StageResult): ResolveDiagnostic[] {
|
|
524
|
+
if (stage.kind === 'staged' || (stage.kind === 'include-error' && !stage.legacy)) {
|
|
525
|
+
return stage.routed;
|
|
447
526
|
}
|
|
448
|
-
return
|
|
527
|
+
return [];
|
|
449
528
|
}
|
|
450
529
|
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
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 !== '-') {
|
|
530
|
+
// Walk up from the input (or `cwd` for stdin) to the nearest `.nowlinerc`.
|
|
531
|
+
// No rc file is not an error. One that exists but cannot be read or parsed is
|
|
532
|
+
// a usage error (specs/cli.md, `.nowlinerc`): silently ignoring it would
|
|
533
|
+
// render with settings the user believes they changed. The loader's reason
|
|
534
|
+
// already names the file, so it is passed through. `--serve` shares this.
|
|
535
|
+
export async function loadConfigFor(inputArg: string, cwd: string): Promise<LoadConfigResult> {
|
|
536
|
+
try {
|
|
537
|
+
if (inputArg === '-') return await loadConfig(cwd);
|
|
538
|
+
const abs = path.resolve(cwd, inputArg);
|
|
539
|
+
return await loadConfig(path.dirname(abs));
|
|
540
|
+
} catch (err) {
|
|
479
541
|
throw new CliError(
|
|
480
542
|
ExitCode.InputError,
|
|
481
|
-
`nowline: invalid
|
|
482
|
-
`Expected YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], or "-".`,
|
|
543
|
+
`nowline: invalid .nowlinerc: ${err instanceof Error ? err.message : String(err)}`,
|
|
483
544
|
);
|
|
484
545
|
}
|
|
485
|
-
return result;
|
|
486
546
|
}
|
|
487
547
|
|
|
488
548
|
function parseScale(raw: string | undefined): number | undefined {
|
|
@@ -497,68 +557,41 @@ function parseScale(raw: string | undefined): number | undefined {
|
|
|
497
557
|
return value;
|
|
498
558
|
}
|
|
499
559
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
function parseMargin(raw: string | undefined): number | undefined {
|
|
511
|
-
if (!raw) return undefined;
|
|
512
|
-
if (/^\d+(?:\.\d+)?$/.test(raw)) return Number(raw); // bare number → points
|
|
513
|
-
try {
|
|
514
|
-
return lengthToPoints(parseLength(raw));
|
|
515
|
-
} catch (err) {
|
|
516
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
517
|
-
throw new CliError(ExitCode.InputError, `nowline: invalid --margin "${raw}": ${message}`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function stringFromConfig(
|
|
522
|
-
config: { [key: string]: unknown } | null,
|
|
523
|
-
key: string,
|
|
524
|
-
): string | undefined {
|
|
525
|
-
if (!config) return undefined;
|
|
526
|
-
const value = config[key];
|
|
527
|
-
return typeof value === 'string' ? value : undefined;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function boolFromConfig(config: { [key: string]: unknown } | null, key: string): boolean {
|
|
531
|
-
if (!config) return false;
|
|
532
|
-
const value = config[key];
|
|
533
|
-
return value === true;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function parseWidthArg(raw: string | undefined): number | undefined {
|
|
537
|
-
if (!raw) return undefined;
|
|
538
|
-
const value = parseInt(raw, 10);
|
|
539
|
-
if (!Number.isFinite(value) || value < 320) {
|
|
540
|
-
throw new CliError(
|
|
541
|
-
ExitCode.InputError,
|
|
542
|
-
`nowline: invalid --width "${raw}". Must be an integer ≥ 320.`,
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
return value;
|
|
560
|
+
/**
|
|
561
|
+
* `--diagnostic-format` is the explicit switch. Left unset, a `--dry-run` that
|
|
562
|
+
* asks for `--format=json` gets JSON diagnostics (the machine-consumer shape
|
|
563
|
+
* in `specs/cli.md`); every other run keeps the text frames.
|
|
564
|
+
*/
|
|
565
|
+
function resolveDiagnosticFormat(args: ParsedArgs): DiagnosticFormat {
|
|
566
|
+
if (args.diagnosticFormat) return args.diagnosticFormat;
|
|
567
|
+
if (args.dryRun && args.format?.toLowerCase() === 'json') return 'json';
|
|
568
|
+
return 'text';
|
|
546
569
|
}
|
|
547
570
|
|
|
548
571
|
function emitDiagnostics(
|
|
549
|
-
diagnostics:
|
|
550
|
-
|
|
551
|
-
displayPath: string,
|
|
572
|
+
diagnostics: CliDiagnostic[],
|
|
573
|
+
sources: Map<string, DiagnosticSource>,
|
|
552
574
|
operatorLocale: string,
|
|
575
|
+
format: DiagnosticFormat,
|
|
576
|
+
log: Log,
|
|
553
577
|
): void {
|
|
554
|
-
|
|
555
|
-
const
|
|
578
|
+
// JSON is machine output and always prints whole; text drops warnings under -q.
|
|
579
|
+
const shown = format === 'text' ? errorsOnlyWhenQuiet(diagnostics, log) : diagnostics;
|
|
580
|
+
const rendered = formatDiagnostics(shown, format, sources, {
|
|
556
581
|
color: process.stderr.isTTY === true,
|
|
557
582
|
operatorLocale,
|
|
558
583
|
});
|
|
559
584
|
if (rendered) process.stderr.write(`${rendered}\n`);
|
|
560
585
|
}
|
|
561
586
|
|
|
587
|
+
/** Under `-q`, text-mode diagnostics keep only errors. Shared with serve. */
|
|
588
|
+
export function errorsOnlyWhenQuiet<T extends { severity: string }>(
|
|
589
|
+
diagnostics: T[],
|
|
590
|
+
log: Log,
|
|
591
|
+
): T[] {
|
|
592
|
+
return log.quiet ? diagnostics.filter((d) => d.severity === 'error') : diagnostics;
|
|
593
|
+
}
|
|
594
|
+
|
|
562
595
|
// ---- Asset resolver (still used by serve.ts) --------------------------------
|
|
563
596
|
|
|
564
597
|
export function createAssetResolver(assetRoot: string): AssetResolver {
|