stego-cli 0.4.0 → 0.4.2
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/.vscode/extensions.json +7 -0
- package/README.md +41 -0
- package/dist/shared/src/contracts/cli/envelopes.js +19 -0
- package/dist/shared/src/contracts/cli/errors.js +14 -0
- package/dist/shared/src/contracts/cli/exit-codes.js +15 -0
- package/dist/shared/src/contracts/cli/index.js +6 -0
- package/dist/shared/src/contracts/cli/metadata.js +1 -0
- package/dist/shared/src/contracts/cli/operations.js +1 -0
- package/dist/shared/src/domain/comments/anchors.js +1 -0
- package/dist/shared/src/domain/comments/index.js +4 -0
- package/dist/shared/src/domain/comments/serializer.js +1 -0
- package/dist/shared/src/domain/comments/thread-key.js +21 -0
- package/dist/shared/src/domain/frontmatter/index.js +3 -0
- package/dist/shared/src/domain/frontmatter/parser.js +34 -0
- package/dist/shared/src/domain/frontmatter/serializer.js +32 -0
- package/dist/shared/src/domain/frontmatter/validators.js +47 -0
- package/dist/shared/src/domain/project/index.js +4 -0
- package/dist/shared/src/domain/stages/index.js +20 -0
- package/dist/shared/src/index.js +6 -0
- package/dist/shared/src/utils/guards.js +6 -0
- package/dist/shared/src/utils/index.js +3 -0
- package/dist/shared/src/utils/invariant.js +5 -0
- package/dist/shared/src/utils/result.js +6 -0
- package/dist/stego-cli/src/app/cli-version.js +32 -0
- package/dist/stego-cli/src/app/command-context.js +1 -0
- package/dist/stego-cli/src/app/command-registry.js +121 -0
- package/dist/stego-cli/src/app/create-cli-app.js +42 -0
- package/dist/stego-cli/src/app/error-boundary.js +42 -0
- package/dist/stego-cli/src/app/index.js +6 -0
- package/dist/stego-cli/src/app/output-renderer.js +6 -0
- package/dist/stego-cli/src/main.js +14 -0
- package/dist/stego-cli/src/modules/comments/application/comment-operations.js +457 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-add.js +40 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +33 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-read.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +36 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +35 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +33 -0
- package/dist/stego-cli/src/modules/comments/domain/comment-policy.js +14 -0
- package/dist/stego-cli/src/modules/comments/index.js +20 -0
- package/dist/stego-cli/src/modules/comments/infra/comments-repo.js +68 -0
- package/dist/stego-cli/src/modules/comments/types.js +1 -0
- package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +16 -0
- package/dist/stego-cli/src/modules/compile/commands/build.js +51 -0
- package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +105 -0
- package/dist/stego-cli/src/modules/compile/index.js +8 -0
- package/dist/stego-cli/src/modules/compile/infra/dist-writer.js +8 -0
- package/dist/stego-cli/src/modules/compile/types.js +1 -0
- package/dist/stego-cli/src/modules/export/application/run-export.js +29 -0
- package/dist/stego-cli/src/modules/export/commands/export.js +61 -0
- package/dist/stego-cli/src/modules/export/domain/exporter.js +6 -0
- package/dist/stego-cli/src/modules/export/index.js +8 -0
- package/dist/stego-cli/src/modules/export/types.js +1 -0
- package/dist/stego-cli/src/modules/index.js +22 -0
- package/dist/stego-cli/src/modules/manuscript/application/create-manuscript.js +107 -0
- package/dist/stego-cli/src/modules/manuscript/application/order-inference.js +56 -0
- package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +54 -0
- package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +1 -0
- package/dist/stego-cli/src/modules/manuscript/index.js +9 -0
- package/dist/stego-cli/src/modules/manuscript/infra/manuscript-repo.js +39 -0
- package/dist/stego-cli/src/modules/manuscript/types.js +1 -0
- package/dist/stego-cli/src/modules/metadata/application/apply-metadata.js +45 -0
- package/dist/stego-cli/src/modules/metadata/application/read-metadata.js +18 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-apply.js +38 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-read.js +33 -0
- package/dist/stego-cli/src/modules/metadata/domain/metadata.js +1 -0
- package/dist/stego-cli/src/modules/metadata/index.js +11 -0
- package/dist/stego-cli/src/modules/metadata/infra/metadata-repo.js +68 -0
- package/dist/stego-cli/src/modules/metadata/types.js +1 -0
- package/dist/stego-cli/src/modules/project/application/create-project.js +203 -0
- package/dist/stego-cli/src/modules/project/application/infer-project.js +72 -0
- package/dist/stego-cli/src/modules/project/commands/new-project.js +86 -0
- package/dist/stego-cli/src/modules/project/domain/project.js +1 -0
- package/dist/stego-cli/src/modules/project/index.js +9 -0
- package/dist/stego-cli/src/modules/project/infra/project-repo.js +27 -0
- package/dist/stego-cli/src/modules/project/types.js +1 -0
- package/dist/stego-cli/src/modules/quality/application/inspect-project.js +603 -0
- package/dist/stego-cli/src/modules/quality/application/lint-runner.js +313 -0
- package/dist/stego-cli/src/modules/quality/application/stage-check.js +87 -0
- package/dist/stego-cli/src/modules/quality/commands/check-stage.js +54 -0
- package/dist/stego-cli/src/modules/quality/commands/lint.js +51 -0
- package/dist/stego-cli/src/modules/quality/commands/validate.js +50 -0
- package/dist/stego-cli/src/modules/quality/domain/issues.js +1 -0
- package/dist/stego-cli/src/modules/quality/domain/policies.js +1 -0
- package/dist/stego-cli/src/modules/quality/index.js +14 -0
- package/dist/stego-cli/src/modules/quality/infra/cspell-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/infra/markdownlint-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/types.js +1 -0
- package/dist/stego-cli/src/modules/scaffold/application/scaffold-workspace.js +307 -0
- package/dist/stego-cli/src/modules/scaffold/commands/init.js +34 -0
- package/dist/stego-cli/src/modules/scaffold/domain/templates.js +182 -0
- package/dist/stego-cli/src/modules/scaffold/index.js +8 -0
- package/dist/stego-cli/src/modules/scaffold/infra/template-repo.js +33 -0
- package/dist/stego-cli/src/modules/scaffold/types.js +1 -0
- package/dist/stego-cli/src/modules/spine/application/create-category.js +14 -0
- package/dist/stego-cli/src/modules/spine/application/create-entry.js +9 -0
- package/dist/stego-cli/src/modules/spine/application/read-catalog.js +13 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +33 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +64 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +65 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-read.js +49 -0
- package/dist/{spine/spine-domain.js → stego-cli/src/modules/spine/domain/spine.js} +13 -7
- package/dist/stego-cli/src/modules/spine/index.js +16 -0
- package/dist/stego-cli/src/modules/spine/infra/spine-repo.js +46 -0
- package/dist/stego-cli/src/modules/spine/types.js +1 -0
- package/dist/stego-cli/src/modules/workspace/application/discover-projects.js +18 -0
- package/dist/stego-cli/src/modules/workspace/application/resolve-workspace.js +73 -0
- package/dist/stego-cli/src/modules/workspace/commands/list-projects.js +40 -0
- package/dist/stego-cli/src/modules/workspace/index.js +9 -0
- package/dist/stego-cli/src/modules/workspace/infra/workspace-repo.js +37 -0
- package/dist/stego-cli/src/modules/workspace/types.js +1 -0
- package/dist/stego-cli/src/platform/clock.js +3 -0
- package/dist/stego-cli/src/platform/fs.js +13 -0
- package/dist/stego-cli/src/platform/index.js +3 -0
- package/dist/stego-cli/src/platform/temp-files.js +6 -0
- package/package.json +20 -11
- package/dist/comments/comments-command.js +0 -499
- package/dist/comments/errors.js +0 -20
- package/dist/metadata/metadata-command.js +0 -127
- package/dist/metadata/metadata-domain.js +0 -209
- package/dist/spine/spine-command.js +0 -129
- package/dist/stego-cli.js +0 -2107
- /package/dist/{exporters/exporter-types.js → shared/src/contracts/cli/comments.js} +0 -0
- /package/dist/{comments/comment-domain.js → shared/src/domain/comments/parser.js} +0 -0
- /package/dist/{exporters → stego-cli/src/modules/export/infra}/markdown-exporter.js +0 -0
- /package/dist/{exporters → stego-cli/src/modules/export/infra}/pandoc-exporter.js +0 -0
package/README.md
CHANGED
|
@@ -42,6 +42,12 @@ Start by reading the manuscript files in order, or build the docs project:
|
|
|
42
42
|
stego build --project stego-docs
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
Internal architecture notes for the domain-first module layout live in:
|
|
48
|
+
|
|
49
|
+
- `docs/architecture.md`
|
|
50
|
+
|
|
45
51
|
## Core commands
|
|
46
52
|
|
|
47
53
|
Run commands from the workspace root and target a project with `--project`.
|
|
@@ -70,6 +76,41 @@ Spine V2 is directory-inferred:
|
|
|
70
76
|
|
|
71
77
|
Projects also include local npm scripts so you can work from inside a project directory.
|
|
72
78
|
|
|
79
|
+
## Complete CLI command reference
|
|
80
|
+
|
|
81
|
+
The full command surface is available via:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
stego --help
|
|
85
|
+
stego --version
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Current `stego --help` command index:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
init [--force]
|
|
92
|
+
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>]
|
|
103
|
+
metadata read <markdown-path> [--format <text|json>]
|
|
104
|
+
metadata apply <markdown-path> --input <path|-> [--format <text|json>]
|
|
105
|
+
comments read <manuscript> [--format <text|json>]
|
|
106
|
+
comments add <manuscript> [--message <text> | --input <path|->] [--author <name>] [--start-line <n> --start-col <n> --end-line <n> --end-col <n>] [--cursor-line <n>] [--format <text|json>]
|
|
107
|
+
comments reply <manuscript> --comment-id <CMT-####> [--message <text> | --input <path|->] [--author <name>] [--format <text|json>]
|
|
108
|
+
comments set-status <manuscript> --comment-id <CMT-####> --status <open|resolved> [--thread] [--format <text|json>]
|
|
109
|
+
comments delete <manuscript> --comment-id <CMT-####> [--format <text|json>]
|
|
110
|
+
comments clear-resolved <manuscript> [--format <text|json>]
|
|
111
|
+
comments sync-anchors <manuscript> --input <path|-> [--format <text|json>]
|
|
112
|
+
```
|
|
113
|
+
|
|
73
114
|
## Advanced integration command
|
|
74
115
|
|
|
75
116
|
`stego comments add` is a machine-facing command for editor/tool integrations.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const CLI_CONTRACT_VERSION = 1;
|
|
2
|
+
export function successEnvelope(operation, data) {
|
|
3
|
+
return {
|
|
4
|
+
ok: true,
|
|
5
|
+
version: CLI_CONTRACT_VERSION,
|
|
6
|
+
operation,
|
|
7
|
+
data
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function errorEnvelope(code, message, options) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
version: CLI_CONTRACT_VERSION,
|
|
14
|
+
operation: options?.operation,
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
details: options?.details
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getExitCodeForError } from "./exit-codes.js";
|
|
2
|
+
export class CliError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CliError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
get exitCode() {
|
|
12
|
+
return getExitCodeForError(this.code);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const EXIT_CODES = {
|
|
2
|
+
INVALID_USAGE: 2,
|
|
3
|
+
WORKSPACE_NOT_FOUND: 3,
|
|
4
|
+
PROJECT_NOT_FOUND: 3,
|
|
5
|
+
INVALID_CONFIGURATION: 4,
|
|
6
|
+
INVALID_PAYLOAD: 4,
|
|
7
|
+
VALIDATION_FAILED: 5,
|
|
8
|
+
COMMENT_APPENDIX_INVALID: 5,
|
|
9
|
+
WRITE_FAILURE: 6,
|
|
10
|
+
TOOLING_MISSING: 7,
|
|
11
|
+
INTERNAL_ERROR: 1
|
|
12
|
+
};
|
|
13
|
+
export function getExitCodeForError(code) {
|
|
14
|
+
return EXIT_CODES[code] ?? 1;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addCommentToState, replyToCommentInState, setCommentStatusInState, deleteCommentInState, clearResolvedInState, syncAnchorsInState, normalizeAuthor } from "./parser.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseCommentAppendix, serializeCommentAppendix, upsertCommentAppendix, renderStateDocument, loadCommentDocumentState, serializeLoadedState, ensureNoParseErrors } from "./parser.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function getCommentThreadKey(comment) {
|
|
2
|
+
const hasExplicitExcerptAnchor = comment.excerptStartLine !== undefined
|
|
3
|
+
&& comment.excerptStartCol !== undefined
|
|
4
|
+
&& comment.excerptEndLine !== undefined
|
|
5
|
+
&& comment.excerptEndCol !== undefined
|
|
6
|
+
&& (comment.excerptStartLine < comment.excerptEndLine
|
|
7
|
+
|| (comment.excerptStartLine === comment.excerptEndLine && comment.excerptStartCol < comment.excerptEndCol));
|
|
8
|
+
if (hasExplicitExcerptAnchor) {
|
|
9
|
+
return [
|
|
10
|
+
"excerpt",
|
|
11
|
+
String(comment.paragraphIndex ?? -1),
|
|
12
|
+
String(comment.excerptStartLine),
|
|
13
|
+
String(comment.excerptStartCol),
|
|
14
|
+
String(comment.excerptEndLine),
|
|
15
|
+
String(comment.excerptEndCol)
|
|
16
|
+
].join(":");
|
|
17
|
+
}
|
|
18
|
+
return comment.paragraphIndex !== undefined
|
|
19
|
+
? `paragraph:${comment.paragraphIndex}`
|
|
20
|
+
: "file";
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
export function parseMarkdownDocument(raw) {
|
|
3
|
+
const lineEnding = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
4
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
5
|
+
if (!match) {
|
|
6
|
+
return {
|
|
7
|
+
lineEnding,
|
|
8
|
+
hasFrontmatter: false,
|
|
9
|
+
frontmatter: {},
|
|
10
|
+
body: raw
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const yamlText = match[1];
|
|
14
|
+
const loaded = yamlText.trim().length > 0
|
|
15
|
+
? yaml.load(yamlText)
|
|
16
|
+
: {};
|
|
17
|
+
if (loaded == null) {
|
|
18
|
+
return {
|
|
19
|
+
lineEnding,
|
|
20
|
+
hasFrontmatter: true,
|
|
21
|
+
frontmatter: {},
|
|
22
|
+
body: raw.slice(match[0].length)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (typeof loaded !== "object" || Array.isArray(loaded)) {
|
|
26
|
+
throw new Error("Frontmatter must be a YAML object with key/value pairs.");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
lineEnding,
|
|
30
|
+
hasFrontmatter: true,
|
|
31
|
+
frontmatter: { ...loaded },
|
|
32
|
+
body: raw.slice(match[0].length)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
export function orderFrontmatterStatusFirst(frontmatter) {
|
|
3
|
+
if (!Object.hasOwn(frontmatter, "status")) {
|
|
4
|
+
return { ...frontmatter };
|
|
5
|
+
}
|
|
6
|
+
const ordered = {
|
|
7
|
+
status: frontmatter.status
|
|
8
|
+
};
|
|
9
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
10
|
+
if (key === "status") {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
ordered[key] = value;
|
|
14
|
+
}
|
|
15
|
+
return ordered;
|
|
16
|
+
}
|
|
17
|
+
export function serializeMarkdownDocument(parsed) {
|
|
18
|
+
const includeFrontmatter = parsed.hasFrontmatter || Object.keys(parsed.frontmatter).length > 0;
|
|
19
|
+
const normalizedBody = parsed.body.replace(/^\r?\n*/, "");
|
|
20
|
+
if (!includeFrontmatter) {
|
|
21
|
+
return parsed.body;
|
|
22
|
+
}
|
|
23
|
+
const ordered = orderFrontmatterStatusFirst(parsed.frontmatter);
|
|
24
|
+
const yamlBody = yaml.dump(ordered, { lineWidth: -1, noRefs: true }).trimEnd();
|
|
25
|
+
const frontmatterBlock = yamlBody.length > 0
|
|
26
|
+
? `---${parsed.lineEnding}${yamlBody}${parsed.lineEnding}---`
|
|
27
|
+
: `---${parsed.lineEnding}---`;
|
|
28
|
+
if (!normalizedBody) {
|
|
29
|
+
return `${frontmatterBlock}${parsed.lineEnding}`;
|
|
30
|
+
}
|
|
31
|
+
return `${frontmatterBlock}${parsed.lineEnding}${parsed.lineEnding}${normalizedBody}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
export function isValidMetadataKey(value) {
|
|
3
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
4
|
+
}
|
|
5
|
+
export function parseMetadataInput(value) {
|
|
6
|
+
if (!value.trim()) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
const loaded = yaml.load(value);
|
|
10
|
+
return loaded === undefined ? value : loaded;
|
|
11
|
+
}
|
|
12
|
+
export function formatMetadataValue(value) {
|
|
13
|
+
if (typeof value === "string") {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
const dumped = yaml.dump(value, { lineWidth: -1, noRefs: true }).trim();
|
|
17
|
+
return dumped || String(value);
|
|
18
|
+
}
|
|
19
|
+
export function normalizeFrontmatterRecord(raw) {
|
|
20
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
21
|
+
throw new Error("Input payload 'frontmatter' must be a JSON object.");
|
|
22
|
+
}
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
25
|
+
const normalizedKey = key.trim();
|
|
26
|
+
if (!normalizedKey) {
|
|
27
|
+
throw new Error("Frontmatter keys cannot be empty.");
|
|
28
|
+
}
|
|
29
|
+
result[normalizedKey] = normalizeFrontmatterValue(value, normalizedKey);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function normalizeFrontmatterValue(value, key) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.map((item) => normalizeFrontmatterScalar(item, key));
|
|
36
|
+
}
|
|
37
|
+
return normalizeFrontmatterScalar(value, key);
|
|
38
|
+
}
|
|
39
|
+
function normalizeFrontmatterScalar(value, key) {
|
|
40
|
+
if (value === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Metadata key '${key}' must be a scalar or array of scalars.`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_ALLOWED_STATUSES = [
|
|
2
|
+
"draft",
|
|
3
|
+
"revise",
|
|
4
|
+
"line-edit",
|
|
5
|
+
"proof",
|
|
6
|
+
"final"
|
|
7
|
+
];
|
|
8
|
+
const STAGE_RANK = {
|
|
9
|
+
draft: 0,
|
|
10
|
+
revise: 1,
|
|
11
|
+
"line-edit": 2,
|
|
12
|
+
proof: 3,
|
|
13
|
+
final: 4
|
|
14
|
+
};
|
|
15
|
+
export function isStageName(value) {
|
|
16
|
+
return Object.hasOwn(STAGE_RANK, value);
|
|
17
|
+
}
|
|
18
|
+
export function getStageRank(stage) {
|
|
19
|
+
return STAGE_RANK[stage];
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * as CliContracts from "./contracts/cli/index.js";
|
|
2
|
+
export * as FrontmatterDomain from "./domain/frontmatter/index.js";
|
|
3
|
+
export * as CommentsDomain from "./domain/comments/index.js";
|
|
4
|
+
export * as StagesDomain from "./domain/stages/index.js";
|
|
5
|
+
export * as ProjectDomain from "./domain/project/index.js";
|
|
6
|
+
export * as SharedUtils from "./utils/index.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
let cachedVersion = null;
|
|
5
|
+
export function resolveCliVersion() {
|
|
6
|
+
if (cachedVersion) {
|
|
7
|
+
return cachedVersion;
|
|
8
|
+
}
|
|
9
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
while (true) {
|
|
11
|
+
const packagePath = path.join(current, "package.json");
|
|
12
|
+
if (fs.existsSync(packagePath)) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
15
|
+
if (parsed.name === "stego-cli" && typeof parsed.version === "string") {
|
|
16
|
+
cachedVersion = parsed.version;
|
|
17
|
+
return cachedVersion;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Continue searching upward.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const parent = path.dirname(current);
|
|
25
|
+
if (parent === current) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
cachedVersion = "0.0.0";
|
|
31
|
+
return cachedVersion;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { cac } from "cac";
|
|
2
|
+
export class CommandRegistry {
|
|
3
|
+
cli;
|
|
4
|
+
appContext;
|
|
5
|
+
multiTokenCommandMappings = [];
|
|
6
|
+
constructor(appContext) {
|
|
7
|
+
this.appContext = appContext;
|
|
8
|
+
this.cli = cac("stego");
|
|
9
|
+
}
|
|
10
|
+
showHelp() {
|
|
11
|
+
this.cli.outputHelp();
|
|
12
|
+
}
|
|
13
|
+
register(spec) {
|
|
14
|
+
const commandName = normalizeRegisteredCommandName(spec.name, this.multiTokenCommandMappings);
|
|
15
|
+
let command = this.cli.command(commandName, spec.description);
|
|
16
|
+
for (const option of spec.options ?? []) {
|
|
17
|
+
const optionConfig = option.defaultValue === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: { default: option.defaultValue };
|
|
20
|
+
command = command.option(option.flags, option.description ?? "", optionConfig);
|
|
21
|
+
}
|
|
22
|
+
if (spec.allowUnknownOptions) {
|
|
23
|
+
command = command.allowUnknownOptions();
|
|
24
|
+
}
|
|
25
|
+
command.action(async (...actionArgs) => {
|
|
26
|
+
const optionsCandidate = actionArgs[actionArgs.length - 1];
|
|
27
|
+
const options = isPlainObject(optionsCandidate)
|
|
28
|
+
? sanitizeOptions(optionsCandidate)
|
|
29
|
+
: {};
|
|
30
|
+
const positionals = actionArgs
|
|
31
|
+
.slice(0, Math.max(0, actionArgs.length - 1))
|
|
32
|
+
.map((value) => String(value));
|
|
33
|
+
await spec.action({
|
|
34
|
+
...this.appContext,
|
|
35
|
+
positionals,
|
|
36
|
+
options
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
run(argv) {
|
|
41
|
+
this.cli.help();
|
|
42
|
+
const normalizedArgv = normalizeDashInputValueArgv(normalizeIncomingArgv(argv, this.multiTokenCommandMappings));
|
|
43
|
+
this.cli.parse(["stego", "stego", ...normalizedArgv], { run: false });
|
|
44
|
+
if (!this.cli.matchedCommand) {
|
|
45
|
+
throw new Error("NO_MATCHED_COMMAND");
|
|
46
|
+
}
|
|
47
|
+
return this.cli.runMatchedCommand();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function normalizeRegisteredCommandName(rawName, mappings) {
|
|
51
|
+
const tokens = rawName.trim().split(/\s+/).filter(Boolean);
|
|
52
|
+
if (tokens.length <= 1) {
|
|
53
|
+
return rawName;
|
|
54
|
+
}
|
|
55
|
+
const commandTokens = [];
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.startsWith("<") || token.startsWith("[")) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
commandTokens.push(token);
|
|
61
|
+
}
|
|
62
|
+
if (commandTokens.length <= 1) {
|
|
63
|
+
return rawName;
|
|
64
|
+
}
|
|
65
|
+
const argumentTokens = tokens.slice(commandTokens.length);
|
|
66
|
+
const normalizedToken = commandTokens.join(":");
|
|
67
|
+
mappings.push({
|
|
68
|
+
rawTokens: commandTokens,
|
|
69
|
+
normalizedToken
|
|
70
|
+
});
|
|
71
|
+
return argumentTokens.length > 0
|
|
72
|
+
? `${normalizedToken} ${argumentTokens.join(" ")}`
|
|
73
|
+
: normalizedToken;
|
|
74
|
+
}
|
|
75
|
+
function normalizeIncomingArgv(argv, mappings) {
|
|
76
|
+
if (argv.length === 0 || mappings.length === 0) {
|
|
77
|
+
return argv;
|
|
78
|
+
}
|
|
79
|
+
const sortedMappings = [...mappings]
|
|
80
|
+
.sort((a, b) => b.rawTokens.length - a.rawTokens.length);
|
|
81
|
+
for (const mapping of sortedMappings) {
|
|
82
|
+
if (!startsWithTokens(argv, mapping.rawTokens)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return [mapping.normalizedToken, ...argv.slice(mapping.rawTokens.length)];
|
|
86
|
+
}
|
|
87
|
+
return argv;
|
|
88
|
+
}
|
|
89
|
+
function normalizeDashInputValueArgv(argv) {
|
|
90
|
+
const normalized = [];
|
|
91
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
92
|
+
const current = argv[index];
|
|
93
|
+
const next = argv[index + 1];
|
|
94
|
+
if (current === "--input" && next === "-") {
|
|
95
|
+
normalized.push("--input=-");
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
normalized.push(current);
|
|
100
|
+
}
|
|
101
|
+
return normalized;
|
|
102
|
+
}
|
|
103
|
+
function startsWithTokens(input, expected) {
|
|
104
|
+
if (input.length < expected.length) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
108
|
+
if (input[index] !== expected[index]) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
function sanitizeOptions(options) {
|
|
115
|
+
const next = { ...options };
|
|
116
|
+
delete next["--"];
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
function isPlainObject(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CommandRegistry } from "./command-registry.js";
|
|
2
|
+
import { coreModules } from "../modules/index.js";
|
|
3
|
+
import { CliError } from "../../../shared/src/contracts/cli/index.js";
|
|
4
|
+
import { writeText } from "./output-renderer.js";
|
|
5
|
+
import { resolveCliVersion } from "./cli-version.js";
|
|
6
|
+
export function createCliApp(appContext) {
|
|
7
|
+
const registry = new CommandRegistry(appContext);
|
|
8
|
+
for (const module of coreModules) {
|
|
9
|
+
module.registerCommands(registry);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
run: async () => {
|
|
13
|
+
const [firstArg] = appContext.argv;
|
|
14
|
+
if (!firstArg || isHelpRequest(firstArg)) {
|
|
15
|
+
registry.showHelp();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (isVersionRequest(firstArg)) {
|
|
19
|
+
writeText(resolveCliVersion());
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await registry.run(appContext.argv);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (isNoMatchedCommandError(error)) {
|
|
27
|
+
throw new CliError("INVALID_USAGE", `Unknown command '${firstArg}'. Run 'stego --help' for usage.`);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isNoMatchedCommandError(error) {
|
|
35
|
+
return error instanceof Error && error.message === "NO_MATCHED_COMMAND";
|
|
36
|
+
}
|
|
37
|
+
function isHelpRequest(arg) {
|
|
38
|
+
return arg === "help" || arg === "--help" || arg === "-h";
|
|
39
|
+
}
|
|
40
|
+
function isVersionRequest(arg) {
|
|
41
|
+
return arg === "version" || arg === "--version" || arg === "-v";
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function shouldRenderJsonError(argv) {
|
|
2
|
+
return argv.includes("--format") && argv.includes("json");
|
|
3
|
+
}
|
|
4
|
+
function errorEnvelope(code, message, details) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
version: 1,
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
details
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function runWithErrorBoundary(argv, run) {
|
|
14
|
+
try {
|
|
15
|
+
await run();
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (isCliLikeError(error)) {
|
|
19
|
+
if (shouldRenderJsonError(argv)) {
|
|
20
|
+
writeJsonError(errorEnvelope(error.code || "INTERNAL_ERROR", error.message, error.details));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
process.stderr.write(`ERROR: ${error.message}\n`);
|
|
24
|
+
}
|
|
25
|
+
process.exit(error.exitCode ?? 1);
|
|
26
|
+
}
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
if (shouldRenderJsonError(argv)) {
|
|
29
|
+
writeJsonError(errorEnvelope("INTERNAL_ERROR", message));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
process.stderr.write(`ERROR: ${message}\n`);
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeJsonError(payload) {
|
|
38
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
function isCliLikeError(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && "message" in value;
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { createCliApp } from "./app/create-cli-app.js";
|
|
4
|
+
import { runWithErrorBoundary } from "./app/error-boundary.js";
|
|
5
|
+
const app = createCliApp({
|
|
6
|
+
argv: process.argv.slice(2),
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
env: process.env,
|
|
9
|
+
stdout: process.stdout,
|
|
10
|
+
stderr: process.stderr
|
|
11
|
+
});
|
|
12
|
+
await runWithErrorBoundary(process.argv.slice(2), async () => {
|
|
13
|
+
await app.run();
|
|
14
|
+
});
|