@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.
- package/LICENSE +190 -0
- package/README.md +372 -0
- package/dist/cli/args.d.ts +54 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +165 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/formats.d.ts +61 -0
- package/dist/cli/formats.d.ts.map +1 -0
- package/dist/cli/formats.js +153 -0
- package/dist/cli/formats.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +90 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output-path.d.ts +57 -0
- package/dist/cli/output-path.d.ts.map +1 -0
- package/dist/cli/output-path.js +70 -0
- package/dist/cli/output-path.js.map +1 -0
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +80 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/render.d.ts +15 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +435 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/serve.d.ts +16 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +287 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/convert/parse-json.d.ts +7 -0
- package/dist/convert/parse-json.d.ts.map +1 -0
- package/dist/convert/parse-json.js +34 -0
- package/dist/convert/parse-json.js.map +1 -0
- package/dist/convert/printer.d.ts +6 -0
- package/dist/convert/printer.d.ts.map +1 -0
- package/dist/convert/printer.js +334 -0
- package/dist/convert/printer.js.map +1 -0
- package/dist/convert/schema.d.ts +33 -0
- package/dist/convert/schema.d.ts.map +1 -0
- package/dist/convert/schema.js +77 -0
- package/dist/convert/schema.js.map +1 -0
- package/dist/core/parse.d.ts +24 -0
- package/dist/core/parse.d.ts.map +1 -0
- package/dist/core/parse.js +58 -0
- package/dist/core/parse.js.map +1 -0
- package/dist/diagnostics/adapt.d.ts +46 -0
- package/dist/diagnostics/adapt.d.ts.map +1 -0
- package/dist/diagnostics/adapt.js +109 -0
- package/dist/diagnostics/adapt.js.map +1 -0
- package/dist/diagnostics/format.d.ts +18 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +41 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/diagnostics/index.d.ts +5 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +5 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/diagnostics/json.d.ts +8 -0
- package/dist/diagnostics/json.d.ts.map +1 -0
- package/dist/diagnostics/json.js +24 -0
- package/dist/diagnostics/json.js.map +1 -0
- package/dist/diagnostics/model.d.ts +44 -0
- package/dist/diagnostics/model.d.ts.map +1 -0
- package/dist/diagnostics/model.js +2 -0
- package/dist/diagnostics/model.js.map +1 -0
- package/dist/diagnostics/text.d.ts +6 -0
- package/dist/diagnostics/text.d.ts.map +1 -0
- package/dist/diagnostics/text.js +43 -0
- package/dist/diagnostics/text.js.map +1 -0
- package/dist/generated/templates.d.ts +4 -0
- package/dist/generated/templates.d.ts.map +1 -0
- package/dist/generated/templates.js +9 -0
- package/dist/generated/templates.js.map +1 -0
- package/dist/generated/version.d.ts +11 -0
- package/dist/generated/version.d.ts.map +1 -0
- package/dist/generated/version.js +8 -0
- package/dist/generated/version.js.map +1 -0
- package/dist/i18n/locale.d.ts +56 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +107 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/io/config.d.ts +2 -0
- package/dist/io/config.d.ts.map +1 -0
- package/dist/io/config.js +5 -0
- package/dist/io/config.js.map +1 -0
- package/dist/io/exit-codes.d.ts +12 -0
- package/dist/io/exit-codes.d.ts.map +1 -0
- package/dist/io/exit-codes.js +15 -0
- package/dist/io/exit-codes.js.map +1 -0
- package/dist/io/read.d.ts +13 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +53 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/write.d.ts +32 -0
- package/dist/io/write.d.ts.map +1 -0
- package/dist/io/write.js +61 -0
- package/dist/io/write.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +20 -0
- package/dist/version.js.map +1 -0
- package/man/fr/nowline.1 +424 -0
- package/man/fr/nowline.5 +1864 -0
- package/man/nowline.1 +517 -0
- package/man/nowline.5 +1784 -0
- package/package.json +66 -0
- package/scripts/bundle-templates.mjs +105 -0
- package/scripts/compile.mjs +131 -0
- package/src/cli/args.ts +252 -0
- package/src/cli/formats.ts +207 -0
- package/src/cli/help.ts +92 -0
- package/src/cli/output-path.ts +98 -0
- package/src/commands/init.ts +99 -0
- package/src/commands/render.ts +566 -0
- package/src/commands/serve.ts +322 -0
- package/src/convert/parse-json.ts +57 -0
- package/src/convert/printer.ts +376 -0
- package/src/convert/schema.ts +105 -0
- package/src/core/parse.ts +93 -0
- package/src/diagnostics/adapt.ts +148 -0
- package/src/diagnostics/format.ts +70 -0
- package/src/diagnostics/index.ts +4 -0
- package/src/diagnostics/json.ts +30 -0
- package/src/diagnostics/model.ts +48 -0
- package/src/diagnostics/text.ts +62 -0
- package/src/generated/templates.ts +12 -0
- package/src/generated/version.ts +18 -0
- package/src/i18n/locale.ts +133 -0
- package/src/index.ts +60 -0
- package/src/io/config.ts +11 -0
- package/src/io/exit-codes.ts +18 -0
- package/src/io/read.ts +70 -0
- package/src/io/write.ts +94 -0
- 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
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -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
|
+
}
|