@nowline/cli 0.2.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 (139) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +372 -0
  3. package/dist/cli/args.d.ts +54 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/args.js +165 -0
  6. package/dist/cli/args.js.map +1 -0
  7. package/dist/cli/formats.d.ts +61 -0
  8. package/dist/cli/formats.d.ts.map +1 -0
  9. package/dist/cli/formats.js +153 -0
  10. package/dist/cli/formats.js.map +1 -0
  11. package/dist/cli/help.d.ts +3 -0
  12. package/dist/cli/help.d.ts.map +1 -0
  13. package/dist/cli/help.js +90 -0
  14. package/dist/cli/help.js.map +1 -0
  15. package/dist/cli/output-path.d.ts +57 -0
  16. package/dist/cli/output-path.d.ts.map +1 -0
  17. package/dist/cli/output-path.js +70 -0
  18. package/dist/cli/output-path.js.map +1 -0
  19. package/dist/commands/init.d.ts +20 -0
  20. package/dist/commands/init.d.ts.map +1 -0
  21. package/dist/commands/init.js +80 -0
  22. package/dist/commands/init.js.map +1 -0
  23. package/dist/commands/render.d.ts +15 -0
  24. package/dist/commands/render.d.ts.map +1 -0
  25. package/dist/commands/render.js +435 -0
  26. package/dist/commands/render.js.map +1 -0
  27. package/dist/commands/serve.d.ts +16 -0
  28. package/dist/commands/serve.d.ts.map +1 -0
  29. package/dist/commands/serve.js +287 -0
  30. package/dist/commands/serve.js.map +1 -0
  31. package/dist/convert/parse-json.d.ts +7 -0
  32. package/dist/convert/parse-json.d.ts.map +1 -0
  33. package/dist/convert/parse-json.js +34 -0
  34. package/dist/convert/parse-json.js.map +1 -0
  35. package/dist/convert/printer.d.ts +6 -0
  36. package/dist/convert/printer.d.ts.map +1 -0
  37. package/dist/convert/printer.js +334 -0
  38. package/dist/convert/printer.js.map +1 -0
  39. package/dist/convert/schema.d.ts +33 -0
  40. package/dist/convert/schema.d.ts.map +1 -0
  41. package/dist/convert/schema.js +77 -0
  42. package/dist/convert/schema.js.map +1 -0
  43. package/dist/core/parse.d.ts +24 -0
  44. package/dist/core/parse.d.ts.map +1 -0
  45. package/dist/core/parse.js +58 -0
  46. package/dist/core/parse.js.map +1 -0
  47. package/dist/diagnostics/adapt.d.ts +46 -0
  48. package/dist/diagnostics/adapt.d.ts.map +1 -0
  49. package/dist/diagnostics/adapt.js +109 -0
  50. package/dist/diagnostics/adapt.js.map +1 -0
  51. package/dist/diagnostics/format.d.ts +18 -0
  52. package/dist/diagnostics/format.d.ts.map +1 -0
  53. package/dist/diagnostics/format.js +41 -0
  54. package/dist/diagnostics/format.js.map +1 -0
  55. package/dist/diagnostics/index.d.ts +5 -0
  56. package/dist/diagnostics/index.d.ts.map +1 -0
  57. package/dist/diagnostics/index.js +5 -0
  58. package/dist/diagnostics/index.js.map +1 -0
  59. package/dist/diagnostics/json.d.ts +8 -0
  60. package/dist/diagnostics/json.d.ts.map +1 -0
  61. package/dist/diagnostics/json.js +24 -0
  62. package/dist/diagnostics/json.js.map +1 -0
  63. package/dist/diagnostics/model.d.ts +44 -0
  64. package/dist/diagnostics/model.d.ts.map +1 -0
  65. package/dist/diagnostics/model.js +2 -0
  66. package/dist/diagnostics/model.js.map +1 -0
  67. package/dist/diagnostics/text.d.ts +6 -0
  68. package/dist/diagnostics/text.d.ts.map +1 -0
  69. package/dist/diagnostics/text.js +43 -0
  70. package/dist/diagnostics/text.js.map +1 -0
  71. package/dist/generated/templates.d.ts +4 -0
  72. package/dist/generated/templates.d.ts.map +1 -0
  73. package/dist/generated/templates.js +9 -0
  74. package/dist/generated/templates.js.map +1 -0
  75. package/dist/generated/version.d.ts +11 -0
  76. package/dist/generated/version.d.ts.map +1 -0
  77. package/dist/generated/version.js +8 -0
  78. package/dist/generated/version.js.map +1 -0
  79. package/dist/i18n/locale.d.ts +56 -0
  80. package/dist/i18n/locale.d.ts.map +1 -0
  81. package/dist/i18n/locale.js +107 -0
  82. package/dist/i18n/locale.js.map +1 -0
  83. package/dist/index.d.ts +3 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +60 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/io/config.d.ts +2 -0
  88. package/dist/io/config.d.ts.map +1 -0
  89. package/dist/io/config.js +5 -0
  90. package/dist/io/config.js.map +1 -0
  91. package/dist/io/exit-codes.d.ts +12 -0
  92. package/dist/io/exit-codes.d.ts.map +1 -0
  93. package/dist/io/exit-codes.js +15 -0
  94. package/dist/io/exit-codes.js.map +1 -0
  95. package/dist/io/read.d.ts +13 -0
  96. package/dist/io/read.d.ts.map +1 -0
  97. package/dist/io/read.js +53 -0
  98. package/dist/io/read.js.map +1 -0
  99. package/dist/io/write.d.ts +32 -0
  100. package/dist/io/write.d.ts.map +1 -0
  101. package/dist/io/write.js +61 -0
  102. package/dist/io/write.js.map +1 -0
  103. package/dist/version.d.ts +13 -0
  104. package/dist/version.d.ts.map +1 -0
  105. package/dist/version.js +20 -0
  106. package/dist/version.js.map +1 -0
  107. package/man/fr/nowline.1 +424 -0
  108. package/man/fr/nowline.5 +1864 -0
  109. package/man/nowline.1 +517 -0
  110. package/man/nowline.5 +1784 -0
  111. package/package.json +66 -0
  112. package/scripts/bundle-templates.mjs +105 -0
  113. package/scripts/compile.mjs +131 -0
  114. package/src/cli/args.ts +252 -0
  115. package/src/cli/formats.ts +207 -0
  116. package/src/cli/help.ts +92 -0
  117. package/src/cli/output-path.ts +98 -0
  118. package/src/commands/init.ts +99 -0
  119. package/src/commands/render.ts +566 -0
  120. package/src/commands/serve.ts +322 -0
  121. package/src/convert/parse-json.ts +57 -0
  122. package/src/convert/printer.ts +376 -0
  123. package/src/convert/schema.ts +105 -0
  124. package/src/core/parse.ts +93 -0
  125. package/src/diagnostics/adapt.ts +148 -0
  126. package/src/diagnostics/format.ts +70 -0
  127. package/src/diagnostics/index.ts +4 -0
  128. package/src/diagnostics/json.ts +30 -0
  129. package/src/diagnostics/model.ts +48 -0
  130. package/src/diagnostics/text.ts +62 -0
  131. package/src/generated/templates.ts +12 -0
  132. package/src/generated/version.ts +18 -0
  133. package/src/i18n/locale.ts +133 -0
  134. package/src/index.ts +60 -0
  135. package/src/io/config.ts +11 -0
  136. package/src/io/exit-codes.ts +18 -0
  137. package/src/io/read.ts +70 -0
  138. package/src/io/write.ts +94 -0
  139. package/src/version.ts +21 -0
@@ -0,0 +1,148 @@
1
+ import type { CliDiagnostic, DiagnosticSeverity, LocalizedMessageData } from './model.js';
2
+
3
+ // Minimal LSP-style diagnostic shape. Langium re-exports vscode-languageserver-types'
4
+ // Diagnostic internally; we keep a narrow local type to avoid a direct coupling and
5
+ // to survive cross-version type relocations.
6
+ export interface LangiumLikeDiagnostic {
7
+ message: string;
8
+ severity?: number;
9
+ code?: string | number;
10
+ range?: {
11
+ start: { line: number; character: number };
12
+ end: { line: number; character: number };
13
+ };
14
+ /**
15
+ * Validator's stash for re-formattable messages. The shape is
16
+ * `{ code: MessageCode, args: MessageArgs<K> }`; we keep it
17
+ * `unknown` here to avoid coupling the CLI's diagnostic adapter
18
+ * to `@nowline/core`'s internal i18n types.
19
+ */
20
+ data?: unknown;
21
+ }
22
+
23
+ // Minimal shapes for chevrotain parser/lexer errors to avoid pulling chevrotain types.
24
+ interface ChevrotainParserError {
25
+ message: string;
26
+ token?: {
27
+ startLine?: number;
28
+ startColumn?: number;
29
+ startOffset?: number;
30
+ endLine?: number;
31
+ endColumn?: number;
32
+ endOffset?: number;
33
+ };
34
+ }
35
+
36
+ interface ChevrotainLexerError {
37
+ message: string;
38
+ line?: number;
39
+ column?: number;
40
+ offset?: number;
41
+ length?: number;
42
+ }
43
+
44
+ export function adaptLangiumDiagnostic(diag: LangiumLikeDiagnostic, file: string): CliDiagnostic {
45
+ const severity = mapSeverity(diag.severity);
46
+ const line = (diag.range?.start.line ?? 0) + 1;
47
+ const column = (diag.range?.start.character ?? 0) + 1;
48
+ const data = extractMessageData(diag.data);
49
+ return {
50
+ file,
51
+ line,
52
+ column,
53
+ severity,
54
+ // Localized validator data carries the stable message code; prefer it
55
+ // over the heuristic message-substring inference below.
56
+ code: data?.code ?? diagnosticCode(diag),
57
+ message: diag.message,
58
+ span: diag.range
59
+ ? {
60
+ start: { line, column },
61
+ end: {
62
+ line: (diag.range.end.line ?? 0) + 1,
63
+ column: (diag.range.end.character ?? 0) + 1,
64
+ },
65
+ }
66
+ : undefined,
67
+ suggestion: extractSuggestion(diag.message),
68
+ data,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Validate the shape of `diag.data`. Validator-emitted diagnostics
74
+ * stash `{ code: MessageCode, args: MessageArgs<K> }` (where `args` is
75
+ * the spread tuple `[]` or `[{...}]`). Anything else (vscode code
76
+ * actions, third-party data, etc.) is ignored.
77
+ */
78
+ function extractMessageData(data: unknown): LocalizedMessageData | undefined {
79
+ if (!data || typeof data !== 'object') return undefined;
80
+ const obj = data as { code?: unknown; args?: unknown };
81
+ if (typeof obj.code !== 'string') return undefined;
82
+ if (!Array.isArray(obj.args)) return undefined;
83
+ return { code: obj.code, args: obj.args };
84
+ }
85
+
86
+ export function adaptParserError(err: ChevrotainParserError, file: string): CliDiagnostic {
87
+ const line = err.token?.startLine ?? 1;
88
+ const column = err.token?.startColumn ?? 1;
89
+ const endLine = err.token?.endLine ?? line;
90
+ const endColumn = (err.token?.endColumn ?? column) + 1;
91
+ return {
92
+ file,
93
+ line,
94
+ column,
95
+ severity: 'error',
96
+ code: 'parse-error',
97
+ message: err.message,
98
+ span: { start: { line, column }, end: { line: endLine, column: endColumn } },
99
+ };
100
+ }
101
+
102
+ export function adaptLexerError(err: ChevrotainLexerError, file: string): CliDiagnostic {
103
+ const line = err.line ?? 1;
104
+ const column = err.column ?? 1;
105
+ const length = err.length ?? 1;
106
+ return {
107
+ file,
108
+ line,
109
+ column,
110
+ severity: 'error',
111
+ code: 'lex-error',
112
+ message: err.message,
113
+ span: {
114
+ start: { line, column },
115
+ end: { line, column: column + Math.max(length, 1) },
116
+ },
117
+ };
118
+ }
119
+
120
+ function mapSeverity(severity: number | undefined): DiagnosticSeverity {
121
+ return severity === 2 ? 'warning' : 'error';
122
+ }
123
+
124
+ function diagnosticCode(diag: LangiumLikeDiagnostic): string {
125
+ if (typeof diag.code === 'string' && diag.code !== '') return diag.code;
126
+ if (typeof diag.code === 'number') return String(diag.code);
127
+ return inferCodeFromMessage(diag.message);
128
+ }
129
+
130
+ function inferCodeFromMessage(message: string): string {
131
+ const lower = message.toLowerCase();
132
+ if (lower.includes('duplicate identifier')) return 'duplicate-identifier';
133
+ if (lower.includes('unknown reference') || lower.includes('did you mean'))
134
+ return 'unknown-reference';
135
+ if (lower.includes('circular')) return 'circular-dependency';
136
+ if (lower.includes('requires') && lower.includes('date:')) return 'missing-date';
137
+ if (lower.includes('duration')) return 'duration';
138
+ if (lower.includes('include')) return 'include';
139
+ if (lower.includes('indent')) return 'indentation';
140
+ return 'validation';
141
+ }
142
+
143
+ const DID_YOU_MEAN_RE = /did you mean ['"]?([^'"?]+)['"]?\??/i;
144
+
145
+ function extractSuggestion(message: string): string | undefined {
146
+ const match = message.match(DID_YOU_MEAN_RE);
147
+ return match ? match[1].trim() : undefined;
148
+ }
@@ -0,0 +1,70 @@
1
+ import { type MessageCode, tr } from '@nowline/core';
2
+ import { renderJson } from './json.js';
3
+ import type { CliDiagnostic, DiagnosticSource } from './model.js';
4
+ import { renderText } from './text.js';
5
+
6
+ export type DiagnosticFormat = 'text' | 'json';
7
+
8
+ export interface FormatDiagnosticsOptions {
9
+ color?: boolean;
10
+ /**
11
+ * Operator locale (BCP-47) used to re-format validator messages
12
+ * that carry `data: { code, args }`. Validator messages without
13
+ * `data` and parser/lexer errors pass through unchanged.
14
+ *
15
+ * Defaults to en-US: an unset operator locale produces the same
16
+ * canonical English text the validator stashed at parse time.
17
+ * See `specs/localization.md` for the two-chain model.
18
+ */
19
+ operatorLocale?: string;
20
+ }
21
+
22
+ export function isDiagnosticFormat(value: unknown): value is DiagnosticFormat {
23
+ return value === 'text' || value === 'json';
24
+ }
25
+
26
+ export function formatDiagnostics(
27
+ diagnostics: CliDiagnostic[],
28
+ format: DiagnosticFormat,
29
+ sources: Map<string, DiagnosticSource>,
30
+ options: FormatDiagnosticsOptions = {},
31
+ ): string {
32
+ const localized = relocalizeDiagnostics(diagnostics, options.operatorLocale);
33
+ if (format === 'json') return renderJson(localized);
34
+ return renderText(localized, sources, { color: options.color });
35
+ }
36
+
37
+ /**
38
+ * Re-format any diagnostic carrying `data.{code,args}` in the operator
39
+ * locale. Diagnostics without `data` (literal-English validator
40
+ * strings, parser/lexer errors, include-resolution errors) pass
41
+ * through verbatim.
42
+ *
43
+ * No-op when `operatorLocale` is undefined or `'en-US'`: the messages
44
+ * stashed by the validator are already en-US, so we avoid the work
45
+ * and the allocation in the common case.
46
+ */
47
+ function relocalizeDiagnostics(
48
+ diagnostics: CliDiagnostic[],
49
+ operatorLocale: string | undefined,
50
+ ): CliDiagnostic[] {
51
+ if (!operatorLocale || operatorLocale === 'en-US') return diagnostics;
52
+ return diagnostics.map((d) => {
53
+ if (!d.data) return d;
54
+ // The validator stashes args as the verbatim spread tuple it received,
55
+ // typed strongly per-code in `@nowline/core/i18n`. By the time it
56
+ // reaches us through `unknown` it has lost that tagged shape; we cast
57
+ // back here because the only producer is the validator and the
58
+ // `code` half of the pair pins the expected arity.
59
+ const args = d.data.args as Parameters<typeof tr<MessageCode>> extends [
60
+ string,
61
+ MessageCode,
62
+ ...infer R,
63
+ ]
64
+ ? R
65
+ : never;
66
+ const message = tr(operatorLocale, d.data.code as MessageCode, ...args);
67
+ if (message === d.message) return d;
68
+ return { ...d, message };
69
+ });
70
+ }
@@ -0,0 +1,4 @@
1
+ export * from './adapt.js';
2
+ export * from './format.js';
3
+ export { DIAGNOSTICS_SCHEMA_VERSION, type DiagnosticsDocument } from './json.js';
4
+ export * from './model.js';
@@ -0,0 +1,30 @@
1
+ import type { CliDiagnostic } from './model.js';
2
+
3
+ export const DIAGNOSTICS_SCHEMA_VERSION = '1';
4
+
5
+ export interface DiagnosticsDocument {
6
+ $nowlineDiagnostics: string;
7
+ diagnostics: CliDiagnostic[];
8
+ }
9
+
10
+ export function renderJson(diagnostics: CliDiagnostic[]): string {
11
+ const doc: DiagnosticsDocument = {
12
+ $nowlineDiagnostics: DIAGNOSTICS_SCHEMA_VERSION,
13
+ diagnostics: diagnostics.map(normalize),
14
+ };
15
+ return JSON.stringify(doc, null, 2);
16
+ }
17
+
18
+ function normalize(d: CliDiagnostic): CliDiagnostic {
19
+ const out: CliDiagnostic = {
20
+ file: d.file,
21
+ line: d.line,
22
+ column: d.column,
23
+ severity: d.severity,
24
+ code: d.code,
25
+ message: d.message,
26
+ };
27
+ if (d.suggestion) out.suggestion = d.suggestion;
28
+ if (d.span) out.span = d.span;
29
+ return out;
30
+ }
@@ -0,0 +1,48 @@
1
+ export type DiagnosticSeverity = 'error' | 'warning';
2
+
3
+ export interface SourcePosition {
4
+ line: number;
5
+ column: number;
6
+ offset?: number;
7
+ }
8
+
9
+ export interface SourceSpan {
10
+ start: SourcePosition;
11
+ end: SourcePosition;
12
+ }
13
+
14
+ /**
15
+ * Carries the validator's stable message code plus the named-argument
16
+ * record it was formatted from, so the CLI can re-format the human
17
+ * message in the operator's locale at print time. See
18
+ * `specs/localization.md` for the two-chain precedence model.
19
+ */
20
+ export interface LocalizedMessageData {
21
+ code: string;
22
+ args: ReadonlyArray<unknown>;
23
+ }
24
+
25
+ export interface CliDiagnostic {
26
+ file: string;
27
+ line: number;
28
+ column: number;
29
+ severity: DiagnosticSeverity;
30
+ code: string;
31
+ message: string;
32
+ suggestion?: string;
33
+ span?: SourceSpan;
34
+ /**
35
+ * Stable code + args as captured by the validator. Present on
36
+ * messages that flow through the `tr()` registry; absent for
37
+ * literal-English validator strings, parser/lexer errors, and
38
+ * include-resolution diagnostics. The CLI re-formats `message`
39
+ * from this when an operator locale is supplied to
40
+ * `formatDiagnostics`.
41
+ */
42
+ data?: LocalizedMessageData;
43
+ }
44
+
45
+ export interface DiagnosticSource {
46
+ file: string;
47
+ contents: string;
48
+ }
@@ -0,0 +1,62 @@
1
+ import { codeFrameColumns } from '@babel/code-frame';
2
+ import chalk from 'chalk';
3
+ import type { CliDiagnostic, DiagnosticSource } from './model.js';
4
+
5
+ export interface RenderTextOptions {
6
+ color?: boolean;
7
+ }
8
+
9
+ export function renderText(
10
+ diagnostics: CliDiagnostic[],
11
+ sources: Map<string, DiagnosticSource>,
12
+ options: RenderTextOptions = {},
13
+ ): string {
14
+ const useColor = options.color ?? chalk.level > 0;
15
+ const parts = diagnostics.map((d) => renderOne(d, sources.get(d.file), useColor));
16
+ return parts.join('\n\n');
17
+ }
18
+
19
+ function renderOne(
20
+ diag: CliDiagnostic,
21
+ source: DiagnosticSource | undefined,
22
+ useColor: boolean,
23
+ ): string {
24
+ const header = renderHeader(diag, useColor);
25
+ if (!source) return header;
26
+ const frame = codeFrameColumns(
27
+ source.contents,
28
+ {
29
+ start: { line: diag.line, column: diag.column },
30
+ end: diag.span ? { line: diag.span.end.line, column: diag.span.end.column } : undefined,
31
+ },
32
+ {
33
+ highlightCode: useColor,
34
+ linesAbove: 2,
35
+ linesBelow: 1,
36
+ forceColor: useColor,
37
+ },
38
+ );
39
+ return `${header}\n${frame}`;
40
+ }
41
+
42
+ function renderHeader(diag: CliDiagnostic, useColor: boolean): string {
43
+ const loc = `${diag.file}:${diag.line}:${diag.column}`;
44
+ const severityWord = diag.severity === 'error' ? 'error' : 'warning';
45
+ if (!useColor) {
46
+ const suffix = diag.suggestion ? ` — did you mean '${diag.suggestion}'?` : '';
47
+ return `${loc} ${severityWord}: ${stripSuggestion(diag.message, diag.suggestion)}${suffix}`;
48
+ }
49
+ const coloredLoc = chalk.cyan(loc);
50
+ const coloredSeverity =
51
+ diag.severity === 'error' ? chalk.red.bold(severityWord) : chalk.yellow.bold(severityWord);
52
+ const message = stripSuggestion(diag.message, diag.suggestion);
53
+ const suggestion = diag.suggestion
54
+ ? ` ${chalk.dim('—')} did you mean ${chalk.green(`'${diag.suggestion}'`)}?`
55
+ : '';
56
+ return `${coloredLoc} ${coloredSeverity}: ${message}${suggestion}`;
57
+ }
58
+
59
+ function stripSuggestion(message: string, suggestion: string | undefined): string {
60
+ if (!suggestion) return message;
61
+ return message.replace(/ —\s*did you mean ['"]?[^'"?]+['"]?\??/i, '').trimEnd();
62
+ }
@@ -0,0 +1,12 @@
1
+ // Auto-generated by scripts/bundle-templates.mjs. Do not edit by hand.
2
+ // Run `pnpm run bundle-templates` to regenerate.
3
+
4
+ export type TemplateName = 'minimal' | 'teams' | 'product';
5
+
6
+ export const TEMPLATE_NAMES: readonly TemplateName[] = ['minimal', 'teams', 'product'];
7
+
8
+ export const TEMPLATES: Record<TemplateName, string> = {
9
+ "minimal": "nowline v1\n\nroadmap minimal \"Starter\" start:2026-01-05 scale:2w author:\"Jane Doe\"\n\nswimlane engineering \"Engineering\"\n parallel concurrent-block style:concurrent\n group research-group \"Research\"\n item research \"Research\" duration:3w status:done\n item design \"Design\" duration:2w status:in-progress remaining:5d\n item build \"Build\" duration:3w status:planned\n group release-group \"Release\"\n item release \"Release\" duration:1w status:planned\n item deploy \"Deploy\" duration:1w status:planned\n",
10
+ "teams": "nowline v1\n\nroadmap teams-2026 \"Teams Roadmap 2026\" start:2026-01-05\n\nperson sam \"Sam Chen\" link:https://github.com/samchen\nperson jen \"Jennifer Wu\"\nperson alex \"Alex Rivera\"\n\nteam engineering \"Engineering\"\n team platform-team \"Platform Team\"\n person sam\n person jen\n team mobile-team \"Mobile Team\"\n person alex\n\nanchor kickoff date:2026-01-06\nanchor mid-year date:2026-07-01\nanchor eoy \"End of Year\" date:2026-12-15\n\nswimlane platform owner:platform-team\n item auth \"Auth refactor\" duration:1m after:kickoff owner:sam status:done\n item api-v2 \"API v2\" duration:2w owner:jen status:in-progress remaining:40%\n\nswimlane mobile owner:mobile-team\n item offline \"Offline mode\" duration:1m after:kickoff owner:alex status:at-risk remaining:60%\n item push \"Push notifications\" duration:2w owner:alex\n\nmilestone beta \"Beta\" after:[auth, api-v2]\nmilestone ga \"GA\" date:2026-12-01 after:[offline, push]\n\nfootnote eng-capacity \"Mobile team capacity risk\" on:mobile-team\n description \"Mobile team is understaffed through Q2.\"\n\nfootnote vendor-dep \"Vendor dependency\" on:[auth, api-v2]\n description \"Blocked until vendor contract is signed.\"\n",
11
+ "product": "nowline v1\n\nconfig\n\nscale\n name: weeks\n label-every: 2\n label: \"W{n}\"\n\ncalendar\n days-per-week: 5\n days-per-month: 22\n days-per-quarter: 65\n days-per-year: 260\n\nstyle enterprise-style \"Enterprise readiness\"\n bg: blue\n fg: navy\n text: white\n border: solid\n\nstyle risky\n border: dashed\n fg: orange\n\nstyle subtle\n bg: gray\n\ndefault item shadow:subtle\ndefault swimlane padding:sm spacing:none\ndefault roadmap padding:md header-height:md font:sans\ndefault parallel bracket:none\n\nroadmap platform-2026 \"Platform 2026\" author:\"Acme Engineering\" start:2026-01-05 calendar:custom\n\nperson sam \"Sam Chen\"\nperson jen \"Jennifer Wu\"\n\nsize xs effort:1d\nsize sm effort:3d\nsize md effort:1w\nsize lg effort:2w\nsize xl effort:1m\n\nstatus awaiting-review\nstatus in-review\n\nlabel enterprise \"Enterprise readiness\" style:enterprise-style\nlabel security \"Security hardening\" style:enterprise-style\nlabel low-confidence style:risky\n\nanchor kickoff date:2026-01-06\nanchor code-freeze \"Code Freeze\" date:2026-05-01\nanchor ga-date \"GA Date\" date:2026-06-01\n\nswimlane platform\n item auth-refactor \"Auth refactor\" size:lg after:kickoff status:done owner:sam labels:enterprise link:https://github.com/acme/platform/issues/123\n parallel after:auth-refactor\n group audit-track \"Audit Track\" labels:security\n item audit-log \"Audit log v2\" size:xl before:code-freeze remaining:30% labels:[enterprise, security]\n description \"Comprehensive audit trail for all admin actions\"\n item audit-ui \"Audit UI\" size:md\n item sso \"SSO plugins\" size:md labels:[enterprise, low-confidence]\n item platform-qa \"Platform QA\" size:sm\n\nswimlane mobile\n item offline \"Offline mode\" size:lg after:kickoff owner:jen status:at-risk remaining:60% link:https://github.com/acme/mobile/pull/87\n item push-v2 \"Push notifications v2\" size:md\n\nmilestone beta \"Beta\" after:auth-refactor\nmilestone ga-launch \"GA launch\" date:2026-06-01 after:[auth-refactor, audit-log]\n\nfootnote vendor-footnote \"Vendor dependency\" on:audit-log\n description \"Blocked until vendor contract is signed. Expected March resolution.\"\n"
12
+ };
@@ -0,0 +1,18 @@
1
+ // Auto-generated by scripts/bundle-templates.mjs. Do not edit by hand.
2
+
3
+ export const CLI_VERSION = "0.2.0";
4
+
5
+ export interface CliBuild {
6
+ /** Short git SHA at build time, or empty when not in a git checkout. */
7
+ readonly sha: string;
8
+ /** True when HEAD is the tag matching `v${CLI_VERSION}`. */
9
+ readonly isRelease: boolean;
10
+ /** True when the working tree had uncommitted changes at build time. */
11
+ readonly isDirty: boolean;
12
+ }
13
+
14
+ export const CLI_BUILD: CliBuild = {
15
+ "sha": "c51e2a4",
16
+ "isRelease": true,
17
+ "isDirty": false
18
+ };
@@ -0,0 +1,133 @@
1
+ import type { NowlineFile } from '@nowline/core';
2
+
3
+ // Resolve the operator-locale override from CLI flag, environment, and
4
+ // `.nowlinerc`. See `specs/localization.md` for the two-chain model.
5
+ //
6
+ // Operator locale governs CLI message output: validator diagnostics on
7
+ // stderr, `--help`, format errors, verbose logs. It is independent from
8
+ // the file's `locale:` directive, which governs the rendered artifact.
9
+ //
10
+ // Returns the resolved tag plus the source it came from, so verbose
11
+ // logging can describe the chain without re-walking it.
12
+
13
+ const ENV_VARS = ['LC_ALL', 'LC_MESSAGES', 'LANG'] as const;
14
+ type EnvVar = (typeof ENV_VARS)[number];
15
+
16
+ export type LocaleSource = 'flag' | 'env' | 'rc';
17
+
18
+ export interface ResolvedLocale {
19
+ /** BCP-47 tag, or undefined when no source produced one. */
20
+ tag: string | undefined;
21
+ /** Which input chain produced `tag`. Undefined iff `tag` is undefined. */
22
+ source: LocaleSource | undefined;
23
+ /** When `source === 'env'`, the specific variable that contributed. */
24
+ envVar?: EnvVar;
25
+ }
26
+
27
+ export interface ResolveLocaleInputs {
28
+ /** Value of the `--locale` flag, or undefined. */
29
+ flag: string | undefined;
30
+ /** Process environment, parameterized for testability. */
31
+ env: NodeJS.ProcessEnv;
32
+ /** Optional `.nowlinerc` `locale` key. Slots below env vars. */
33
+ rc?: string | undefined;
34
+ }
35
+
36
+ /**
37
+ * Resolve the operator's locale override.
38
+ *
39
+ * Precedence (highest wins): `--locale` flag > env vars > `.nowlinerc`.
40
+ * POSIX `C` / `POSIX` env values mean "no localization" and are skipped.
41
+ *
42
+ * Returns `{ tag: undefined, source: undefined }` when nothing is set —
43
+ * `operatorLocale` then defaults to `en-US`, while the layout's content
44
+ * chain falls back to whatever the file's `locale:` directive declares.
45
+ */
46
+ export function resolveLocaleOverride({ flag, env, rc }: ResolveLocaleInputs): ResolvedLocale {
47
+ if (flag) return { tag: flag, source: 'flag' };
48
+ for (const name of ENV_VARS) {
49
+ const raw = env[name];
50
+ if (!raw) continue;
51
+ const stripped = stripPosixSuffix(raw);
52
+ if (!stripped || stripped === 'C' || stripped === 'POSIX') {
53
+ // POSIX `C` / `POSIX` locale means "no localization." Skip and
54
+ // let the next env var or the rc file take over.
55
+ continue;
56
+ }
57
+ return { tag: normalizePosixToBcp47(stripped), source: 'env', envVar: name };
58
+ }
59
+ if (rc) return { tag: rc, source: 'rc' };
60
+ return { tag: undefined, source: undefined };
61
+ }
62
+
63
+ /**
64
+ * Resolve the operator-facing locale used to format CLI message output.
65
+ * `en-US` when the operator has no signal in the environment.
66
+ */
67
+ export function operatorLocale(resolved: ResolvedLocale): string {
68
+ return resolved.tag ?? 'en-US';
69
+ }
70
+
71
+ /**
72
+ * Describe which locale governs the rendered artifact and where the
73
+ * value came from. Used by the `--verbose` log line so an operator can
74
+ * see at a glance whether the file or the operator chain is winning.
75
+ *
76
+ * Precedence mirrors the layout's `resolveLocale`:
77
+ * file directive > CLI flag > env > `.nowlinerc` > en-US (default).
78
+ */
79
+ export function describeContentLocaleSource(
80
+ directive: string | undefined,
81
+ resolved: ResolvedLocale,
82
+ ): { tag: string; source: string } {
83
+ if (directive) return { tag: directive, source: 'from file directive' };
84
+ if (resolved.tag === undefined) return { tag: 'en-US', source: 'default' };
85
+ switch (resolved.source) {
86
+ case 'flag':
87
+ return { tag: resolved.tag, source: 'from --locale' };
88
+ case 'env':
89
+ return { tag: resolved.tag, source: `from ${resolved.envVar} env var` };
90
+ case 'rc':
91
+ return { tag: resolved.tag, source: 'from .nowlinerc' };
92
+ default:
93
+ return { tag: resolved.tag, source: 'from operator chain' };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Read the optional `locale:` property from a parsed file's
99
+ * `nowline v1` directive. Returns undefined when the directive is
100
+ * absent, malformed, or omits the property.
101
+ */
102
+ 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;
109
+ }
110
+
111
+ /**
112
+ * POSIX `LANG` values look like `en_US.UTF-8` or `fr_CA@euro`. Strip
113
+ * everything from `.` or `@` onward — Nowline only cares about the
114
+ * language and region.
115
+ */
116
+ function stripPosixSuffix(value: string): string {
117
+ const dot = value.indexOf('.');
118
+ const at = value.indexOf('@');
119
+ let end = value.length;
120
+ if (dot !== -1 && dot < end) end = dot;
121
+ if (at !== -1 && at < end) end = at;
122
+ return value.slice(0, end).trim();
123
+ }
124
+
125
+ /**
126
+ * Normalize `fr_CA` to `fr-CA`. POSIX uses `_`; BCP-47 uses `-`.
127
+ * Casing follows BCP-47 conventions: language lowercase, region
128
+ * uppercase 2-letter, but the layout's BCP-47 walker normalizes again
129
+ * defensively, so this is best-effort.
130
+ */
131
+ function normalizePosixToBcp47(value: string): string {
132
+ return value.replace('_', '-');
133
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgv } from './cli/args.js';
3
+ import { renderHelp, renderVersion } from './cli/help.js';
4
+ import { initHandler } from './commands/init.js';
5
+ import { renderHandler } from './commands/render.js';
6
+ import { serveHandler } from './commands/serve.js';
7
+ import { CliError, ExitCode } from './io/exit-codes.js';
8
+
9
+ async function run(): Promise<number> {
10
+ const argv = process.argv.slice(2);
11
+
12
+ let parsed: ReturnType<typeof parseArgv>;
13
+ try {
14
+ parsed = parseArgv(argv);
15
+ } catch (err) {
16
+ return handleError(err);
17
+ }
18
+
19
+ if (parsed.mode === 'help') {
20
+ process.stdout.write(renderHelp());
21
+ return ExitCode.Success;
22
+ }
23
+ if (parsed.mode === 'version') {
24
+ process.stdout.write(renderVersion());
25
+ return ExitCode.Success;
26
+ }
27
+
28
+ try {
29
+ if (parsed.mode === 'init') {
30
+ await initHandler({ args: parsed });
31
+ } else if (parsed.mode === 'serve') {
32
+ await serveHandler({ args: parsed });
33
+ } else {
34
+ await renderHandler({ args: parsed });
35
+ }
36
+ return ExitCode.Success;
37
+ } catch (err) {
38
+ return handleError(err);
39
+ }
40
+ }
41
+
42
+ function handleError(err: unknown): number {
43
+ if (err instanceof CliError) {
44
+ if (err.message) process.stderr.write(`${err.message}\n`);
45
+ return err.exitCode;
46
+ }
47
+ const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
48
+ process.stderr.write(`${message}\n`);
49
+ return ExitCode.ValidationError;
50
+ }
51
+
52
+ run()
53
+ .then((code) => {
54
+ process.exit(code);
55
+ })
56
+ .catch((err: unknown) => {
57
+ const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
58
+ process.stderr.write(`${message}\n`);
59
+ process.exit(ExitCode.ValidationError);
60
+ });
@@ -0,0 +1,11 @@
1
+ // Thin re-export of the shared `.nowlinerc` loader. The implementation
2
+ // lives in `@nowline/config` so the VS Code/Cursor extension can read the
3
+ // same files without depending on the CLI's heavier export-format graph.
4
+ export {
5
+ type LoadConfigOptions,
6
+ type LoadConfigResult,
7
+ loadConfig,
8
+ mergeConfig,
9
+ type NowlineRc,
10
+ parseConfig,
11
+ } from '@nowline/config';
@@ -0,0 +1,18 @@
1
+ export const ExitCode = {
2
+ Success: 0,
3
+ ValidationError: 1,
4
+ InputError: 2,
5
+ OutputError: 3,
6
+ } as const;
7
+
8
+ export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
9
+
10
+ export class CliError extends Error {
11
+ constructor(
12
+ public readonly exitCode: ExitCode,
13
+ message: string,
14
+ ) {
15
+ super(message);
16
+ this.name = 'CliError';
17
+ }
18
+ }