stego-cli 0.4.2 → 0.5.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/README.md +68 -22
- package/dist/shared/src/domain/frontmatter/validators.js +20 -2
- package/dist/shared/src/domain/images/index.js +1 -0
- package/dist/shared/src/domain/images/style.js +185 -0
- package/dist/shared/src/index.js +1 -0
- package/dist/stego-cli/src/app/command-registry.js +93 -1
- package/dist/stego-cli/src/app/create-cli-app.js +3 -0
- package/dist/stego-cli/src/app/error-boundary.js +11 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-add.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-read.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +0 -1
- package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +12 -1
- package/dist/stego-cli/src/modules/compile/commands/build.js +1 -2
- package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +0 -3
- package/dist/stego-cli/src/modules/compile/domain/image-settings.js +394 -0
- package/dist/stego-cli/src/modules/export/application/run-export.js +22 -1
- package/dist/stego-cli/src/modules/export/commands/export.js +1 -2
- package/dist/stego-cli/src/modules/export/infra/pandoc-exporter.js +29 -2
- package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +1 -1
- package/dist/stego-cli/src/modules/project/application/create-project.js +41 -1
- package/dist/stego-cli/src/modules/project/application/infer-project.js +1 -1
- package/dist/stego-cli/src/modules/project/commands/new-project.js +1 -1
- package/dist/stego-cli/src/modules/quality/application/inspect-project.js +147 -112
- package/dist/stego-cli/src/modules/quality/commands/check-stage.js +1 -2
- package/dist/stego-cli/src/modules/quality/commands/lint.js +1 -2
- package/dist/stego-cli/src/modules/quality/commands/validate.js +1 -2
- package/dist/stego-cli/src/modules/scaffold/commands/init.js +2 -2
- package/dist/stego-cli/src/modules/scaffold/domain/templates.js +15 -14
- package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +2 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +1 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +1 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-read.js +1 -2
- package/filters/image-layout.css +23 -0
- package/filters/image-layout.lua +170 -0
- package/package.json +4 -2
- package/projects/fiction-example/assets/README.md +31 -0
- package/projects/stego-docs/assets/README.md +31 -0
- package/projects/stego-docs/manuscript/500-project-configuration.md +37 -0
- package/projects/stego-docs/manuscript/800-build-export-and-release-outputs.md +4 -0
- package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +0 -1
- package/dist/stego-cli/src/modules/project/domain/project.js +0 -1
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@ stego init
|
|
|
17
17
|
npm install
|
|
18
18
|
|
|
19
19
|
stego list-projects
|
|
20
|
-
stego validate
|
|
21
|
-
stego build
|
|
22
|
-
stego new
|
|
20
|
+
stego validate -p fiction-example
|
|
21
|
+
stego build -p fiction-example
|
|
22
|
+
stego new -p fiction-example
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
`stego init` scaffolds two example projects:
|
|
@@ -54,18 +54,20 @@ Run commands from the workspace root and target a project with `--project`.
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
stego list-projects
|
|
57
|
-
stego new-project
|
|
58
|
-
stego new
|
|
59
|
-
stego validate
|
|
60
|
-
stego build
|
|
61
|
-
stego check-stage
|
|
62
|
-
stego export
|
|
63
|
-
stego spine read
|
|
64
|
-
stego spine new-category
|
|
65
|
-
stego spine new
|
|
57
|
+
stego new-project -p my-book --title "My Book"
|
|
58
|
+
stego new -p fiction-example
|
|
59
|
+
stego validate -p fiction-example
|
|
60
|
+
stego build -p fiction-example
|
|
61
|
+
stego check-stage -p fiction-example --stage revise
|
|
62
|
+
stego export -p fiction-example --format md
|
|
63
|
+
stego spine read -p fiction-example
|
|
64
|
+
stego spine new-category -p fiction-example --key characters
|
|
65
|
+
stego spine new -p fiction-example --category characters --filename supporting/abigail
|
|
66
66
|
stego metadata read projects/fiction-example/manuscript/100-the-commission.md --format json
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
All project-scoped commands accept `-p` as shorthand for `--project`.
|
|
70
|
+
|
|
69
71
|
`stego new` also supports `--i <prefix>` for numeric prefix override and `--filename <name>` for an explicit manuscript filename.
|
|
70
72
|
|
|
71
73
|
Spine V2 is directory-inferred:
|
|
@@ -74,6 +76,50 @@ Spine V2 is directory-inferred:
|
|
|
74
76
|
- category metadata lives at `spine/<category>/_category.md`
|
|
75
77
|
- entries are markdown files in each category directory tree
|
|
76
78
|
|
|
79
|
+
## Image assets and manuscript image settings
|
|
80
|
+
|
|
81
|
+
Stego projects scaffold an `assets/` directory for manuscript images.
|
|
82
|
+
|
|
83
|
+
Use standard Markdown image syntax in manuscript files:
|
|
84
|
+
|
|
85
|
+
```md
|
|
86
|
+

|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Set global image defaults in `stego-project.json`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"images": {
|
|
94
|
+
"layout": "block",
|
|
95
|
+
"align": "center",
|
|
96
|
+
"width": "50%",
|
|
97
|
+
"classes": ["illustration"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use manuscript frontmatter `images` only for per-path overrides:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
images:
|
|
106
|
+
assets/maps/city-plan.png:
|
|
107
|
+
layout: inline
|
|
108
|
+
align: left
|
|
109
|
+
width: 100%
|
|
110
|
+
classes: [diagram]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Rules:
|
|
114
|
+
|
|
115
|
+
- project-level global keys in `stego-project.json images`: `width`, `height`, `classes`, `id`, `attrs`, `layout`, `align`
|
|
116
|
+
- manuscript frontmatter `images` keys are per-image overrides by project-relative asset path
|
|
117
|
+
- manuscript frontmatter should not define global keys; put defaults in `stego-project.json`
|
|
118
|
+
- `layout` (`block|inline`) and `align` (`left|center|right`) are emitted as image attrs (`data-layout`, `data-align`) in compiled markdown
|
|
119
|
+
- EPUB export includes a default image layout stylesheet (`filters/image-layout.css`) for `data-layout`/`data-align` behavior
|
|
120
|
+
- inline image attrs in markdown win over both project defaults and frontmatter overrides
|
|
121
|
+
- local manuscript image targets outside `assets/` are reported as validate warnings
|
|
122
|
+
|
|
77
123
|
Projects also include local npm scripts so you can work from inside a project directory.
|
|
78
124
|
|
|
79
125
|
## Complete CLI command reference
|
|
@@ -90,16 +136,16 @@ Current `stego --help` command index:
|
|
|
90
136
|
```text
|
|
91
137
|
init [--force]
|
|
92
138
|
list-projects [--root <path>]
|
|
93
|
-
new-project --project <project-id> [--title <title>] [--prose-font <yes|no|prompt>] [--format <text|json>] [--root <path>]
|
|
94
|
-
new --project <project-id> [--i <prefix>|-i <prefix>] [--filename <name>] [--format <text|json>] [--root <path>]
|
|
95
|
-
validate --project <project-id> [--file <project-relative-manuscript-path>] [--root <path>]
|
|
96
|
-
build --project <project-id> [--root <path>]
|
|
97
|
-
check-stage --project <project-id> --stage <draft|revise|line-edit|proof|final> [--file <project-relative-manuscript-path>] [--root <path>]
|
|
98
|
-
lint --project <project-id> [--manuscript|--spine] [--root <path>]
|
|
99
|
-
export --project <project-id> --format <md|docx|pdf|epub> [--output <path>] [--root <path>]
|
|
100
|
-
spine read --project <project-id> [--format <text|json>] [--root <path>]
|
|
101
|
-
spine new-category --project <project-id> --key <category> [--label <label>] [--require-metadata] [--format <text|json>] [--root <path>]
|
|
102
|
-
spine new --project <project-id> --category <category> [--filename <relative-path>] [--format <text|json>] [--root <path>]
|
|
139
|
+
new-project --project|-p <project-id> [--title <title>] [--prose-font <yes|no|prompt>] [--format <text|json>] [--root <path>]
|
|
140
|
+
new --project|-p <project-id> [--i <prefix>|-i <prefix>] [--filename <name>] [--format <text|json>] [--root <path>]
|
|
141
|
+
validate --project|-p <project-id> [--file <project-relative-manuscript-path>] [--root <path>]
|
|
142
|
+
build --project|-p <project-id> [--root <path>]
|
|
143
|
+
check-stage --project|-p <project-id> --stage <draft|revise|line-edit|proof|final> [--file <project-relative-manuscript-path>] [--root <path>]
|
|
144
|
+
lint --project|-p <project-id> [--manuscript|--spine] [--root <path>]
|
|
145
|
+
export --project|-p <project-id> --format <md|docx|pdf|epub> [--output <path>] [--root <path>]
|
|
146
|
+
spine read --project|-p <project-id> [--format <text|json>] [--root <path>]
|
|
147
|
+
spine new-category --project|-p <project-id> --key <category> [--label <label>] [--require-metadata] [--format <text|json>] [--root <path>]
|
|
148
|
+
spine new --project|-p <project-id> --category <category> [--filename <relative-path>] [--format <text|json>] [--root <path>]
|
|
103
149
|
metadata read <markdown-path> [--format <text|json>]
|
|
104
150
|
metadata apply <markdown-path> --input <path|-> [--format <text|json>]
|
|
105
151
|
comments read <manuscript> [--format <text|json>]
|
|
@@ -32,7 +32,18 @@ export function normalizeFrontmatterRecord(raw) {
|
|
|
32
32
|
}
|
|
33
33
|
function normalizeFrontmatterValue(value, key) {
|
|
34
34
|
if (Array.isArray(value)) {
|
|
35
|
-
return value.map((item) =>
|
|
35
|
+
return value.map((item) => normalizeFrontmatterValue(item, key));
|
|
36
|
+
}
|
|
37
|
+
if (isPlainObject(value)) {
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
40
|
+
const normalizedKey = entryKey.trim();
|
|
41
|
+
if (!normalizedKey) {
|
|
42
|
+
throw new Error(`Metadata key '${key}' contains an empty nested key.`);
|
|
43
|
+
}
|
|
44
|
+
result[normalizedKey] = normalizeFrontmatterValue(entryValue, key);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
36
47
|
}
|
|
37
48
|
return normalizeFrontmatterScalar(value, key);
|
|
38
49
|
}
|
|
@@ -43,5 +54,12 @@ function normalizeFrontmatterScalar(value, key) {
|
|
|
43
54
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
44
55
|
return value;
|
|
45
56
|
}
|
|
46
|
-
throw new Error(`Metadata key '${key}' must be
|
|
57
|
+
throw new Error(`Metadata key '${key}' must be scalar, array, or object.`);
|
|
58
|
+
}
|
|
59
|
+
function isPlainObject(value) {
|
|
60
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const prototype = Object.getPrototypeOf(value);
|
|
64
|
+
return prototype === Object.prototype || prototype === null;
|
|
47
65
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./style.js";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export const IMAGE_STYLE_KEYS = ["width", "height", "classes", "id", "attrs", "layout", "align"];
|
|
2
|
+
export const IMAGE_GLOBAL_KEYS = new Set(IMAGE_STYLE_KEYS);
|
|
3
|
+
export function asPlainRecord(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const prototype = Object.getPrototypeOf(value);
|
|
8
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function normalizeImageScalar(value) {
|
|
14
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const normalized = String(value).trim();
|
|
18
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function normalizeImageClasses(value) {
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const classes = value
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.map((entry) => entry.trim())
|
|
25
|
+
.filter((entry) => entry.length > 0);
|
|
26
|
+
return classes;
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const classes = [];
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
if (typeof entry !== "string") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const normalized = entry.trim();
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
classes.push(normalized);
|
|
41
|
+
}
|
|
42
|
+
return classes;
|
|
43
|
+
}
|
|
44
|
+
export function normalizeImageAttrs(value) {
|
|
45
|
+
const record = asPlainRecord(value);
|
|
46
|
+
if (!record) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const attrs = {};
|
|
50
|
+
for (const [key, raw] of Object.entries(record)) {
|
|
51
|
+
const normalized = normalizeImageScalar(raw);
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
attrs[key] = normalized;
|
|
56
|
+
}
|
|
57
|
+
return Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
58
|
+
}
|
|
59
|
+
export function cloneImageStyle(style) {
|
|
60
|
+
return {
|
|
61
|
+
width: style?.width,
|
|
62
|
+
height: style?.height,
|
|
63
|
+
id: style?.id,
|
|
64
|
+
classes: style?.classes ? [...style.classes] : undefined,
|
|
65
|
+
attrs: style?.attrs ? { ...style.attrs } : undefined,
|
|
66
|
+
layout: style?.layout,
|
|
67
|
+
align: style?.align
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function mergeImageStyles(base, override) {
|
|
71
|
+
const merged = cloneImageStyle(base);
|
|
72
|
+
if (override.width) {
|
|
73
|
+
merged.width = override.width;
|
|
74
|
+
}
|
|
75
|
+
if (override.height) {
|
|
76
|
+
merged.height = override.height;
|
|
77
|
+
}
|
|
78
|
+
if (override.id) {
|
|
79
|
+
merged.id = override.id;
|
|
80
|
+
}
|
|
81
|
+
if (override.layout) {
|
|
82
|
+
merged.layout = override.layout;
|
|
83
|
+
}
|
|
84
|
+
if (override.align) {
|
|
85
|
+
merged.align = override.align;
|
|
86
|
+
}
|
|
87
|
+
if (override.classes && override.classes.length > 0) {
|
|
88
|
+
merged.classes = [...override.classes];
|
|
89
|
+
}
|
|
90
|
+
const attrs = { ...(merged.attrs ?? {}) };
|
|
91
|
+
for (const [key, value] of Object.entries(override.attrs ?? {})) {
|
|
92
|
+
attrs[key] = value;
|
|
93
|
+
}
|
|
94
|
+
merged.attrs = Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
95
|
+
return merged;
|
|
96
|
+
}
|
|
97
|
+
export function isImageStyleEmpty(style) {
|
|
98
|
+
if (!style) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return !style.width
|
|
102
|
+
&& !style.height
|
|
103
|
+
&& !style.id
|
|
104
|
+
&& !style.layout
|
|
105
|
+
&& !style.align
|
|
106
|
+
&& (!style.classes || style.classes.length === 0)
|
|
107
|
+
&& (!style.attrs || Object.keys(style.attrs).length === 0);
|
|
108
|
+
}
|
|
109
|
+
export function parseImageStyle(value) {
|
|
110
|
+
const record = asPlainRecord(value);
|
|
111
|
+
if (!record) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const style = {};
|
|
115
|
+
for (const [key, raw] of Object.entries(record)) {
|
|
116
|
+
if (key === "width" || key === "height" || key === "id") {
|
|
117
|
+
const normalized = normalizeImageScalar(raw);
|
|
118
|
+
if (normalized) {
|
|
119
|
+
style[key] = normalized;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (key === "layout") {
|
|
124
|
+
if (raw === "block" || raw === "inline") {
|
|
125
|
+
style.layout = raw;
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (key === "align") {
|
|
130
|
+
if (raw === "left" || raw === "center" || raw === "right") {
|
|
131
|
+
style.align = raw;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (key === "classes") {
|
|
136
|
+
const normalized = normalizeImageClasses(raw);
|
|
137
|
+
if (normalized) {
|
|
138
|
+
style.classes = normalized;
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (key === "attrs") {
|
|
143
|
+
const normalized = normalizeImageAttrs(raw);
|
|
144
|
+
if (normalized) {
|
|
145
|
+
style.attrs = normalized;
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const layoutFromAttrs = style.attrs?.["data-layout"];
|
|
151
|
+
if (!style.layout && (layoutFromAttrs === "block" || layoutFromAttrs === "inline")) {
|
|
152
|
+
style.layout = layoutFromAttrs;
|
|
153
|
+
}
|
|
154
|
+
const alignFromAttrs = style.attrs?.["data-align"];
|
|
155
|
+
if (!style.align && (alignFromAttrs === "left" || alignFromAttrs === "center" || alignFromAttrs === "right")) {
|
|
156
|
+
style.align = alignFromAttrs;
|
|
157
|
+
}
|
|
158
|
+
return isImageStyleEmpty(style) ? undefined : style;
|
|
159
|
+
}
|
|
160
|
+
export function normalizeImagePathKey(value) {
|
|
161
|
+
return value.trim().replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
162
|
+
}
|
|
163
|
+
export function extractImageDestinationTarget(value) {
|
|
164
|
+
let target = value.trim();
|
|
165
|
+
if (target.startsWith("<") && target.endsWith(">")) {
|
|
166
|
+
target = target.slice(1, -1).trim();
|
|
167
|
+
}
|
|
168
|
+
return target
|
|
169
|
+
.split(/\s+"/)[0]
|
|
170
|
+
.split(/\s+'/)[0]
|
|
171
|
+
.trim();
|
|
172
|
+
}
|
|
173
|
+
export function stripImageQueryAndAnchor(target) {
|
|
174
|
+
return target.split("#")[0].split("?")[0].trim();
|
|
175
|
+
}
|
|
176
|
+
export function isExternalImageTarget(target) {
|
|
177
|
+
return target.startsWith("http://")
|
|
178
|
+
|| target.startsWith("https://")
|
|
179
|
+
|| target.startsWith("mailto:")
|
|
180
|
+
|| target.startsWith("tel:")
|
|
181
|
+
|| target.startsWith("data:");
|
|
182
|
+
}
|
|
183
|
+
export function inferEffectiveImageLayout(style) {
|
|
184
|
+
return style.layout ?? (style.align ? "block" : undefined);
|
|
185
|
+
}
|
package/dist/shared/src/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export * as FrontmatterDomain from "./domain/frontmatter/index.js";
|
|
|
3
3
|
export * as CommentsDomain from "./domain/comments/index.js";
|
|
4
4
|
export * as StagesDomain from "./domain/stages/index.js";
|
|
5
5
|
export * as ProjectDomain from "./domain/project/index.js";
|
|
6
|
+
export * as ImagesDomain from "./domain/images/index.js";
|
|
6
7
|
export * as SharedUtils from "./utils/index.js";
|
|
@@ -3,15 +3,62 @@ export class CommandRegistry {
|
|
|
3
3
|
cli;
|
|
4
4
|
appContext;
|
|
5
5
|
multiTokenCommandMappings = [];
|
|
6
|
+
helpEntries = [];
|
|
6
7
|
constructor(appContext) {
|
|
7
8
|
this.appContext = appContext;
|
|
8
9
|
this.cli = cac("stego");
|
|
9
10
|
}
|
|
10
11
|
showHelp() {
|
|
11
|
-
this.
|
|
12
|
+
const commandWidth = this.helpEntries.reduce((max, entry) => Math.max(max, entry.usageName.length), 0);
|
|
13
|
+
const lines = [
|
|
14
|
+
"stego",
|
|
15
|
+
"",
|
|
16
|
+
"Usage:",
|
|
17
|
+
" $ stego <command> [options]",
|
|
18
|
+
"",
|
|
19
|
+
"Commands:"
|
|
20
|
+
];
|
|
21
|
+
for (const entry of this.helpEntries) {
|
|
22
|
+
lines.push(` ${entry.usageName.padEnd(commandWidth + 2)}${entry.description}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
lines.push("For more info, run any command with the `--help` flag:");
|
|
26
|
+
for (const entry of this.helpEntries) {
|
|
27
|
+
lines.push(` $ stego ${entry.commandTokens.join(" ")} --help`);
|
|
28
|
+
}
|
|
29
|
+
this.appContext.stdout.write(`${lines.join("\n")}\n`);
|
|
30
|
+
}
|
|
31
|
+
tryShowCommandHelp(argv) {
|
|
32
|
+
if (!containsHelpFlag(argv)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const candidate = argv.filter((token) => token !== "--help" && token !== "-h");
|
|
36
|
+
const entry = this.findHelpEntry(candidate);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const optionRows = [...entry.options.map((option) => ({
|
|
41
|
+
flags: option.flags,
|
|
42
|
+
description: option.description ?? ""
|
|
43
|
+
})), { flags: "-h, --help", description: "Display this message" }];
|
|
44
|
+
const optionWidth = optionRows.reduce((max, row) => Math.max(max, row.flags.length), 0);
|
|
45
|
+
const lines = [
|
|
46
|
+
"stego",
|
|
47
|
+
"",
|
|
48
|
+
"Usage:",
|
|
49
|
+
` $ stego ${entry.usageName}`,
|
|
50
|
+
"",
|
|
51
|
+
"Options:"
|
|
52
|
+
];
|
|
53
|
+
for (const row of optionRows) {
|
|
54
|
+
lines.push(` ${row.flags.padEnd(optionWidth + 2)}${row.description}`);
|
|
55
|
+
}
|
|
56
|
+
this.appContext.stdout.write(`${lines.join("\n")}\n`);
|
|
57
|
+
return true;
|
|
12
58
|
}
|
|
13
59
|
register(spec) {
|
|
14
60
|
const commandName = normalizeRegisteredCommandName(spec.name, this.multiTokenCommandMappings);
|
|
61
|
+
this.helpEntries.push(createCommandHelpEntry(spec.name, spec.description, spec.options ?? []));
|
|
15
62
|
let command = this.cli.command(commandName, spec.description);
|
|
16
63
|
for (const option of spec.options ?? []) {
|
|
17
64
|
const optionConfig = option.defaultValue === undefined
|
|
@@ -46,6 +93,27 @@ export class CommandRegistry {
|
|
|
46
93
|
}
|
|
47
94
|
return this.cli.runMatchedCommand();
|
|
48
95
|
}
|
|
96
|
+
findHelpEntry(argv) {
|
|
97
|
+
if (argv.length === 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const entries = [...this.helpEntries]
|
|
101
|
+
.sort((a, b) => b.commandTokens.length - a.commandTokens.length);
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (startsWithTokens(argv, entry.commandTokens)) {
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const colonCandidate = argv[0];
|
|
108
|
+
if (typeof colonCandidate === "string") {
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (entry.normalizedToken === colonCandidate) {
|
|
111
|
+
return entry;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
49
117
|
}
|
|
50
118
|
function normalizeRegisteredCommandName(rawName, mappings) {
|
|
51
119
|
const tokens = rawName.trim().split(/\s+/).filter(Boolean);
|
|
@@ -72,6 +140,27 @@ function normalizeRegisteredCommandName(rawName, mappings) {
|
|
|
72
140
|
? `${normalizedToken} ${argumentTokens.join(" ")}`
|
|
73
141
|
: normalizedToken;
|
|
74
142
|
}
|
|
143
|
+
function createCommandHelpEntry(rawName, description, options) {
|
|
144
|
+
const tokens = rawName.trim().split(/\s+/).filter(Boolean);
|
|
145
|
+
const commandTokens = extractCommandTokens(tokens);
|
|
146
|
+
return {
|
|
147
|
+
usageName: rawName,
|
|
148
|
+
description,
|
|
149
|
+
commandTokens,
|
|
150
|
+
normalizedToken: commandTokens.join(":"),
|
|
151
|
+
options
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function extractCommandTokens(tokens) {
|
|
155
|
+
const commandTokens = [];
|
|
156
|
+
for (const token of tokens) {
|
|
157
|
+
if (token.startsWith("<") || token.startsWith("[")) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
commandTokens.push(token);
|
|
161
|
+
}
|
|
162
|
+
return commandTokens;
|
|
163
|
+
}
|
|
75
164
|
function normalizeIncomingArgv(argv, mappings) {
|
|
76
165
|
if (argv.length === 0 || mappings.length === 0) {
|
|
77
166
|
return argv;
|
|
@@ -100,6 +189,9 @@ function normalizeDashInputValueArgv(argv) {
|
|
|
100
189
|
}
|
|
101
190
|
return normalized;
|
|
102
191
|
}
|
|
192
|
+
function containsHelpFlag(argv) {
|
|
193
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
194
|
+
}
|
|
103
195
|
function startsWithTokens(input, expected) {
|
|
104
196
|
if (input.length < expected.length) {
|
|
105
197
|
return false;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
function shouldRenderJsonError(argv) {
|
|
2
|
-
|
|
2
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
3
|
+
const current = argv[index];
|
|
4
|
+
const next = argv[index + 1];
|
|
5
|
+
if (current === "--format" && typeof next === "string" && next.toLowerCase() === "json") {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (current.startsWith("--format=") && current.slice("--format=".length).toLowerCase() === "json") {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
3
13
|
}
|
|
4
14
|
function errorEnvelope(code, message, details) {
|
|
5
15
|
return {
|
|
@@ -6,7 +6,6 @@ export function registerCommentsAddCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments add <manuscript>",
|
|
8
8
|
description: "Add a new comment",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--message <text>", description: "Comment text" },
|
|
12
11
|
{ flags: "--author <name>", description: "Comment author" },
|
|
@@ -6,7 +6,6 @@ export function registerCommentsClearResolvedCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments clear-resolved <manuscript>",
|
|
8
8
|
description: "Clear resolved comments",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--format <format>", description: "text|json" }
|
|
12
11
|
],
|
|
@@ -6,7 +6,6 @@ export function registerCommentsDeleteCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments delete <manuscript>",
|
|
8
8
|
description: "Delete a comment",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--comment-id <id>", description: "Comment id (CMT-####)" },
|
|
12
11
|
{ flags: "--format <format>", description: "text|json" }
|
|
@@ -6,7 +6,6 @@ export function registerCommentsReplyCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments reply <manuscript>",
|
|
8
8
|
description: "Reply to an existing comment",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--comment-id <id>", description: "Comment id (CMT-####)" },
|
|
12
11
|
{ flags: "--message <text>", description: "Reply text" },
|
|
@@ -6,7 +6,6 @@ export function registerCommentsSetStatusCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments set-status <manuscript>",
|
|
8
8
|
description: "Set comment status",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--comment-id <id>", description: "Comment id (CMT-####)" },
|
|
12
11
|
{ flags: "--status <status>", description: "open|resolved" },
|
|
@@ -6,7 +6,6 @@ export function registerCommentsSyncAnchorsCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "comments sync-anchors <manuscript>",
|
|
8
8
|
description: "Sync comment anchors",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
10
|
{ flags: "--input <path>", description: "JSON payload path or '-'" },
|
|
12
11
|
{ flags: "--format <format>", description: "text|json" }
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { nowIsoString } from "../../../platform/clock.js";
|
|
2
2
|
import { renderCompiledManuscript } from "../domain/compile-structure.js";
|
|
3
|
+
import { rewriteMarkdownImagesForChapter } from "../domain/image-settings.js";
|
|
3
4
|
import { writeCompiledOutput } from "../infra/dist-writer.js";
|
|
4
5
|
export function compileManuscript(input) {
|
|
6
|
+
const chapters = input.chapters.map((chapter) => ({
|
|
7
|
+
...chapter,
|
|
8
|
+
body: rewriteMarkdownImagesForChapter({
|
|
9
|
+
body: chapter.body,
|
|
10
|
+
chapterPath: chapter.path,
|
|
11
|
+
projectRoot: input.project.root,
|
|
12
|
+
projectMeta: input.project.meta,
|
|
13
|
+
frontmatter: chapter.metadata
|
|
14
|
+
})
|
|
15
|
+
}));
|
|
5
16
|
const markdown = renderCompiledManuscript({
|
|
6
17
|
generatedAt: nowIsoString(),
|
|
7
18
|
projectId: input.project.id,
|
|
8
19
|
title: input.project.meta.title || input.project.id,
|
|
9
20
|
subtitle: input.project.meta.subtitle,
|
|
10
21
|
author: input.project.meta.author,
|
|
11
|
-
chapters
|
|
22
|
+
chapters,
|
|
12
23
|
compileStructureLevels: input.compileStructureLevels
|
|
13
24
|
});
|
|
14
25
|
const outputPath = writeCompiledOutput(input.project.distDir, input.project.id, markdown);
|
|
@@ -7,9 +7,8 @@ export function registerBuildCommand(registry) {
|
|
|
7
7
|
registry.register({
|
|
8
8
|
name: "build",
|
|
9
9
|
description: "Compile manuscript output",
|
|
10
|
-
allowUnknownOptions: true,
|
|
11
10
|
options: [
|
|
12
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
11
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
13
12
|
{ flags: "--root <path>", description: "Workspace root path" }
|
|
14
13
|
],
|
|
15
14
|
action: (context) => {
|
|
@@ -2,7 +2,6 @@ export function renderCompiledManuscript(input) {
|
|
|
2
2
|
const tocEntries = [];
|
|
3
3
|
const previousGroupValues = new Map();
|
|
4
4
|
const previousGroupTitles = new Map();
|
|
5
|
-
const entryHeadingLevel = Math.min(6, 2 + input.compileStructureLevels.length);
|
|
6
5
|
const lines = [];
|
|
7
6
|
lines.push(`<!-- generated: ${input.generatedAt} -->`);
|
|
8
7
|
lines.push("");
|
|
@@ -59,8 +58,6 @@ export function renderCompiledManuscript(input) {
|
|
|
59
58
|
previousGroupValues.set(level.key, currentValue);
|
|
60
59
|
previousGroupTitles.set(level.key, currentTitle);
|
|
61
60
|
}
|
|
62
|
-
lines.push(`${"#".repeat(entryHeadingLevel)} ${chapter.title}`);
|
|
63
|
-
lines.push("");
|
|
64
61
|
lines.push(`<!-- source: ${chapter.relativePath} | order: ${chapter.order} | status: ${chapter.status} -->`);
|
|
65
62
|
lines.push("");
|
|
66
63
|
lines.push(chapter.body.trim());
|