@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,207 @@
1
+ import * as path from 'node:path';
2
+
3
+ export type OutputFormat =
4
+ | 'svg'
5
+ | 'png'
6
+ | 'pdf'
7
+ | 'html'
8
+ | 'mermaid'
9
+ | 'xlsx'
10
+ | 'msproj'
11
+ | 'json'
12
+ | 'nowline';
13
+
14
+ export type InputFormat = 'nowline' | 'json';
15
+
16
+ const TEXT_FORMATS: ReadonlySet<OutputFormat> = new Set([
17
+ 'svg',
18
+ 'html',
19
+ 'mermaid',
20
+ 'msproj',
21
+ 'json',
22
+ 'nowline',
23
+ ]);
24
+
25
+ const BINARY_FORMATS: ReadonlySet<OutputFormat> = new Set(['png', 'pdf', 'xlsx']);
26
+
27
+ export const ALL_OUTPUT_FORMATS: readonly OutputFormat[] = [
28
+ 'svg',
29
+ 'png',
30
+ 'pdf',
31
+ 'html',
32
+ 'mermaid',
33
+ 'xlsx',
34
+ 'msproj',
35
+ 'json',
36
+ 'nowline',
37
+ ];
38
+
39
+ export function isOutputFormat(value: string): value is OutputFormat {
40
+ return (ALL_OUTPUT_FORMATS as readonly string[]).includes(value);
41
+ }
42
+
43
+ /**
44
+ * User-facing format aliases. The canonical token is `msproj` (matches the
45
+ * package name `@nowline/export-msproj`); `ms-project` and `mspx` are
46
+ * accepted shorthands documented in `specs/handoffs/m2c.md` § 8.
47
+ */
48
+ const FORMAT_ALIASES: Readonly<Record<string, OutputFormat>> = {
49
+ 'ms-project': 'msproj',
50
+ msproject: 'msproj',
51
+ mspx: 'msproj',
52
+ md: 'mermaid',
53
+ markdown: 'mermaid',
54
+ excel: 'xlsx',
55
+ 'ms-excel': 'xlsx',
56
+ };
57
+
58
+ export function normalizeFormatAlias(raw: string): string {
59
+ const lower = raw.toLowerCase();
60
+ return FORMAT_ALIASES[lower] ?? lower;
61
+ }
62
+
63
+ export function isInputFormat(value: string): value is InputFormat {
64
+ return value === 'nowline' || value === 'json';
65
+ }
66
+
67
+ export function isBinaryFormat(format: OutputFormat): boolean {
68
+ return BINARY_FORMATS.has(format);
69
+ }
70
+
71
+ export function isTextFormat(format: OutputFormat): boolean {
72
+ return TEXT_FORMATS.has(format);
73
+ }
74
+
75
+ const EXTENSION_MAP: ReadonlyMap<string, OutputFormat> = new Map<string, OutputFormat>([
76
+ ['.svg', 'svg'],
77
+ ['.png', 'png'],
78
+ ['.pdf', 'pdf'],
79
+ ['.html', 'html'],
80
+ ['.htm', 'html'],
81
+ ['.md', 'mermaid'],
82
+ ['.markdown', 'mermaid'],
83
+ ['.xlsx', 'xlsx'],
84
+ ['.json', 'json'],
85
+ ['.nowline', 'nowline'],
86
+ ]);
87
+
88
+ const CANONICAL_EXTENSION: Readonly<Record<OutputFormat, string>> = {
89
+ svg: '.svg',
90
+ png: '.png',
91
+ pdf: '.pdf',
92
+ html: '.html',
93
+ mermaid: '.md',
94
+ xlsx: '.xlsx',
95
+ msproj: '.xml',
96
+ json: '.json',
97
+ nowline: '.nowline',
98
+ };
99
+
100
+ /**
101
+ * Maps a recognized output extension to its canonical format.
102
+ *
103
+ * `.xml` is intentionally absent — it is ambiguous (MS Project XML vs generic
104
+ * XML) and requires `-f msproj` to disambiguate.
105
+ */
106
+ export function formatFromExtension(extension: string): OutputFormat | undefined {
107
+ return EXTENSION_MAP.get(extension.toLowerCase());
108
+ }
109
+
110
+ export function canonicalExtension(format: OutputFormat): string {
111
+ return CANONICAL_EXTENSION[format];
112
+ }
113
+
114
+ export interface FormatResolutionInputs {
115
+ /** `-f / --format` flag value (already lower-cased). */
116
+ flagFormat?: string;
117
+ /** `-o / --output` path; the literal string the user wrote. */
118
+ outputPath?: string;
119
+ /** `defaultFormat` from `.nowlinerc`. */
120
+ configFormat?: string;
121
+ /** True when output is `-` (stdout); skips extension inference. */
122
+ isStdout?: boolean;
123
+ }
124
+
125
+ export interface FormatResolution {
126
+ format: OutputFormat;
127
+ /** Where the format came from. Useful for `-v` diagnostics. */
128
+ source: 'flag' | 'output-extension' | 'config' | 'fallback';
129
+ }
130
+
131
+ /**
132
+ * Resolves the output format using the documented precedence chain:
133
+ *
134
+ * 1. `-f / --format` flag
135
+ * 2. `-o <path>` recognized extension (skipped for `-` / stdout)
136
+ * 3. `.nowlinerc` `defaultFormat`
137
+ * 4. Built-in fallback `svg`
138
+ *
139
+ * Returns the resolved format and the precedence step that produced it. May
140
+ * throw a `FormatResolutionError` for unsupported / ambiguous inputs (caller
141
+ * is expected to surface this as an exit-2 usage error).
142
+ */
143
+ export function resolveFormat(inputs: FormatResolutionInputs): FormatResolution {
144
+ if (inputs.flagFormat) {
145
+ const flag = normalizeFormatAlias(inputs.flagFormat);
146
+ if (!isOutputFormat(flag)) {
147
+ throw new FormatResolutionError(
148
+ `Unknown --format "${inputs.flagFormat}". Expected one of: ${ALL_OUTPUT_FORMATS.join(', ')}.`,
149
+ );
150
+ }
151
+ return { format: flag, source: 'flag' };
152
+ }
153
+
154
+ if (!inputs.isStdout && inputs.outputPath) {
155
+ const ext = path.extname(inputs.outputPath);
156
+ if (ext) {
157
+ if (ext.toLowerCase() === '.xml') {
158
+ throw new FormatResolutionError(
159
+ 'Cannot infer format from .xml extension; use -f msproj for MS Project XML output.',
160
+ );
161
+ }
162
+ const inferred = formatFromExtension(ext);
163
+ if (inferred) {
164
+ return { format: inferred, source: 'output-extension' };
165
+ }
166
+ }
167
+ }
168
+
169
+ if (inputs.configFormat) {
170
+ const cfg = normalizeFormatAlias(inputs.configFormat);
171
+ if (!isOutputFormat(cfg)) {
172
+ throw new FormatResolutionError(
173
+ `Invalid .nowlinerc defaultFormat "${inputs.configFormat}". Expected one of: ${ALL_OUTPUT_FORMATS.join(', ')}.`,
174
+ );
175
+ }
176
+ return { format: cfg, source: 'config' };
177
+ }
178
+
179
+ return { format: 'svg', source: 'fallback' };
180
+ }
181
+
182
+ /**
183
+ * Applies the documented output-extension auto-add rule:
184
+ *
185
+ * - If `<path>` ends in no extension, append the canonical extension for the
186
+ * resolved format (`-o report -f pdf` → `report.pdf`).
187
+ * - If `<path>` ends in the canonical extension for the resolved format, leave
188
+ * it alone.
189
+ * - If `<path>` ends in any *other* extension, leave it alone (no auto-rename).
190
+ * The user wrote literal bytes; we trust them.
191
+ *
192
+ * Stdout (`-`) is never rewritten; callers should bypass this helper for stdout.
193
+ */
194
+ export function autoAddExtension(outputPath: string, format: OutputFormat): string {
195
+ const ext = path.extname(outputPath);
196
+ if (!ext) {
197
+ return outputPath + canonicalExtension(format);
198
+ }
199
+ return outputPath;
200
+ }
201
+
202
+ export class FormatResolutionError extends Error {
203
+ constructor(message: string) {
204
+ super(message);
205
+ this.name = 'FormatResolutionError';
206
+ }
207
+ }
@@ -0,0 +1,92 @@
1
+ import { fullVersionString } from '../version.js';
2
+
3
+ // Use the dev-aware string here too: a contributor running `pnpm dev`
4
+ // against a feature branch should see e.g. `0.1.0+abc1234.dirty` in
5
+ // `--help` so it's obvious which build they're looking at.
6
+ const HELP_TEXT = `nowline ${fullVersionString()} — render, validate, and serve .nowline roadmaps.
7
+
8
+ USAGE
9
+ nowline <input> [options]
10
+ nowline --serve <input> [options]
11
+ nowline --init [<name>]
12
+ nowline --version
13
+ nowline --help
14
+
15
+ INPUT
16
+ <input> Path to .nowline or .json, or '-' for stdin.
17
+ Required for render, --serve, and --dry-run.
18
+
19
+ I/O OPTIONS
20
+ -f, --format <fmt> Output format: svg, png, pdf, html, mermaid, xlsx,
21
+ msproj, json, nowline. Default: inferred from -o
22
+ extension, else .nowlinerc 'defaultFormat', else svg.
23
+ -o, --output <path> Output file path. Use '-' for stdout (Unix dash).
24
+ Default: <cwd>/<input-base>.<format>.
25
+ Existing files are silently overwritten.
26
+ --input-format <f> Force input format: nowline | json. Default: by
27
+ extension; stdin defaults to nowline.
28
+
29
+ MODE FLAGS (mutually exclusive)
30
+ --serve Live HTTP preview server. -o opt-in writes the
31
+ rendered output to disk on each rebuild.
32
+ --init [<name>] Scaffold a starter .nowline file in cwd. Positional
33
+ becomes project name; .nowline appended if missing.
34
+ -n, --dry-run Run the full pipeline (parse + validate + layout +
35
+ format) but skip the write step. Subsumes the old
36
+ 'validate' verb. Exit 0 on success, 1 on errors.
37
+
38
+ RENDER OPTIONS
39
+ -t, --theme <name> light | dark
40
+ --now <YYYY-MM-DD> Date for the now-line. Default: today.
41
+ Use --now - to suppress the now-line.
42
+ --no-links Omit link icons from rendered items.
43
+ -s, --scale <n> Raster scale factor (PNG only; default 1).
44
+ --strict Promote asset / sanitizer warnings to errors.
45
+ -w, --width <px> Canvas width in pixels (default: 1280).
46
+ --asset-root <dir> Root for logo / image assets (default: input dir).
47
+ --locale <bcp47> BCP-47 locale (e.g. fr, fr-CA) for CLI message
48
+ output (validator diagnostics, --help, errors).
49
+ Used as a fallback for the rendered drawing only
50
+ when the file does not declare its own
51
+ 'nowline v1 locale:' directive — the file always
52
+ wins for content. Falls back to LC_ALL /
53
+ LC_MESSAGES / LANG, then en-US.
54
+
55
+ SERVE OPTIONS
56
+ -p, --port <n> Port (default: 4318).
57
+ --host <host> Bind address (default: 127.0.0.1).
58
+ --open Open the browser on start.
59
+
60
+ LOGGING (mutually exclusive)
61
+ -v, --verbose Print extra diagnostics to stderr.
62
+ -q, --quiet Suppress non-error stderr.
63
+
64
+ STANDARD
65
+ -h, --help Print this help and exit 0.
66
+ -V, --version Print version and exit 0.
67
+
68
+ EXAMPLES
69
+ nowline roadmap.nowline # writes ./roadmap.svg
70
+ nowline roadmap.nowline -f pdf # writes ./roadmap.pdf
71
+ nowline roadmap.nowline -o roadmap.pdf # format inferred from extension
72
+ nowline roadmap.nowline -o - # SVG to stdout
73
+ cat foo.nowline | nowline - # stdin → ./roadmap.svg
74
+ nowline roadmap.nowline -f json -o - # JSON AST to stdout
75
+ nowline roadmap.nowline --dry-run # validate-only
76
+ nowline roadmap.nowline --serve -p 8080 # live preview
77
+ nowline --init my-project # scaffold ./my-project.nowline
78
+
79
+ EXIT CODES
80
+ 0 Success
81
+ 1 Validation error
82
+ 2 Usage error (missing input, bad flags, format unavailable)
83
+ 3 Output error (cannot write to destination)
84
+ `;
85
+
86
+ export function renderHelp(): string {
87
+ return HELP_TEXT;
88
+ }
89
+
90
+ export function renderVersion(): string {
91
+ return `${fullVersionString()}\n`;
92
+ }
@@ -0,0 +1,98 @@
1
+ import * as path from 'node:path';
2
+ import { autoAddExtension, canonicalExtension, type OutputFormat } from './formats.js';
3
+
4
+ export interface DefaultOutputInputs {
5
+ /** Original input argument: file path or `-` for stdin. */
6
+ inputArg: string;
7
+ /** True when input is `-` (stdin). */
8
+ isStdin: boolean;
9
+ /** Resolved output format. */
10
+ format: OutputFormat;
11
+ /** Current working directory; defaults to `process.cwd()`. */
12
+ cwd?: string;
13
+ }
14
+
15
+ /**
16
+ * Resolves the default output path for render operations when `-o` is absent.
17
+ *
18
+ * Rules:
19
+ * - Default-named outputs land in **cwd**, never next to the input.
20
+ * - File input: `<cwd>/<input-base>.<format>` (input directory is ignored).
21
+ * - Stdin input: `<cwd>/roadmap.<format>`.
22
+ */
23
+ export function defaultRenderOutputPath(inputs: DefaultOutputInputs): string {
24
+ const cwd = inputs.cwd ?? process.cwd();
25
+ const ext = canonicalExtension(inputs.format);
26
+ if (inputs.isStdin) {
27
+ return path.join(cwd, `roadmap${ext}`);
28
+ }
29
+ const base = path.basename(inputs.inputArg, path.extname(inputs.inputArg)) || 'roadmap';
30
+ return path.join(cwd, `${base}${ext}`);
31
+ }
32
+
33
+ export interface InitOutputInputs {
34
+ /** Positional argument from `--init` (project name); undefined → default `roadmap`. */
35
+ name?: string;
36
+ /** Current working directory; defaults to `process.cwd()`. */
37
+ cwd?: string;
38
+ }
39
+
40
+ /**
41
+ * Resolves the output path for `--init`. The positional is treated as a project
42
+ * *name*, not a file path:
43
+ *
44
+ * - No extension (`my-project`) → append `.nowline`.
45
+ * - Already `.nowline` → use literal.
46
+ * - Other extension → caller should reject as a usage error (exit 2).
47
+ * - Missing → default name `roadmap`.
48
+ */
49
+ export function defaultInitOutputPath(inputs: InitOutputInputs): string {
50
+ const cwd = inputs.cwd ?? process.cwd();
51
+ const name = inputs.name ?? 'roadmap';
52
+ const ext = path.extname(name);
53
+ if (!ext) {
54
+ return path.join(cwd, `${name}.nowline`);
55
+ }
56
+ return path.join(cwd, name);
57
+ }
58
+
59
+ /**
60
+ * Returns true when an `--init` positional has an extension other than
61
+ * `.nowline`. Caller should reject this with an exit-2 usage error.
62
+ */
63
+ export function initNameHasIncompatibleExtension(name: string): boolean {
64
+ const ext = path.extname(name).toLowerCase();
65
+ return ext !== '' && ext !== '.nowline';
66
+ }
67
+
68
+ /**
69
+ * Resolves the final output path for render operations, applying the
70
+ * extension-auto-add rule when `-o` is provided and has no extension.
71
+ */
72
+ export function resolveRenderOutputPath(args: {
73
+ outputArg: string | undefined;
74
+ isStdout: boolean;
75
+ inputArg: string;
76
+ isStdin: boolean;
77
+ format: OutputFormat;
78
+ cwd?: string;
79
+ }): { path: string; isStdout: boolean } {
80
+ if (args.isStdout) {
81
+ return { path: '-', isStdout: true };
82
+ }
83
+ if (args.outputArg !== undefined) {
84
+ return {
85
+ path: autoAddExtension(args.outputArg, args.format),
86
+ isStdout: false,
87
+ };
88
+ }
89
+ return {
90
+ path: defaultRenderOutputPath({
91
+ inputArg: args.inputArg,
92
+ isStdin: args.isStdin,
93
+ format: args.format,
94
+ cwd: args.cwd,
95
+ }),
96
+ isStdout: false,
97
+ };
98
+ }
@@ -0,0 +1,99 @@
1
+ import type { ParsedArgs } from '../cli/args.js';
2
+ import { defaultInitOutputPath, initNameHasIncompatibleExtension } from '../cli/output-path.js';
3
+ import { TEMPLATE_NAMES, TEMPLATES, type TemplateName } from '../generated/templates.js';
4
+ import { CliError, ExitCode } from '../io/exit-codes.js';
5
+ import { writeOutput } from '../io/write.js';
6
+
7
+ export interface InitHandlerOptions {
8
+ args: ParsedArgs;
9
+ /** Test seam: cwd override. Defaults to `process.cwd()`. */
10
+ cwd?: string;
11
+ }
12
+
13
+ /**
14
+ * `--init` mode handler. Scaffolds a starter `.nowline` file in cwd.
15
+ *
16
+ * Positional argument is the project *name*, not a file path:
17
+ * - No extension → append `.nowline`.
18
+ * - `.nowline` → use literal.
19
+ * - Other extension → exit 2.
20
+ * - Missing positional → default name "roadmap".
21
+ *
22
+ * `-o` overrides the resolved path. Existing files are silently overwritten.
23
+ */
24
+ export async function initHandler(options: InitHandlerOptions): Promise<void> {
25
+ const { args } = options;
26
+ const cwd = options.cwd ?? process.cwd();
27
+ const template = parseTemplateName(args.template);
28
+ const positional = args.positional;
29
+
30
+ if (
31
+ positional !== undefined &&
32
+ positional !== '' &&
33
+ initNameHasIncompatibleExtension(positional)
34
+ ) {
35
+ throw new CliError(
36
+ ExitCode.InputError,
37
+ `nowline: --init only scaffolds .nowline files; got "${positional}".`,
38
+ );
39
+ }
40
+
41
+ const projectName = projectNameFor(positional);
42
+ const titleName = titleFor(positional);
43
+ const templateText = TEMPLATES[template];
44
+ const rendered = applyName(templateText, titleName);
45
+
46
+ const outputPath = args.output ?? defaultInitOutputPath({ name: projectName, cwd });
47
+
48
+ await writeOutput(outputPath, rendered, 'text', { cwd });
49
+
50
+ if (args.logLevel === 'verbose') {
51
+ process.stderr.write(`nowline: wrote ${outputPath}\n`);
52
+ }
53
+ }
54
+
55
+ function parseTemplateName(raw: string | undefined): TemplateName {
56
+ const value = (raw ?? '').toLowerCase();
57
+ if (value === '') return 'minimal';
58
+ if ((TEMPLATE_NAMES as readonly string[]).includes(value)) {
59
+ return value as TemplateName;
60
+ }
61
+ throw new CliError(
62
+ ExitCode.InputError,
63
+ `nowline: unknown --template "${raw}". Choose one of: ${TEMPLATE_NAMES.join(', ')}.`,
64
+ );
65
+ }
66
+
67
+ function projectNameFor(positional: string | undefined): string | undefined {
68
+ if (!positional) return undefined;
69
+ return positional;
70
+ }
71
+
72
+ /**
73
+ * Returns the title to substitute into the roadmap declaration. When the user
74
+ * passes a positional we use it as-is; trailing `.nowline` is stripped so the
75
+ * title isn't `My Plan.nowline`.
76
+ */
77
+ function titleFor(positional: string | undefined): string | undefined {
78
+ if (!positional) return undefined;
79
+ return positional.endsWith('.nowline') ? positional.slice(0, -'.nowline'.length) : positional;
80
+ }
81
+
82
+ export function applyName(template: string, name: string | undefined): string {
83
+ if (!name) return template;
84
+ const lines = template.split('\n');
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i];
87
+ if (!/^\s*roadmap\b/.test(line)) continue;
88
+ lines[i] = rewriteRoadmapTitle(line, name);
89
+ break;
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+
94
+ function rewriteRoadmapTitle(line: string, name: string): string {
95
+ const match = line.match(/^(\s*roadmap\s+[A-Za-z_][A-Za-z0-9_-]*)(\s+"[^"]*")?(.*)$/);
96
+ if (!match) return line;
97
+ const [, head, , tail] = match;
98
+ return `${head} ${JSON.stringify(name)}${tail}`;
99
+ }