@nowline/cli 0.8.6 → 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.
Files changed (89) hide show
  1. package/README.md +32 -9
  2. package/dist/cli/args.d.ts +20 -4
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +20 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/help.d.ts.map +1 -1
  7. package/dist/cli/help.js +30 -2
  8. package/dist/cli/help.js.map +1 -1
  9. package/dist/cli/options.d.ts +74 -0
  10. package/dist/cli/options.d.ts.map +1 -0
  11. package/dist/cli/options.js +209 -0
  12. package/dist/cli/options.js.map +1 -0
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +2 -3
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/mcp-install.d.ts +18 -0
  17. package/dist/commands/mcp-install.d.ts.map +1 -0
  18. package/dist/commands/mcp-install.js +151 -0
  19. package/dist/commands/mcp-install.js.map +1 -0
  20. package/dist/commands/mcp.d.ts.map +1 -1
  21. package/dist/commands/mcp.js +5 -11
  22. package/dist/commands/mcp.js.map +1 -1
  23. package/dist/commands/render.d.ts +7 -0
  24. package/dist/commands/render.d.ts.map +1 -1
  25. package/dist/commands/render.js +161 -155
  26. package/dist/commands/render.js.map +1 -1
  27. package/dist/commands/serve.d.ts.map +1 -1
  28. package/dist/commands/serve.js +87 -60
  29. package/dist/commands/serve.js.map +1 -1
  30. package/dist/diagnostics/adapt.d.ts +12 -1
  31. package/dist/diagnostics/adapt.d.ts.map +1 -1
  32. package/dist/diagnostics/adapt.js +53 -1
  33. package/dist/diagnostics/adapt.js.map +1 -1
  34. package/dist/diagnostics/model.d.ts +1 -1
  35. package/dist/generated/version.d.ts +1 -1
  36. package/dist/generated/version.js +2 -2
  37. package/dist/i18n/locale.d.ts.map +1 -1
  38. package/dist/i18n/locale.js +2 -5
  39. package/dist/i18n/locale.js.map +1 -1
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/io/config.d.ts +1 -1
  43. package/dist/io/config.d.ts.map +1 -1
  44. package/dist/io/config.js +1 -1
  45. package/dist/io/config.js.map +1 -1
  46. package/dist/io/log.d.ts +17 -0
  47. package/dist/io/log.d.ts.map +1 -0
  48. package/dist/io/log.js +16 -0
  49. package/dist/io/log.js.map +1 -0
  50. package/dist/mcp-install/clients.d.ts +58 -0
  51. package/dist/mcp-install/clients.d.ts.map +1 -0
  52. package/dist/mcp-install/clients.js +304 -0
  53. package/dist/mcp-install/clients.js.map +1 -0
  54. package/dist/mcp-install/host.d.ts +29 -0
  55. package/dist/mcp-install/host.d.ts.map +1 -0
  56. package/dist/mcp-install/host.js +72 -0
  57. package/dist/mcp-install/host.js.map +1 -0
  58. package/dist/mcp-install/json-config.d.ts +29 -0
  59. package/dist/mcp-install/json-config.d.ts.map +1 -0
  60. package/dist/mcp-install/json-config.js +139 -0
  61. package/dist/mcp-install/json-config.js.map +1 -0
  62. package/dist/mcp-install/server-command.d.ts +26 -0
  63. package/dist/mcp-install/server-command.d.ts.map +1 -0
  64. package/dist/mcp-install/server-command.js +52 -0
  65. package/dist/mcp-install/server-command.js.map +1 -0
  66. package/man/fr/nowline.1 +246 -19
  67. package/man/fr/nowline.5 +819 -45
  68. package/man/nowline.1 +191 -18
  69. package/man/nowline.5 +764 -36
  70. package/package.json +19 -19
  71. package/src/cli/args.ts +55 -5
  72. package/src/cli/help.ts +30 -2
  73. package/src/cli/options.ts +286 -0
  74. package/src/commands/init.ts +2 -3
  75. package/src/commands/mcp-install.ts +211 -0
  76. package/src/commands/mcp.ts +8 -13
  77. package/src/commands/render.ts +198 -195
  78. package/src/commands/serve.ts +102 -74
  79. package/src/diagnostics/adapt.ts +62 -0
  80. package/src/diagnostics/model.ts +1 -1
  81. package/src/generated/version.ts +2 -2
  82. package/src/i18n/locale.ts +2 -6
  83. package/src/index.ts +3 -0
  84. package/src/io/config.ts +0 -1
  85. package/src/io/log.ts +33 -0
  86. package/src/mcp-install/clients.ts +443 -0
  87. package/src/mcp-install/host.ts +106 -0
  88. package/src/mcp-install/json-config.ts +179 -0
  89. package/src/mcp-install/server-command.ts +75 -0
@@ -1,19 +1,17 @@
1
1
  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
- import { resolveIncludes } from '@nowline/core';
5
- import {
6
- layoutRoadmap,
7
- type NormalizedZone,
8
- normalizeZone,
9
- resolveToday,
10
- type ThemeName,
11
- TimezoneError,
12
- } from '@nowline/layout';
13
- import { renderSvg } from '@nowline/renderer';
4
+ import { isRoutedResolveDiagnostic } from '@nowline/core';
5
+ import type { ThemeName } from '@nowline/layout';
6
+ import { renderSvg, stageRoadmap } from '@nowline/renderer';
14
7
  import type { ParsedArgs } from '../cli/args.js';
8
+ import { resolveCliOptions } from '../cli/options.js';
15
9
  import { getServices, parseSource } from '../core/parse.js';
16
- import { type DiagnosticSource, formatDiagnostics } from '../diagnostics/index.js';
10
+ import {
11
+ adaptResolveDiagnostic,
12
+ type DiagnosticSource,
13
+ formatDiagnostics,
14
+ } from '../diagnostics/index.js';
17
15
  import {
18
16
  describeContentLocaleSource,
19
17
  operatorLocale,
@@ -21,7 +19,8 @@ import {
21
19
  resolveLocaleOverride,
22
20
  } from '../i18n/locale.js';
23
21
  import { CliError, ExitCode } from '../io/exit-codes.js';
24
- import { createAssetResolver } from './render.js';
22
+ import { createLog, type Log } from '../io/log.js';
23
+ import { createAssetResolver, errorsOnlyWhenQuiet, loadConfigFor } from './render.js';
25
24
 
26
25
  export interface ServeHandlerOptions {
27
26
  args: ParsedArgs;
@@ -60,11 +59,20 @@ export async function serveHandler(options: ServeHandlerOptions): Promise<void>
60
59
  );
61
60
  }
62
61
 
62
+ const log = createLog(args.logLevel);
63
63
  const port = parsePort(args.port);
64
64
  const host = args.host ?? '127.0.0.1';
65
- const theme = parseTheme(args.theme);
66
- const today = resolveNowArg(args);
67
- const resolvedLocale = resolveLocaleOverride({ flag: args.locale, env: process.env });
65
+ const { config, path: rcPath } = await loadConfigFor(args.positional, cwd);
66
+ // The same resolution as render: flag > .nowlinerc > default, so --width,
67
+ // the rc width, --no-links and --strict apply here too.
68
+ const cliOptions = resolveCliOptions(args, config, { rcPath, cwd });
69
+ cliOptions.assertValid();
70
+ const { theme, today, width, nonWorking } = cliOptions.surface;
71
+ const resolvedLocale = resolveLocaleOverride({
72
+ flag: args.locale,
73
+ env: process.env,
74
+ rc: cliOptions.rcLocale,
75
+ });
68
76
  const locale = resolvedLocale.tag;
69
77
  const opLocale = operatorLocale(resolvedLocale);
70
78
 
@@ -75,7 +83,9 @@ export async function serveHandler(options: ServeHandlerOptions): Promise<void>
75
83
  throw new CliError(ExitCode.InputError, `nowline: file not found: ${args.positional}`);
76
84
  }
77
85
 
78
- const assetRoot = args.assetRoot ? path.resolve(cwd, args.assetRoot) : path.dirname(inputPath);
86
+ // `--asset-root`, else the rc `assetRoot` (relative to the `.nowlinerc`), else
87
+ // the input's directory.
88
+ const assetRoot = cliOptions.assetRoot ?? path.dirname(inputPath);
79
89
 
80
90
  const fileSink = args.output ? path.resolve(cwd, args.output) : undefined;
81
91
 
@@ -91,44 +101,94 @@ export async function serveHandler(options: ServeHandlerOptions): Promise<void>
91
101
  const parse = await parseSource(text, inputPath, { validate: true });
92
102
  if (parse.hasErrors) {
93
103
  const sources = new Map<string, DiagnosticSource>([[inputPath, parse.source]]);
94
- const rendered = formatDiagnostics(parse.diagnostics, 'text', sources, {
95
- color: false,
96
- operatorLocale: opLocale,
97
- });
104
+ const format = (diagnostics: typeof parse.diagnostics): string =>
105
+ formatDiagnostics(diagnostics, 'text', sources, {
106
+ color: false,
107
+ operatorLocale: opLocale,
108
+ });
109
+ // The browser always gets the full report; stderr honors -q.
110
+ const rendered = format(parse.diagnostics);
98
111
  lastPayload = { kind: 'error', body: rendered };
99
- process.stderr.write(`${rendered}\n`);
112
+ process.stderr.write(`${format(errorsOnlyWhenQuiet(parse.diagnostics, log))}\n`);
100
113
  broadcast(clients, 'error', rendered);
101
114
  return;
102
115
  }
103
- if (args.logLevel === 'verbose') {
104
- const directive = readDirectiveLocale(parse.ast);
105
- const { tag, source } = describeContentLocaleSource(directive, resolvedLocale);
106
- process.stderr.write(`nowline: locale=${tag} (${source})\n`);
107
- }
108
- const resolved = await resolveIncludes(parse.ast, inputPath, {
109
- services: getServices().Nowline,
116
+ const directive = readDirectiveLocale(parse.ast);
117
+ const { tag, source } = describeContentLocaleSource(directive, resolvedLocale);
118
+ log.verbose(`nowline: locale=${tag} (${source})`);
119
+ // Included files' text, kept for the code frames of routed diagnostics.
120
+ const sources = new Map<string, DiagnosticSource>([[inputPath, parse.source]]);
121
+ const assetResolver = createAssetResolver(assetRoot);
122
+ const stage = await stageRoadmap({
123
+ source: text,
124
+ sourcePath: inputPath,
125
+ services: getServices(),
126
+ readSource: async (absPath) => {
127
+ const contents = await fs.readFile(absPath, 'utf-8');
128
+ sources.set(absPath, { file: absPath, contents });
129
+ return contents;
130
+ },
131
+ assetResolver,
132
+ document: parse.document,
133
+ layout: { theme, today, locale, width, nonWorking },
110
134
  });
111
- if (resolved.diagnostics.some((d) => d.severity === 'error')) {
112
- const msg = resolved.diagnostics
113
- .filter((d) => d.severity === 'error')
135
+ if (stage.kind === 'invalid') {
136
+ // The parse above found no errors, so the stage cannot disagree.
137
+ throw new Error(`${inputPath} failed validation.`);
138
+ }
139
+ if (stage.kind === 'include-error') {
140
+ // Uncoded include errors keep their one-line form, broadcast
141
+ // only, and take precedence: when any is present, routed
142
+ // (wave-rule) diagnostics are dropped. Otherwise routed
143
+ // diagnostics are reported like validator diagnostics
144
+ // (specs/waves.md §6.1).
145
+ const legacy = stage.resolved.diagnostics
146
+ .filter((d) => d.severity === 'error' && !isRoutedResolveDiagnostic(d))
114
147
  .map((d) => `${d.sourcePath}: ${d.message}`)
115
148
  .join('\n');
149
+ let msg = legacy;
150
+ if (!legacy && stage.routed.some((d) => d.severity === 'error')) {
151
+ const diagnostics = [
152
+ ...parse.diagnostics,
153
+ ...stage.routed.map((d) =>
154
+ adaptResolveDiagnostic(
155
+ d,
156
+ d.sourcePath,
157
+ sources.get(d.sourcePath)?.contents,
158
+ ),
159
+ ),
160
+ ];
161
+ const format = (shown: typeof diagnostics): string =>
162
+ formatDiagnostics(shown, 'text', sources, {
163
+ color: false,
164
+ operatorLocale: opLocale,
165
+ });
166
+ process.stderr.write(`${format(errorsOnlyWhenQuiet(diagnostics, log))}\n`);
167
+ msg = format(diagnostics);
168
+ }
116
169
  lastPayload = { kind: 'error', body: msg };
117
170
  broadcast(clients, 'error', msg);
118
171
  return;
119
172
  }
120
- const model = layoutRoadmap(parse.ast, resolved, { theme, today, locale });
121
- const svg = await renderSvg(model, {
122
- assetResolver: createAssetResolver(assetRoot),
123
- warn: (m) => process.stderr.write(`warning: ${m}\n`),
173
+ // The logo's load warnings, then the renderer's; `--strict` makes
174
+ // any of them an error, as in render.
175
+ const warnings = [...stage.warnings];
176
+ const svg = await renderSvg(stage.model, {
177
+ assetResolver,
178
+ logo: stage.logo,
179
+ noLinks: args.noLinks,
180
+ strict: args.strict,
181
+ warn: (m) => warnings.push(m),
124
182
  });
183
+ if (args.strict && warnings.length > 0) {
184
+ throw new Error(`render warnings (--strict): ${warnings.join('; ')}`);
185
+ }
186
+ for (const m of warnings) log.notice(`warning: ${m}`);
125
187
  lastPayload = { kind: 'svg', body: svg };
126
188
  if (fileSink) {
127
189
  try {
128
190
  await fs.writeFile(fileSink, svg);
129
- if (args.logLevel === 'verbose') {
130
- process.stderr.write(`nowline: wrote ${fileSink}\n`);
131
- }
191
+ log.verbose(`nowline: wrote ${fileSink}`);
132
192
  } catch (err) {
133
193
  const msg = err instanceof Error ? err.message : String(err);
134
194
  process.stderr.write(`nowline: failed to write ${fileSink}: ${msg}\n`);
@@ -183,7 +243,7 @@ export async function serveHandler(options: ServeHandlerOptions): Promise<void>
183
243
  process.stdout.write(`nowline serve: http://${host}:${port}\n`);
184
244
 
185
245
  if (args.open) {
186
- await openBrowser(`http://${host}:${port}`);
246
+ await openBrowser(`http://${host}:${port}`, log);
187
247
  }
188
248
 
189
249
  const shutdown = async (): Promise<void> => {
@@ -217,39 +277,6 @@ function parsePort(raw: string | undefined): number {
217
277
  return port;
218
278
  }
219
279
 
220
- function parseTheme(raw: string | undefined): ThemeName {
221
- const lower = (raw ?? 'light').toLowerCase();
222
- if (lower !== 'light' && lower !== 'dark') {
223
- throw new CliError(ExitCode.InputError, `nowline: invalid --theme "${raw}".`);
224
- }
225
- return lower;
226
- }
227
-
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
- );
249
- }
250
- return result;
251
- }
252
-
253
280
  function sendEvent(res: http.ServerResponse, event: string, data: string): void {
254
281
  const lines = data
255
282
  .split('\n')
@@ -279,7 +306,7 @@ function watchFile(target: string, onChange: () => Promise<void>): FSWatcher {
279
306
  return w;
280
307
  }
281
308
 
282
- async function openBrowser(url: string): Promise<void> {
309
+ async function openBrowser(url: string, log: Log): Promise<void> {
283
310
  const { spawn } = await import('node:child_process');
284
311
  const platform = process.platform;
285
312
  const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -288,7 +315,7 @@ async function openBrowser(url: string): Promise<void> {
288
315
  const child = spawn(command, argList, { stdio: 'ignore', detached: true });
289
316
  child.unref();
290
317
  } catch {
291
- process.stderr.write(`nowline: warning — failed to open browser; visit ${url} manually.\n`);
318
+ log.notice(`nowline: warning — failed to open browser; visit ${url} manually.`);
292
319
  }
293
320
  }
294
321
 
@@ -296,6 +323,7 @@ const emptySvg =
296
323
  '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="60"><text x="10" y="30" font-family="system-ui" font-size="14" fill="#999">no output yet</text></svg>';
297
324
 
298
325
  function shellHtml(theme: ThemeName): string {
326
+ // Only dark needs dark chrome; grayscale sits on the light page.
299
327
  const bg = theme === 'dark' ? '#121212' : '#ffffff';
300
328
  const fg = theme === 'dark' ? '#e0e0e0' : '#212121';
301
329
  return `<!doctype html>
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  extractSuggestion,
3
+ inferCodeFromMessage,
3
4
  type LangiumLikeDiagnostic,
4
5
  type LexerErrorLike,
5
6
  type ParserErrorLike,
7
+ type ResolveDiagnostic,
6
8
  resolveDiagnosticCode,
7
9
  } from '@nowline/core';
8
10
  import type { CliDiagnostic, DiagnosticSeverity, LocalizedMessageData } from './model.js';
@@ -87,3 +89,63 @@ export function adaptLexerError(err: LexerErrorLike, file: string): CliDiagnosti
87
89
  function mapSeverity(severity: number | undefined): DiagnosticSeverity {
88
90
  return severity === 2 ? 'warning' : 'error';
89
91
  }
92
+
93
+ /**
94
+ * Adapt a routed include-resolver diagnostic (a wave rule or an included
95
+ * file's NL.E0302). With a `range`, the column and span are the validator's
96
+ * for the same finding. Without one, the diagnostic carries only a 0-based
97
+ * line: `contents` is the text of the file it points into, when known, and
98
+ * the span covers the line's text, so the code frame still underlines the
99
+ * declaration. A coded diagnostic keeps `{ code, args }` in `data` for
100
+ * re-localization. An included file's syntax error is adapted like that
101
+ * file's own (`lex-error` / `parse-error`).
102
+ */
103
+ export function adaptResolveDiagnostic(
104
+ diag: ResolveDiagnostic,
105
+ file: string,
106
+ contents?: string,
107
+ ): CliDiagnostic {
108
+ // An included file's syntax error reads as it does when that file is
109
+ // checked on its own.
110
+ if (diag.syntax?.origin === 'lexer') return adaptLexerError(diag.syntax.error, file);
111
+ if (diag.syntax?.origin === 'parser') return adaptParserError(diag.syntax.error, file);
112
+ return {
113
+ file,
114
+ ...resolveLocation(diag, contents),
115
+ severity: diag.severity,
116
+ // WV8 is uncoded; label it the way the validator's copy is labelled.
117
+ code: diag.code ?? inferCodeFromMessage(diag.message),
118
+ message: diag.message,
119
+ suggestion: extractSuggestion(diag.message),
120
+ data: diag.code === undefined ? undefined : { code: diag.code, args: diag.args ?? [] },
121
+ };
122
+ }
123
+
124
+ function resolveLocation(
125
+ diag: ResolveDiagnostic,
126
+ contents: string | undefined,
127
+ ): Pick<CliDiagnostic, 'line' | 'column' | 'span'> {
128
+ if (diag.range) {
129
+ const line = diag.range.start.line + 1;
130
+ const column = diag.range.start.character + 1;
131
+ return {
132
+ line,
133
+ column,
134
+ span: {
135
+ start: { line, column },
136
+ end: { line: diag.range.end.line + 1, column: diag.range.end.character + 1 },
137
+ },
138
+ };
139
+ }
140
+ const line = (diag.line ?? 0) + 1;
141
+ const text = contents?.split(/\r?\n/)[line - 1]?.trimEnd() ?? '';
142
+ const column = text.length - text.trimStart().length + 1;
143
+ return {
144
+ line,
145
+ column,
146
+ span:
147
+ text.trim() === ''
148
+ ? undefined
149
+ : { start: { line, column }, end: { line, column: text.length + 1 } },
150
+ };
151
+ }
@@ -35,7 +35,7 @@ export interface CliDiagnostic {
35
35
  * Stable code + args as captured by the validator. Present on
36
36
  * messages that flow through the `tr()` registry; absent for
37
37
  * literal-English validator strings, parser/lexer errors, and
38
- * include-resolution diagnostics. The CLI re-formats `message`
38
+ * uncoded include-resolution diagnostics. The CLI re-formats `message`
39
39
  * from this when an operator locale is supplied to
40
40
  * `formatDiagnostics`.
41
41
  */
@@ -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.8.6";
3
+ export const CLI_VERSION = "0.9.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": "29a8950",
15
+ "sha": "06f9e6f",
16
16
  "isRelease": true,
17
17
  "isDirty": false
18
18
  };
@@ -1,4 +1,5 @@
1
1
  import type { NowlineFile } from '@nowline/core';
2
+ import { findProp } from '@nowline/core/internal';
2
3
 
3
4
  // Resolve the operator-locale override from CLI flag, environment, and
4
5
  // `.nowlinerc`. See `specs/localization.md` for the two-chain model.
@@ -100,12 +101,7 @@ export function describeContentLocaleSource(
100
101
  * absent, malformed, or omits the property.
101
102
  */
102
103
  export function readDirectiveLocale(file: NowlineFile | undefined): string | undefined {
103
- const prop = file?.directive?.properties.find((p) => stripColon(p.key) === 'locale');
104
- return prop?.value || undefined;
105
- }
106
-
107
- function stripColon(key: string): string {
108
- return key.endsWith(':') ? key.slice(0, -1) : key;
104
+ return findProp(file?.directive?.properties ?? [], 'locale')?.value || undefined;
109
105
  }
110
106
 
111
107
  /**
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { parseArgv } from './cli/args.js';
3
3
  import { renderHelp, renderVersion } from './cli/help.js';
4
4
  import { initHandler } from './commands/init.js';
5
5
  import { mcpHandler } from './commands/mcp.js';
6
+ import { mcpInstallHandler } from './commands/mcp-install.js';
6
7
  import { renderHandler } from './commands/render.js';
7
8
  import { serveHandler } from './commands/serve.js';
8
9
  import { CliError, ExitCode } from './io/exit-codes.js';
@@ -33,6 +34,8 @@ async function run(): Promise<number> {
33
34
  await serveHandler({ args: parsed });
34
35
  } else if (parsed.mode === 'mcp') {
35
36
  await mcpHandler({ args: parsed });
37
+ } else if (parsed.mode === 'mcp-install' || parsed.mode === 'mcp-uninstall') {
38
+ await mcpInstallHandler({ args: parsed });
36
39
  } else {
37
40
  await renderHandler({ args: parsed });
38
41
  }
package/src/io/config.ts CHANGED
@@ -5,7 +5,6 @@ export {
5
5
  type LoadConfigOptions,
6
6
  type LoadConfigResult,
7
7
  loadConfig,
8
- mergeConfig,
9
8
  type NowlineRc,
10
9
  parseConfig,
11
10
  } from '@nowline/config';
package/src/io/log.ts ADDED
@@ -0,0 +1,33 @@
1
+ export type LogLevel = 'verbose' | 'quiet' | 'normal';
2
+
3
+ /**
4
+ * The one place `-v` / `-q` decide what reaches stderr. Errors (usage errors,
5
+ * failed writes, validation errors) and machine output (JSON diagnostics)
6
+ * bypass this and always print; everything else goes through here so every
7
+ * command honors the flags the same way.
8
+ */
9
+ export interface Log {
10
+ /** `-v` only: progress lines such as `nowline: wrote <path>`. */
11
+ verbose(line: string): void;
12
+ /** Non-error notices (warnings, lossy summaries, banners). Dropped under `-q`. */
13
+ notice(line: string): void;
14
+ /** True under `-q`; callers that filter mixed output (diagnostics) check it. */
15
+ readonly quiet: boolean;
16
+ }
17
+
18
+ export function createLog(
19
+ level: LogLevel,
20
+ write: (chunk: string) => void = (chunk) => {
21
+ process.stderr.write(chunk);
22
+ },
23
+ ): Log {
24
+ return {
25
+ verbose: (line) => {
26
+ if (level === 'verbose') write(`${line}\n`);
27
+ },
28
+ notice: (line) => {
29
+ if (level !== 'quiet') write(`${line}\n`);
30
+ },
31
+ quiet: level === 'quiet',
32
+ };
33
+ }