@nowline/config 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.
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { type LoadConfigOptions, type LoadConfigResult, loadConfig, mergeConfig, type NowlineRc, parseConfig, } from './load-config.js';
1
+ export { type LoadConfigOptions, type LoadConfigResult, loadConfig, type NowlineRc, parseConfig, } from './load-config.js';
2
+ export { ASSET_ROOT_SPEC, MIN_WIDTH, type NowPolicy, type OptionError, type OptionKeyOf, type OptionLayer, type OptionParse, type OptionSource, type OptionSpec, type OptionSpecTable, type OptionValues, rcLayer, resolveOptions, resolveSurfaceOptions, SURFACE_OPTION_SPECS, SURFACE_RC_KEYS, type SurfaceOptionKey, type SurfaceOptionSpecs, type SurfaceOptions, type SurfaceRcValues, } from './surface-options.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,UAAU,EACV,WAAW,EACX,KAAK,SAAS,EACd,WAAW,GACd,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,UAAU,EACV,KAAK,SAAS,EACd,WAAW,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACH,eAAe,EACf,SAAS,EACT,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,OAAO,EACP,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,GACvB,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export { loadConfig, mergeConfig, parseConfig, } from './load-config.js';
1
+ export { loadConfig, parseConfig, } from './load-config.js';
2
+ export { ASSET_ROOT_SPEC, MIN_WIDTH, rcLayer, resolveOptions, resolveSurfaceOptions, SURFACE_OPTION_SPECS, SURFACE_RC_KEYS, } from './surface-options.js';
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,UAAU,EACV,WAAW,EAEX,WAAW,GACd,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,UAAU,EAEV,WAAW,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACH,eAAe,EACf,SAAS,EAUT,OAAO,EACP,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,GAKlB,MAAM,sBAAsB,CAAC"}
@@ -1,15 +1,14 @@
1
+ import type { SurfaceRcValues } from './surface-options.js';
1
2
  /**
2
- * Shape of the `.nowlinerc` file consumed by the CLI today and (post-m3d)
3
- * by the VS Code/Cursor extension. Keys are intentionally optional — the
4
- * file is sparse and any key may be absent.
3
+ * Shape of the `.nowlinerc` file consumed by the CLI and the VS Code/Cursor
4
+ * extension. Keys are intentionally optional — the file is sparse and any
5
+ * key may be absent. The keys the shared options schema owns (`theme`,
6
+ * `width`, `pdfPageSize`, `assetRoot`) take their types from
7
+ * `SurfaceRcValues`; nothing here is validated until a surface resolves it.
5
8
  */
6
- export interface NowlineRc {
7
- quiet?: boolean;
9
+ export interface NowlineRc extends SurfaceRcValues {
8
10
  format?: string;
9
- theme?: string;
10
11
  defaultFormat?: string;
11
- width?: number;
12
- pdfPageSize?: string;
13
12
  pdfOrientation?: string;
14
13
  pdfMargin?: string;
15
14
  fontSans?: string;
@@ -38,10 +37,4 @@ export interface LoadConfigOptions {
38
37
  */
39
38
  export declare function loadConfig(startDir: string, options?: LoadConfigOptions): Promise<LoadConfigResult>;
40
39
  export declare function parseConfig(contents: string, source: string): NowlineRc;
41
- /**
42
- * Layer flag values on top of a parsed config. `undefined` flag values
43
- * leave the underlying config key untouched — useful for "missing CLI
44
- * flag" semantics. Returned object is a shallow merge.
45
- */
46
- export declare function mergeConfig<T extends NowlineRc>(config: NowlineRc, flags: Partial<T>): T;
47
40
  //# sourceMappingURL=load-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;GAIG;AACH,wBAAsB,UAAU,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAsB3B;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAUvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAOxF"}
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,eAAe;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;GAIG;AACH,wBAAsB,UAAU,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAsB3B;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAUvE"}
@@ -39,20 +39,6 @@ export function parseConfig(contents, source) {
39
39
  }
40
40
  return parsed;
41
41
  }
42
- /**
43
- * Layer flag values on top of a parsed config. `undefined` flag values
44
- * leave the underlying config key untouched — useful for "missing CLI
45
- * flag" semantics. Returned object is a shallow merge.
46
- */
47
- export function mergeConfig(config, flags) {
48
- const merged = { ...config };
49
- for (const [key, value] of Object.entries(flags)) {
50
- if (value === undefined)
51
- continue;
52
- merged[key] = value;
53
- }
54
- return merged;
55
- }
56
42
  function parseJsonOrYaml(contents, source) {
57
43
  if (contents.startsWith('{') || contents.startsWith('[')) {
58
44
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ import { type PdfPageSize } from '@nowline/export-core';
2
+ import { type NonWorkingDisplay, type NormalizedZone, type ThemeName } from '@nowline/layout';
3
+ export type SurfaceOptionKey = 'theme' | 'width' | 'now' | 'pageSize' | 'nonWorking';
4
+ export interface SurfaceOptions {
5
+ theme: ThemeName;
6
+ /** Integer ≥ 320; undefined fits the content. */
7
+ width?: number;
8
+ /** The now-line date (UTC midnight); undefined suppresses it. */
9
+ today?: Date;
10
+ pageSize?: PdfPageSize;
11
+ /** Undefined lets the file's `default roadmap non-working:` key apply. */
12
+ nonWorking?: NonWorkingDisplay;
13
+ }
14
+ /** Where a raw value came from: what an error must name. */
15
+ export type OptionSource = {
16
+ kind: 'flag';
17
+ name: string;
18
+ } | {
19
+ kind: 'rc';
20
+ key: string;
21
+ } | {
22
+ kind: 'argument';
23
+ name: string;
24
+ } | {
25
+ kind: 'setting';
26
+ name: string;
27
+ };
28
+ /**
29
+ * One source's raw values. Layers are passed highest precedence first. `K`
30
+ * widens past the shared keys when a surface registers its own specs, and a
31
+ * layer over the union serves both resolutions.
32
+ */
33
+ export interface OptionLayer<K extends string = SurfaceOptionKey> {
34
+ source: (key: K) => OptionSource;
35
+ values: Partial<Record<K, unknown>>;
36
+ }
37
+ export interface OptionError<K extends string = SurfaceOptionKey> {
38
+ key: K;
39
+ source: OptionSource;
40
+ raw: unknown;
41
+ /** What was wrong, without a lead: `must be an integer ≥ 320`. */
42
+ detail: string;
43
+ /** The accepted values, for an enum option. */
44
+ expected?: readonly string[];
45
+ }
46
+ export interface NowPolicy {
47
+ /** Zone for "today" when no layer sets `now`; local when omitted. */
48
+ zone?: NormalizedZone;
49
+ clock?: () => Date;
50
+ }
51
+ /**
52
+ * A parser's verdict on the winning raw value. `value: undefined` is an
53
+ * explicit "unset" (an empty `--width ""`): the option stays at its default
54
+ * and lower layers are not consulted.
55
+ */
56
+ export type OptionParse<T> = {
57
+ ok: true;
58
+ value: T | undefined;
59
+ } | {
60
+ ok: false;
61
+ detail: string;
62
+ expected?: readonly string[];
63
+ };
64
+ /** How one option is read: its `.nowlinerc` key, when it has one, and its parser. */
65
+ export interface OptionSpec<T> {
66
+ rcKey?: string;
67
+ parse: (raw: unknown) => OptionParse<T>;
68
+ }
69
+ /**
70
+ * A set of option specs keyed by option name. The shared table is
71
+ * {@link SURFACE_OPTION_SPECS}; a surface registers its own keys (the CLI's
72
+ * `defaultFormat`, `pdfMargin`, ...) by passing its own table to
73
+ * {@link resolveOptions} and {@link rcLayer} over the same layers.
74
+ */
75
+ export type OptionSpecTable<S> = {
76
+ readonly [K in keyof S]: OptionSpec<unknown>;
77
+ };
78
+ /** The parsed values of a spec table; a key is absent when no layer set it. */
79
+ export type OptionValues<S extends OptionSpecTable<S>> = {
80
+ [K in keyof S]?: S[K] extends OptionSpec<infer V> ? V : never;
81
+ };
82
+ /** The string keys of a spec table. */
83
+ export type OptionKeyOf<S> = Extract<keyof S, string>;
84
+ /**
85
+ * The `.nowlinerc` key of every setting the schema owns; `undefined` means
86
+ * the option has no rc key. `assetRoot` is the default for `--asset-root`
87
+ * (specs/surface-options.md §7, decision 5). It is not a shared option (a
88
+ * path means something different to each surface), so it has its own spec,
89
+ * {@link ASSET_ROOT_SPEC}, that a surface adds to its table.
90
+ */
91
+ export declare const SURFACE_RC_KEYS: Readonly<{
92
+ readonly theme: 'theme';
93
+ readonly width: 'width';
94
+ readonly now: undefined;
95
+ readonly pageSize: 'pdfPageSize';
96
+ readonly nonWorking: undefined;
97
+ readonly assetRoot: 'assetRoot';
98
+ }>;
99
+ /**
100
+ * The value types of the `.nowlinerc` keys the schema owns, as a file may
101
+ * hold them before parsing. `NowlineRc` extends this.
102
+ */
103
+ export interface SurfaceRcValues {
104
+ /** A theme name, case-insensitive; `greyscale` is accepted. */
105
+ theme?: string;
106
+ /** Integer ≥ 320. A string is read with `parseInt`, as the CLI always has. */
107
+ width?: number | string;
108
+ /** A preset name, `content`, or `<W>x<H><unit>`. */
109
+ pdfPageSize?: string;
110
+ /** Base directory for `logo:` assets. */
111
+ assetRoot?: string;
112
+ }
113
+ /** The narrowest width a surface accepts, in px. */
114
+ export declare const MIN_WIDTH = 320;
115
+ /**
116
+ * How `assetRoot` is read: a path, kept as written. Resolving a relative one
117
+ * is the surface's job, because the base differs: a flag or setting resolves
118
+ * against the working directory, an rc value against the `.nowlinerc`'s own
119
+ * directory. Empty means unset.
120
+ */
121
+ export declare const ASSET_ROOT_SPEC: Readonly<OptionSpec<string>>;
122
+ export interface SurfaceOptionSpecs {
123
+ theme: OptionSpec<ThemeName>;
124
+ width: OptionSpec<number>;
125
+ /** `null` is the parsed `-` (suppress the now-line). */
126
+ now: OptionSpec<Date | null>;
127
+ pageSize: OptionSpec<PdfPageSize>;
128
+ nonWorking: OptionSpec<NonWorkingDisplay>;
129
+ }
130
+ /** The shared option specs, in the order their errors are reported. */
131
+ export declare const SURFACE_OPTION_SPECS: Readonly<SurfaceOptionSpecs>;
132
+ /**
133
+ * Resolve every option in `specs` over `layers` (highest precedence first).
134
+ * For each option the first layer holding a value other than `undefined` or
135
+ * `null` wins and is parsed; lower layers are never read for it, so a bad
136
+ * value below a valid one is not reported. A bad winning value is reported
137
+ * and the option is left unset. Errors follow the table's key order.
138
+ */
139
+ export declare function resolveOptions<S extends OptionSpecTable<S>>(specs: S, layers: readonly OptionLayer<OptionKeyOf<S>>[]): {
140
+ values: OptionValues<S>;
141
+ errors: OptionError<OptionKeyOf<S>>[];
142
+ };
143
+ /**
144
+ * Resolve the shared options. With no `now` from any layer (or a bad one),
145
+ * the now-line is today's civil date in `now.zone`; a `-` suppresses it.
146
+ */
147
+ export declare function resolveSurfaceOptions(layers: readonly OptionLayer[], defaults: {
148
+ theme: ThemeName;
149
+ }, now: NowPolicy): {
150
+ options: SurfaceOptions;
151
+ errors: OptionError[];
152
+ };
153
+ /**
154
+ * Turn a loaded `.nowlinerc` into a layer: each spec with an `rcKey` reads
155
+ * that key, and an error names it. Pass a surface's own spec table to read
156
+ * its keys; with none, the shared specs are read.
157
+ */
158
+ export declare function rcLayer(config: Readonly<Record<string, unknown>>): OptionLayer;
159
+ export declare function rcLayer<S extends OptionSpecTable<S>>(config: Readonly<Record<string, unknown>>, specs: S): OptionLayer<OptionKeyOf<S>>;
160
+ //# sourceMappingURL=surface-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-options.d.ts","sourceRoot":"","sources":["../src/surface-options.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ // The options every render surface shares: theme, width, now, page size and
2
+ // non-working display (specs/surface-options.md §3.1). Each option is parsed
3
+ // once, here, from whatever layers a surface stacks (flags, MCP arguments,
4
+ // editor settings, `.nowlinerc`), so a value accepted by one surface is
5
+ // accepted by all of them. Errors come back as fields, not text: each surface
6
+ // keeps its own message templates.
7
+ import { PageSizeParseError, parsePageSize } from '@nowline/export-core';
8
+ import { hasImpossibleNowDate, isNonWorkingDisplay, NON_WORKING_DISPLAYS, normalizeThemeName, resolveToday, THEME_NAMES, } from '@nowline/layout';
9
+ // ---- `.nowlinerc` keys ------------------------------------------------------
10
+ /**
11
+ * The `.nowlinerc` key of every setting the schema owns; `undefined` means
12
+ * the option has no rc key. `assetRoot` is the default for `--asset-root`
13
+ * (specs/surface-options.md §7, decision 5). It is not a shared option (a
14
+ * path means something different to each surface), so it has its own spec,
15
+ * {@link ASSET_ROOT_SPEC}, that a surface adds to its table.
16
+ */
17
+ export const SURFACE_RC_KEYS = Object.freeze({
18
+ theme: 'theme',
19
+ width: 'width',
20
+ now: undefined,
21
+ pageSize: 'pdfPageSize',
22
+ nonWorking: undefined,
23
+ assetRoot: 'assetRoot',
24
+ });
25
+ // ---- Option specs -----------------------------------------------------------
26
+ /** The narrowest width a surface accepts, in px. */
27
+ export const MIN_WIDTH = 320;
28
+ const NOW_DETAIL = 'must be YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], or "-"';
29
+ // `2026-02-30` has the right shape but names no day; it is an error, not March 2.
30
+ const NOW_REAL_DAY_DETAIL = 'must be a real calendar date';
31
+ function parseTheme(raw) {
32
+ const theme = typeof raw === 'string' ? normalizeThemeName(raw) : undefined;
33
+ if (theme)
34
+ return { ok: true, value: theme };
35
+ return { ok: false, detail: 'must be a theme name', expected: THEME_NAMES };
36
+ }
37
+ // The CLI's rule, kept exactly: a string goes through `parseInt`, so
38
+ // `"1200px"` reads as 1200; anything else must already be a number.
39
+ function parseWidth(raw) {
40
+ if (raw === '')
41
+ return { ok: true, value: undefined };
42
+ const value = typeof raw === 'number'
43
+ ? raw
44
+ : typeof raw === 'string'
45
+ ? Number.parseInt(raw, 10)
46
+ : Number.NaN;
47
+ if (Number.isInteger(value) && value >= MIN_WIDTH)
48
+ return { ok: true, value };
49
+ return { ok: false, detail: `must be an integer ≥ ${MIN_WIDTH}` };
50
+ }
51
+ // `null` is a parsed `-`: the now-line is suppressed.
52
+ function parseNow(raw) {
53
+ if (raw instanceof Date) {
54
+ if (!Number.isNaN(raw.getTime()))
55
+ return { ok: true, value: raw };
56
+ }
57
+ else if (raw === '-') {
58
+ return { ok: true, value: null };
59
+ }
60
+ else if (typeof raw === 'string') {
61
+ const today = resolveToday({ now: raw });
62
+ if (today)
63
+ return { ok: true, value: today };
64
+ if (hasImpossibleNowDate(raw))
65
+ return { ok: false, detail: NOW_REAL_DAY_DETAIL };
66
+ }
67
+ return { ok: false, detail: NOW_DETAIL };
68
+ }
69
+ function parsePageSizeOption(raw) {
70
+ if (raw === '')
71
+ return { ok: true, value: undefined };
72
+ try {
73
+ return { ok: true, value: parsePageSize(String(raw)) };
74
+ }
75
+ catch (err) {
76
+ if (!(err instanceof PageSizeParseError))
77
+ throw err;
78
+ // The parser names a generic "page size"; the surface supplies the lead.
79
+ return { ok: false, detail: err.message.replace(/^invalid page size "[^"]*": /, '') };
80
+ }
81
+ }
82
+ function parseNonWorking(raw) {
83
+ if (raw === '')
84
+ return { ok: true, value: undefined };
85
+ if (isNonWorkingDisplay(raw))
86
+ return { ok: true, value: raw };
87
+ return { ok: false, detail: 'must be a non-working display', expected: NON_WORKING_DISPLAYS };
88
+ }
89
+ function parseAssetRoot(raw) {
90
+ if (raw === '')
91
+ return { ok: true, value: undefined };
92
+ if (typeof raw === 'string')
93
+ return { ok: true, value: raw };
94
+ return { ok: false, detail: 'must be a directory path' };
95
+ }
96
+ /**
97
+ * How `assetRoot` is read: a path, kept as written. Resolving a relative one
98
+ * is the surface's job, because the base differs: a flag or setting resolves
99
+ * against the working directory, an rc value against the `.nowlinerc`'s own
100
+ * directory. Empty means unset.
101
+ */
102
+ export const ASSET_ROOT_SPEC = Object.freeze({
103
+ rcKey: SURFACE_RC_KEYS.assetRoot,
104
+ parse: parseAssetRoot,
105
+ });
106
+ /** The shared option specs, in the order their errors are reported. */
107
+ export const SURFACE_OPTION_SPECS = Object.freeze({
108
+ theme: { rcKey: SURFACE_RC_KEYS.theme, parse: parseTheme },
109
+ width: { rcKey: SURFACE_RC_KEYS.width, parse: parseWidth },
110
+ now: { parse: parseNow },
111
+ pageSize: { rcKey: SURFACE_RC_KEYS.pageSize, parse: parsePageSizeOption },
112
+ nonWorking: { parse: parseNonWorking },
113
+ });
114
+ // ---- Resolution -------------------------------------------------------------
115
+ /**
116
+ * Resolve every option in `specs` over `layers` (highest precedence first).
117
+ * For each option the first layer holding a value other than `undefined` or
118
+ * `null` wins and is parsed; lower layers are never read for it, so a bad
119
+ * value below a valid one is not reported. A bad winning value is reported
120
+ * and the option is left unset. Errors follow the table's key order.
121
+ */
122
+ export function resolveOptions(specs, layers) {
123
+ const values = {};
124
+ const errors = [];
125
+ for (const key of Object.keys(specs)) {
126
+ const layer = layers.find((candidate) => isGiven(candidate.values[key]));
127
+ if (!layer)
128
+ continue;
129
+ const raw = layer.values[key];
130
+ const parsed = specs[key].parse(raw);
131
+ if (!parsed.ok) {
132
+ const error = {
133
+ key,
134
+ source: layer.source(key),
135
+ raw,
136
+ detail: parsed.detail,
137
+ };
138
+ if (parsed.expected)
139
+ error.expected = parsed.expected;
140
+ errors.push(error);
141
+ }
142
+ else if (parsed.value !== undefined) {
143
+ values[key] = parsed.value;
144
+ }
145
+ }
146
+ return { values, errors };
147
+ }
148
+ /**
149
+ * Resolve the shared options. With no `now` from any layer (or a bad one),
150
+ * the now-line is today's civil date in `now.zone`; a `-` suppresses it.
151
+ */
152
+ export function resolveSurfaceOptions(layers, defaults, now) {
153
+ const { values, errors } = resolveOptions(SURFACE_OPTION_SPECS, layers);
154
+ const today = values.now === undefined
155
+ ? resolveToday({ zone: now.zone, clock: now.clock })
156
+ : (values.now ?? undefined);
157
+ const options = { theme: values.theme ?? defaults.theme };
158
+ if (values.width !== undefined)
159
+ options.width = values.width;
160
+ if (today !== undefined)
161
+ options.today = today;
162
+ if (values.pageSize !== undefined)
163
+ options.pageSize = values.pageSize;
164
+ if (values.nonWorking !== undefined)
165
+ options.nonWorking = values.nonWorking;
166
+ return { options, errors };
167
+ }
168
+ export function rcLayer(config, specs = SURFACE_OPTION_SPECS) {
169
+ const values = {};
170
+ for (const [key, spec] of Object.entries(specs)) {
171
+ if (spec.rcKey !== undefined)
172
+ values[key] = config[spec.rcKey];
173
+ }
174
+ return {
175
+ source: (key) => ({ kind: 'rc', key: specs[key]?.rcKey ?? key }),
176
+ values,
177
+ };
178
+ }
179
+ function isGiven(value) {
180
+ return value !== undefined && value !== null;
181
+ }
182
+ //# sourceMappingURL=surface-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-options.js","sourceRoot":"","sources":["../src/surface-options.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowline/config",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "description": "Loads Nowline project settings (the .nowlinerc file) for the CLI and editor integrations.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -21,12 +21,14 @@
21
21
  "src/"
22
22
  ],
23
23
  "dependencies": {
24
- "js-yaml": "^5.4.2"
24
+ "@nowline/export-core": "0.9.0",
25
+ "@nowline/layout": "0.9.0",
26
+ "js-yaml": "^5.5.0"
25
27
  },
26
28
  "devDependencies": {
27
- "@types/node": "^26.6.3",
29
+ "@types/node": "^26.6.5",
28
30
  "typescript": "^7.0.2",
29
- "vitest": "^5.0.2"
31
+ "vitest": "^5.0.5"
30
32
  },
31
33
  "scripts": {
32
34
  "build": "tsc -b tsconfig.json",
package/src/index.ts CHANGED
@@ -2,7 +2,28 @@ export {
2
2
  type LoadConfigOptions,
3
3
  type LoadConfigResult,
4
4
  loadConfig,
5
- mergeConfig,
6
5
  type NowlineRc,
7
6
  parseConfig,
8
7
  } from './load-config.js';
8
+ export {
9
+ ASSET_ROOT_SPEC,
10
+ MIN_WIDTH,
11
+ type NowPolicy,
12
+ type OptionError,
13
+ type OptionKeyOf,
14
+ type OptionLayer,
15
+ type OptionParse,
16
+ type OptionSource,
17
+ type OptionSpec,
18
+ type OptionSpecTable,
19
+ type OptionValues,
20
+ rcLayer,
21
+ resolveOptions,
22
+ resolveSurfaceOptions,
23
+ SURFACE_OPTION_SPECS,
24
+ SURFACE_RC_KEYS,
25
+ type SurfaceOptionKey,
26
+ type SurfaceOptionSpecs,
27
+ type SurfaceOptions,
28
+ type SurfaceRcValues,
29
+ } from './surface-options.js';
@@ -1,19 +1,18 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as yaml from 'js-yaml';
4
+ import type { SurfaceRcValues } from './surface-options.js';
4
5
 
5
6
  /**
6
- * Shape of the `.nowlinerc` file consumed by the CLI today and (post-m3d)
7
- * by the VS Code/Cursor extension. Keys are intentionally optional — the
8
- * file is sparse and any key may be absent.
7
+ * Shape of the `.nowlinerc` file consumed by the CLI and the VS Code/Cursor
8
+ * extension. Keys are intentionally optional — the file is sparse and any
9
+ * key may be absent. The keys the shared options schema owns (`theme`,
10
+ * `width`, `pdfPageSize`, `assetRoot`) take their types from
11
+ * `SurfaceRcValues`; nothing here is validated until a surface resolves it.
9
12
  */
10
- export interface NowlineRc {
11
- quiet?: boolean;
13
+ export interface NowlineRc extends SurfaceRcValues {
12
14
  format?: string;
13
- theme?: string;
14
15
  defaultFormat?: string;
15
- width?: number;
16
- pdfPageSize?: string;
17
16
  pdfOrientation?: string;
18
17
  pdfMargin?: string;
19
18
  fontSans?: string;
@@ -84,20 +83,6 @@ export function parseConfig(contents: string, source: string): NowlineRc {
84
83
  return parsed as NowlineRc;
85
84
  }
86
85
 
87
- /**
88
- * Layer flag values on top of a parsed config. `undefined` flag values
89
- * leave the underlying config key untouched — useful for "missing CLI
90
- * flag" semantics. Returned object is a shallow merge.
91
- */
92
- export function mergeConfig<T extends NowlineRc>(config: NowlineRc, flags: Partial<T>): T {
93
- const merged: NowlineRc = { ...config };
94
- for (const [key, value] of Object.entries(flags)) {
95
- if (value === undefined) continue;
96
- merged[key] = value as unknown;
97
- }
98
- return merged as T;
99
- }
100
-
101
86
  function parseJsonOrYaml(contents: string, source: string): unknown {
102
87
  if (contents.startsWith('{') || contents.startsWith('[')) {
103
88
  try {
@@ -0,0 +1,312 @@
1
+ // The options every render surface shares: theme, width, now, page size and
2
+ // non-working display (specs/surface-options.md §3.1). Each option is parsed
3
+ // once, here, from whatever layers a surface stacks (flags, MCP arguments,
4
+ // editor settings, `.nowlinerc`), so a value accepted by one surface is
5
+ // accepted by all of them. Errors come back as fields, not text: each surface
6
+ // keeps its own message templates.
7
+
8
+ import { PageSizeParseError, type PdfPageSize, parsePageSize } from '@nowline/export-core';
9
+ import {
10
+ hasImpossibleNowDate,
11
+ isNonWorkingDisplay,
12
+ NON_WORKING_DISPLAYS,
13
+ type NonWorkingDisplay,
14
+ type NormalizedZone,
15
+ normalizeThemeName,
16
+ resolveToday,
17
+ THEME_NAMES,
18
+ type ThemeName,
19
+ } from '@nowline/layout';
20
+
21
+ // ---- Public types -----------------------------------------------------------
22
+
23
+ export type SurfaceOptionKey = 'theme' | 'width' | 'now' | 'pageSize' | 'nonWorking';
24
+
25
+ export interface SurfaceOptions {
26
+ theme: ThemeName;
27
+ /** Integer ≥ 320; undefined fits the content. */
28
+ width?: number;
29
+ /** The now-line date (UTC midnight); undefined suppresses it. */
30
+ today?: Date;
31
+ pageSize?: PdfPageSize;
32
+ /** Undefined lets the file's `default roadmap non-working:` key apply. */
33
+ nonWorking?: NonWorkingDisplay;
34
+ }
35
+
36
+ /** Where a raw value came from: what an error must name. */
37
+ export type OptionSource =
38
+ | { kind: 'flag'; name: string } // --width
39
+ | { kind: 'rc'; key: string } // .nowlinerc width
40
+ | { kind: 'argument'; name: string } // MCP width
41
+ | { kind: 'setting'; name: string }; // nowline.preview.width
42
+
43
+ /**
44
+ * One source's raw values. Layers are passed highest precedence first. `K`
45
+ * widens past the shared keys when a surface registers its own specs, and a
46
+ * layer over the union serves both resolutions.
47
+ */
48
+ export interface OptionLayer<K extends string = SurfaceOptionKey> {
49
+ source: (key: K) => OptionSource;
50
+ values: Partial<Record<K, unknown>>;
51
+ }
52
+
53
+ export interface OptionError<K extends string = SurfaceOptionKey> {
54
+ key: K;
55
+ source: OptionSource;
56
+ raw: unknown;
57
+ /** What was wrong, without a lead: `must be an integer ≥ 320`. */
58
+ detail: string;
59
+ /** The accepted values, for an enum option. */
60
+ expected?: readonly string[];
61
+ }
62
+
63
+ export interface NowPolicy {
64
+ /** Zone for "today" when no layer sets `now`; local when omitted. */
65
+ zone?: NormalizedZone;
66
+ clock?: () => Date;
67
+ }
68
+
69
+ /**
70
+ * A parser's verdict on the winning raw value. `value: undefined` is an
71
+ * explicit "unset" (an empty `--width ""`): the option stays at its default
72
+ * and lower layers are not consulted.
73
+ */
74
+ export type OptionParse<T> =
75
+ | { ok: true; value: T | undefined }
76
+ | { ok: false; detail: string; expected?: readonly string[] };
77
+
78
+ /** How one option is read: its `.nowlinerc` key, when it has one, and its parser. */
79
+ export interface OptionSpec<T> {
80
+ rcKey?: string;
81
+ parse: (raw: unknown) => OptionParse<T>;
82
+ }
83
+
84
+ /**
85
+ * A set of option specs keyed by option name. The shared table is
86
+ * {@link SURFACE_OPTION_SPECS}; a surface registers its own keys (the CLI's
87
+ * `defaultFormat`, `pdfMargin`, ...) by passing its own table to
88
+ * {@link resolveOptions} and {@link rcLayer} over the same layers.
89
+ */
90
+ export type OptionSpecTable<S> = { readonly [K in keyof S]: OptionSpec<unknown> };
91
+
92
+ /** The parsed values of a spec table; a key is absent when no layer set it. */
93
+ export type OptionValues<S extends OptionSpecTable<S>> = {
94
+ [K in keyof S]?: S[K] extends OptionSpec<infer V> ? V : never;
95
+ };
96
+
97
+ /** The string keys of a spec table. */
98
+ export type OptionKeyOf<S> = Extract<keyof S, string>;
99
+
100
+ // ---- `.nowlinerc` keys ------------------------------------------------------
101
+
102
+ /**
103
+ * The `.nowlinerc` key of every setting the schema owns; `undefined` means
104
+ * the option has no rc key. `assetRoot` is the default for `--asset-root`
105
+ * (specs/surface-options.md §7, decision 5). It is not a shared option (a
106
+ * path means something different to each surface), so it has its own spec,
107
+ * {@link ASSET_ROOT_SPEC}, that a surface adds to its table.
108
+ */
109
+ export const SURFACE_RC_KEYS = Object.freeze({
110
+ theme: 'theme',
111
+ width: 'width',
112
+ now: undefined,
113
+ pageSize: 'pdfPageSize',
114
+ nonWorking: undefined,
115
+ assetRoot: 'assetRoot',
116
+ } as const satisfies Record<SurfaceOptionKey | 'assetRoot', string | undefined>);
117
+
118
+ /**
119
+ * The value types of the `.nowlinerc` keys the schema owns, as a file may
120
+ * hold them before parsing. `NowlineRc` extends this.
121
+ */
122
+ export interface SurfaceRcValues {
123
+ /** A theme name, case-insensitive; `greyscale` is accepted. */
124
+ theme?: string;
125
+ /** Integer ≥ 320. A string is read with `parseInt`, as the CLI always has. */
126
+ width?: number | string;
127
+ /** A preset name, `content`, or `<W>x<H><unit>`. */
128
+ pdfPageSize?: string;
129
+ /** Base directory for `logo:` assets. */
130
+ assetRoot?: string;
131
+ }
132
+
133
+ // ---- Option specs -----------------------------------------------------------
134
+
135
+ /** The narrowest width a surface accepts, in px. */
136
+ export const MIN_WIDTH = 320;
137
+
138
+ const NOW_DETAIL = 'must be YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], or "-"';
139
+ // `2026-02-30` has the right shape but names no day; it is an error, not March 2.
140
+ const NOW_REAL_DAY_DETAIL = 'must be a real calendar date';
141
+
142
+ function parseTheme(raw: unknown): OptionParse<ThemeName> {
143
+ const theme = typeof raw === 'string' ? normalizeThemeName(raw) : undefined;
144
+ if (theme) return { ok: true, value: theme };
145
+ return { ok: false, detail: 'must be a theme name', expected: THEME_NAMES };
146
+ }
147
+
148
+ // The CLI's rule, kept exactly: a string goes through `parseInt`, so
149
+ // `"1200px"` reads as 1200; anything else must already be a number.
150
+ function parseWidth(raw: unknown): OptionParse<number> {
151
+ if (raw === '') return { ok: true, value: undefined };
152
+ const value =
153
+ typeof raw === 'number'
154
+ ? raw
155
+ : typeof raw === 'string'
156
+ ? Number.parseInt(raw, 10)
157
+ : Number.NaN;
158
+ if (Number.isInteger(value) && value >= MIN_WIDTH) return { ok: true, value };
159
+ return { ok: false, detail: `must be an integer ≥ ${MIN_WIDTH}` };
160
+ }
161
+
162
+ // `null` is a parsed `-`: the now-line is suppressed.
163
+ function parseNow(raw: unknown): OptionParse<Date | null> {
164
+ if (raw instanceof Date) {
165
+ if (!Number.isNaN(raw.getTime())) return { ok: true, value: raw };
166
+ } else if (raw === '-') {
167
+ return { ok: true, value: null };
168
+ } else if (typeof raw === 'string') {
169
+ const today = resolveToday({ now: raw });
170
+ if (today) return { ok: true, value: today };
171
+ if (hasImpossibleNowDate(raw)) return { ok: false, detail: NOW_REAL_DAY_DETAIL };
172
+ }
173
+ return { ok: false, detail: NOW_DETAIL };
174
+ }
175
+
176
+ function parsePageSizeOption(raw: unknown): OptionParse<PdfPageSize> {
177
+ if (raw === '') return { ok: true, value: undefined };
178
+ try {
179
+ return { ok: true, value: parsePageSize(String(raw)) };
180
+ } catch (err) {
181
+ if (!(err instanceof PageSizeParseError)) throw err;
182
+ // The parser names a generic "page size"; the surface supplies the lead.
183
+ return { ok: false, detail: err.message.replace(/^invalid page size "[^"]*": /, '') };
184
+ }
185
+ }
186
+
187
+ function parseNonWorking(raw: unknown): OptionParse<NonWorkingDisplay> {
188
+ if (raw === '') return { ok: true, value: undefined };
189
+ if (isNonWorkingDisplay(raw)) return { ok: true, value: raw };
190
+ return { ok: false, detail: 'must be a non-working display', expected: NON_WORKING_DISPLAYS };
191
+ }
192
+
193
+ function parseAssetRoot(raw: unknown): OptionParse<string> {
194
+ if (raw === '') return { ok: true, value: undefined };
195
+ if (typeof raw === 'string') return { ok: true, value: raw };
196
+ return { ok: false, detail: 'must be a directory path' };
197
+ }
198
+
199
+ /**
200
+ * How `assetRoot` is read: a path, kept as written. Resolving a relative one
201
+ * is the surface's job, because the base differs: a flag or setting resolves
202
+ * against the working directory, an rc value against the `.nowlinerc`'s own
203
+ * directory. Empty means unset.
204
+ */
205
+ export const ASSET_ROOT_SPEC: Readonly<OptionSpec<string>> = Object.freeze({
206
+ rcKey: SURFACE_RC_KEYS.assetRoot,
207
+ parse: parseAssetRoot,
208
+ });
209
+
210
+ export interface SurfaceOptionSpecs {
211
+ theme: OptionSpec<ThemeName>;
212
+ width: OptionSpec<number>;
213
+ /** `null` is the parsed `-` (suppress the now-line). */
214
+ now: OptionSpec<Date | null>;
215
+ pageSize: OptionSpec<PdfPageSize>;
216
+ nonWorking: OptionSpec<NonWorkingDisplay>;
217
+ }
218
+
219
+ /** The shared option specs, in the order their errors are reported. */
220
+ export const SURFACE_OPTION_SPECS: Readonly<SurfaceOptionSpecs> = Object.freeze({
221
+ theme: { rcKey: SURFACE_RC_KEYS.theme, parse: parseTheme },
222
+ width: { rcKey: SURFACE_RC_KEYS.width, parse: parseWidth },
223
+ now: { parse: parseNow },
224
+ pageSize: { rcKey: SURFACE_RC_KEYS.pageSize, parse: parsePageSizeOption },
225
+ nonWorking: { parse: parseNonWorking },
226
+ });
227
+
228
+ // ---- Resolution -------------------------------------------------------------
229
+
230
+ /**
231
+ * Resolve every option in `specs` over `layers` (highest precedence first).
232
+ * For each option the first layer holding a value other than `undefined` or
233
+ * `null` wins and is parsed; lower layers are never read for it, so a bad
234
+ * value below a valid one is not reported. A bad winning value is reported
235
+ * and the option is left unset. Errors follow the table's key order.
236
+ */
237
+ export function resolveOptions<S extends OptionSpecTable<S>>(
238
+ specs: S,
239
+ layers: readonly OptionLayer<OptionKeyOf<S>>[],
240
+ ): { values: OptionValues<S>; errors: OptionError<OptionKeyOf<S>>[] } {
241
+ const values: OptionValues<S> = {};
242
+ const errors: OptionError<OptionKeyOf<S>>[] = [];
243
+ for (const key of Object.keys(specs) as OptionKeyOf<S>[]) {
244
+ const layer = layers.find((candidate) => isGiven(candidate.values[key]));
245
+ if (!layer) continue;
246
+ const raw = layer.values[key];
247
+ const parsed = specs[key].parse(raw);
248
+ if (!parsed.ok) {
249
+ const error: OptionError<OptionKeyOf<S>> = {
250
+ key,
251
+ source: layer.source(key),
252
+ raw,
253
+ detail: parsed.detail,
254
+ };
255
+ if (parsed.expected) error.expected = parsed.expected;
256
+ errors.push(error);
257
+ } else if (parsed.value !== undefined) {
258
+ values[key] = parsed.value as OptionValues<S>[OptionKeyOf<S>];
259
+ }
260
+ }
261
+ return { values, errors };
262
+ }
263
+
264
+ /**
265
+ * Resolve the shared options. With no `now` from any layer (or a bad one),
266
+ * the now-line is today's civil date in `now.zone`; a `-` suppresses it.
267
+ */
268
+ export function resolveSurfaceOptions(
269
+ layers: readonly OptionLayer[],
270
+ defaults: { theme: ThemeName },
271
+ now: NowPolicy,
272
+ ): { options: SurfaceOptions; errors: OptionError[] } {
273
+ const { values, errors } = resolveOptions(SURFACE_OPTION_SPECS, layers);
274
+ const today =
275
+ values.now === undefined
276
+ ? resolveToday({ zone: now.zone, clock: now.clock })
277
+ : (values.now ?? undefined);
278
+ const options: SurfaceOptions = { theme: values.theme ?? defaults.theme };
279
+ if (values.width !== undefined) options.width = values.width;
280
+ if (today !== undefined) options.today = today;
281
+ if (values.pageSize !== undefined) options.pageSize = values.pageSize;
282
+ if (values.nonWorking !== undefined) options.nonWorking = values.nonWorking;
283
+ return { options, errors };
284
+ }
285
+
286
+ /**
287
+ * Turn a loaded `.nowlinerc` into a layer: each spec with an `rcKey` reads
288
+ * that key, and an error names it. Pass a surface's own spec table to read
289
+ * its keys; with none, the shared specs are read.
290
+ */
291
+ export function rcLayer(config: Readonly<Record<string, unknown>>): OptionLayer;
292
+ export function rcLayer<S extends OptionSpecTable<S>>(
293
+ config: Readonly<Record<string, unknown>>,
294
+ specs: S,
295
+ ): OptionLayer<OptionKeyOf<S>>;
296
+ export function rcLayer(
297
+ config: Readonly<Record<string, unknown>>,
298
+ specs: Readonly<Record<string, OptionSpec<unknown>>> = SURFACE_OPTION_SPECS,
299
+ ): OptionLayer<string> {
300
+ const values: Record<string, unknown> = {};
301
+ for (const [key, spec] of Object.entries(specs)) {
302
+ if (spec.rcKey !== undefined) values[key] = config[spec.rcKey];
303
+ }
304
+ return {
305
+ source: (key) => ({ kind: 'rc', key: specs[key]?.rcKey ?? key }),
306
+ values,
307
+ };
308
+ }
309
+
310
+ function isGiven(value: unknown): boolean {
311
+ return value !== undefined && value !== null;
312
+ }