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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { writeJson, writeText } from "../../../app/output-renderer.js";
|
|
2
|
+
import { resolveProjectContext } from "../../project/index.js";
|
|
3
|
+
import { resolveWorkspaceContext } from "../../workspace/index.js";
|
|
4
|
+
import { createNewManuscript, parseManuscriptOutputFormat } from "../application/create-manuscript.js";
|
|
5
|
+
export function registerNewManuscriptCommand(registry) {
|
|
6
|
+
registry.register({
|
|
7
|
+
name: "new",
|
|
8
|
+
description: "Create a new manuscript file",
|
|
9
|
+
options: [
|
|
10
|
+
{ flags: "--project <project-id>", description: "Project id" },
|
|
11
|
+
{ flags: "-i, --i <prefix>", description: "Numeric filename prefix override" },
|
|
12
|
+
{ flags: "--filename <name>", description: "Explicit manuscript filename" },
|
|
13
|
+
{ flags: "--format <format>", description: "text|json" },
|
|
14
|
+
{ flags: "--root <path>", description: "Workspace root path" }
|
|
15
|
+
],
|
|
16
|
+
action: (context) => {
|
|
17
|
+
const workspace = resolveWorkspaceContext({
|
|
18
|
+
cwd: context.cwd,
|
|
19
|
+
rootOption: readStringOption(context.options, "root")
|
|
20
|
+
});
|
|
21
|
+
const project = resolveProjectContext({
|
|
22
|
+
workspace,
|
|
23
|
+
cwd: context.cwd,
|
|
24
|
+
env: context.env,
|
|
25
|
+
explicitProjectId: readStringOption(context.options, "project")
|
|
26
|
+
});
|
|
27
|
+
const result = createNewManuscript({
|
|
28
|
+
project,
|
|
29
|
+
requestedPrefixRaw: readStringOption(context.options, "i"),
|
|
30
|
+
requestedFilenameRaw: readStringOption(context.options, "filename")
|
|
31
|
+
});
|
|
32
|
+
const outputFormat = parseManuscriptOutputFormat(readStringOption(context.options, "format"));
|
|
33
|
+
if (outputFormat === "json") {
|
|
34
|
+
writeJson({
|
|
35
|
+
ok: true,
|
|
36
|
+
operation: "new",
|
|
37
|
+
result
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
writeText(`Created manuscript: ${result.filePath}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function readStringOption(options, key) {
|
|
46
|
+
const value = options[key];
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "number") {
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerNewManuscriptCommand } from "./commands/new-manuscript.js";
|
|
2
|
+
export const manuscriptModule = {
|
|
3
|
+
registerCommands(registry) {
|
|
4
|
+
registerNewManuscriptCommand(registry);
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export * from "./application/create-manuscript.js";
|
|
9
|
+
export * from "./application/order-inference.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function ensureManuscriptDir(manuscriptDir) {
|
|
4
|
+
fs.mkdirSync(manuscriptDir, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export function fileExists(filePath) {
|
|
7
|
+
return fs.existsSync(filePath);
|
|
8
|
+
}
|
|
9
|
+
export function writeTextFile(filePath, content) {
|
|
10
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
11
|
+
}
|
|
12
|
+
export function listManuscriptOrderEntries(manuscriptDir) {
|
|
13
|
+
if (!fs.existsSync(manuscriptDir)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return fs
|
|
17
|
+
.readdirSync(manuscriptDir, { withFileTypes: true })
|
|
18
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
19
|
+
.map((entry) => {
|
|
20
|
+
const match = entry.name.match(/^(\d+)[-_]/);
|
|
21
|
+
if (!match) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
order: Number(match[1]),
|
|
26
|
+
filename: entry.name
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.filter((entry) => entry !== null)
|
|
30
|
+
.sort((a, b) => {
|
|
31
|
+
if (a.order === b.order) {
|
|
32
|
+
return a.filename.localeCompare(b.filename);
|
|
33
|
+
}
|
|
34
|
+
return a.order - b.order;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function joinPath(...parts) {
|
|
38
|
+
return path.join(...parts);
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
2
|
+
import { normalizeFrontmatterRecord, parseMarkdownDocument, serializeMarkdownDocument } from "../domain/metadata.js";
|
|
3
|
+
import { readJsonPayload, readMarkdownFile, resolveMarkdownPath, writeMarkdownFile } from "../infra/metadata-repo.js";
|
|
4
|
+
export function applyMetadata(input) {
|
|
5
|
+
const absolutePath = resolveMarkdownPath(input.cwd, input.markdownPath);
|
|
6
|
+
const raw = readMarkdownFile(absolutePath, input.markdownPath);
|
|
7
|
+
const payload = readJsonPayload(input.inputPath, input.cwd);
|
|
8
|
+
let frontmatter;
|
|
9
|
+
try {
|
|
10
|
+
frontmatter = normalizeFrontmatterRecord(payload.frontmatter);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
throw new CliError("INVALID_PAYLOAD", message);
|
|
15
|
+
}
|
|
16
|
+
const body = typeof payload.body === "string" ? payload.body : undefined;
|
|
17
|
+
const hasFrontmatter = typeof payload.hasFrontmatter === "boolean"
|
|
18
|
+
? payload.hasFrontmatter
|
|
19
|
+
: undefined;
|
|
20
|
+
const existing = parseMarkdownDocument(raw);
|
|
21
|
+
const next = {
|
|
22
|
+
lineEnding: existing.lineEnding,
|
|
23
|
+
hasFrontmatter: hasFrontmatter ?? (existing.hasFrontmatter || Object.keys(frontmatter).length > 0),
|
|
24
|
+
frontmatter,
|
|
25
|
+
body: body ?? existing.body
|
|
26
|
+
};
|
|
27
|
+
const nextText = serializeMarkdownDocument(next);
|
|
28
|
+
const changed = nextText !== raw;
|
|
29
|
+
if (changed) {
|
|
30
|
+
writeMarkdownFile(absolutePath, nextText);
|
|
31
|
+
}
|
|
32
|
+
const reparsed = parseMarkdownDocument(nextText);
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
operation: "apply",
|
|
36
|
+
changed,
|
|
37
|
+
state: {
|
|
38
|
+
path: absolutePath,
|
|
39
|
+
hasFrontmatter: reparsed.hasFrontmatter,
|
|
40
|
+
lineEnding: reparsed.lineEnding,
|
|
41
|
+
frontmatter: reparsed.frontmatter,
|
|
42
|
+
body: reparsed.body
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseMarkdownDocument } from "../domain/metadata.js";
|
|
2
|
+
import { readMarkdownFile, resolveMarkdownPath } from "../infra/metadata-repo.js";
|
|
3
|
+
export function readMetadata(input) {
|
|
4
|
+
const absolutePath = resolveMarkdownPath(input.cwd, input.markdownPath);
|
|
5
|
+
const raw = readMarkdownFile(absolutePath, input.markdownPath);
|
|
6
|
+
const parsed = parseMarkdownDocument(raw);
|
|
7
|
+
return {
|
|
8
|
+
ok: true,
|
|
9
|
+
operation: "read",
|
|
10
|
+
state: {
|
|
11
|
+
path: absolutePath,
|
|
12
|
+
hasFrontmatter: parsed.hasFrontmatter,
|
|
13
|
+
lineEnding: parsed.lineEnding,
|
|
14
|
+
frontmatter: parsed.frontmatter,
|
|
15
|
+
body: parsed.body
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
2
|
+
import { writeJson, writeText } from "../../../app/output-renderer.js";
|
|
3
|
+
import { applyMetadata } from "../application/apply-metadata.js";
|
|
4
|
+
import { parseMetadataOutputFormat } from "../infra/metadata-repo.js";
|
|
5
|
+
export function registerMetadataApplyCommand(registry) {
|
|
6
|
+
registry.register({
|
|
7
|
+
name: "metadata apply <markdown-path>",
|
|
8
|
+
description: "Apply frontmatter metadata",
|
|
9
|
+
options: [
|
|
10
|
+
{ flags: "--input <path>", description: "JSON payload path or '-'" },
|
|
11
|
+
{ flags: "--format <format>", description: "text|json" }
|
|
12
|
+
],
|
|
13
|
+
action: (context) => {
|
|
14
|
+
const markdownPath = context.positionals[0];
|
|
15
|
+
if (!markdownPath) {
|
|
16
|
+
throw new CliError("INVALID_USAGE", "Markdown path is required. Use: stego metadata apply <path> --input <path|->.");
|
|
17
|
+
}
|
|
18
|
+
const inputPath = readOption(context.options, "input");
|
|
19
|
+
if (typeof inputPath !== "string" || inputPath.trim().length === 0) {
|
|
20
|
+
throw new CliError("INVALID_USAGE", "--input <path|-> is required for 'metadata apply'.");
|
|
21
|
+
}
|
|
22
|
+
const outputFormat = parseMetadataOutputFormat(readOption(context.options, "format"));
|
|
23
|
+
const result = applyMetadata({
|
|
24
|
+
cwd: context.cwd,
|
|
25
|
+
markdownPath,
|
|
26
|
+
inputPath
|
|
27
|
+
});
|
|
28
|
+
if (outputFormat === "json") {
|
|
29
|
+
writeJson(result);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
writeText(`${result.changed ? "Updated" : "No changes for"} metadata in ${result.state.path}.`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function readOption(options, key) {
|
|
37
|
+
return options[key];
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
2
|
+
import { writeJson, writeText } from "../../../app/output-renderer.js";
|
|
3
|
+
import { readMetadata } from "../application/read-metadata.js";
|
|
4
|
+
import { parseMetadataOutputFormat } from "../infra/metadata-repo.js";
|
|
5
|
+
export function registerMetadataReadCommand(registry) {
|
|
6
|
+
registry.register({
|
|
7
|
+
name: "metadata read <markdown-path>",
|
|
8
|
+
description: "Read frontmatter metadata",
|
|
9
|
+
options: [
|
|
10
|
+
{ flags: "--format <format>", description: "text|json" }
|
|
11
|
+
],
|
|
12
|
+
action: (context) => {
|
|
13
|
+
const markdownPath = context.positionals[0];
|
|
14
|
+
if (!markdownPath) {
|
|
15
|
+
throw new CliError("INVALID_USAGE", "Markdown path is required. Use: stego metadata read <path>.");
|
|
16
|
+
}
|
|
17
|
+
const outputFormat = parseMetadataOutputFormat(readOption(context.options, "format"));
|
|
18
|
+
const result = readMetadata({
|
|
19
|
+
cwd: context.cwd,
|
|
20
|
+
markdownPath
|
|
21
|
+
});
|
|
22
|
+
if (outputFormat === "json") {
|
|
23
|
+
writeJson(result);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const keyCount = Object.keys(result.state.frontmatter).length;
|
|
27
|
+
writeText(`Read metadata for ${result.state.path} (${keyCount} keys).`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function readOption(options, key) {
|
|
32
|
+
return options[key];
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseMarkdownDocument, serializeMarkdownDocument, normalizeFrontmatterRecord } from "../../../../../shared/src/domain/frontmatter/index.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerMetadataReadCommand } from "./commands/metadata-read.js";
|
|
2
|
+
import { registerMetadataApplyCommand } from "./commands/metadata-apply.js";
|
|
3
|
+
export const metadataModule = {
|
|
4
|
+
registerCommands(registry) {
|
|
5
|
+
registerMetadataReadCommand(registry);
|
|
6
|
+
registerMetadataApplyCommand(registry);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
export * from "./application/read-metadata.js";
|
|
11
|
+
export * from "./application/apply-metadata.js";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
4
|
+
export function parseMetadataOutputFormat(rawValue) {
|
|
5
|
+
if (rawValue === undefined || rawValue === null || rawValue === "text") {
|
|
6
|
+
return "text";
|
|
7
|
+
}
|
|
8
|
+
if (rawValue === "json") {
|
|
9
|
+
return "json";
|
|
10
|
+
}
|
|
11
|
+
throw new CliError("INVALID_USAGE", "Invalid --format value. Use 'text' or 'json'.");
|
|
12
|
+
}
|
|
13
|
+
export function resolveMarkdownPath(cwd, markdownPath) {
|
|
14
|
+
return path.resolve(cwd, markdownPath);
|
|
15
|
+
}
|
|
16
|
+
export function readMarkdownFile(absolutePath, originalArg) {
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.statSync(absolutePath);
|
|
19
|
+
if (!stat.isFile()) {
|
|
20
|
+
throw new Error("Not a file");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new CliError("INVALID_USAGE", `Markdown file not found: ${originalArg}`);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(absolutePath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
throw new CliError("INVALID_USAGE", `Unable to read markdown file: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function writeMarkdownFile(absolutePath, contents) {
|
|
35
|
+
try {
|
|
36
|
+
fs.writeFileSync(absolutePath, contents, "utf8");
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
throw new CliError("WRITE_FAILURE", `Failed to update markdown file: ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function readJsonPayload(inputPath, cwd) {
|
|
44
|
+
let rawPayload = "";
|
|
45
|
+
try {
|
|
46
|
+
rawPayload = inputPath === "-"
|
|
47
|
+
? fs.readFileSync(0, "utf8")
|
|
48
|
+
: fs.readFileSync(path.resolve(cwd, inputPath), "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
throw new CliError("INVALID_PAYLOAD", `Unable to read input payload: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(rawPayload);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new CliError("INVALID_PAYLOAD", "Input payload is not valid JSON.");
|
|
60
|
+
}
|
|
61
|
+
if (!isPlainObject(parsed)) {
|
|
62
|
+
throw new CliError("INVALID_PAYLOAD", "Input payload must be a JSON object.");
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
function isPlainObject(value) {
|
|
67
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
4
|
+
import { isValidProjectId } from "../../../../../shared/src/domain/project/index.js";
|
|
5
|
+
import { ensureDirectory, pathExists, writeTextFile } from "../infra/project-repo.js";
|
|
6
|
+
const PROJECT_EXTENSION_RECOMMENDATIONS = [
|
|
7
|
+
"matt-gold.stego-extension",
|
|
8
|
+
"matt-gold.saurus-extension",
|
|
9
|
+
"streetsidesoftware.code-spell-checker"
|
|
10
|
+
];
|
|
11
|
+
const PROSE_MARKDOWN_EDITOR_SETTINGS = {
|
|
12
|
+
"[markdown]": {
|
|
13
|
+
"editor.fontFamily": "Inter, Helvetica Neue, Helvetica, Arial, sans-serif",
|
|
14
|
+
"editor.fontSize": 17,
|
|
15
|
+
"editor.lineHeight": 28,
|
|
16
|
+
"editor.wordWrap": "wordWrapColumn",
|
|
17
|
+
"editor.wordWrapColumn": 72,
|
|
18
|
+
"editor.lineNumbers": "off"
|
|
19
|
+
},
|
|
20
|
+
"markdown.preview.fontFamily": "Inter, Helvetica Neue, Helvetica, Arial, sans-serif"
|
|
21
|
+
};
|
|
22
|
+
export function createProject(input) {
|
|
23
|
+
const projectId = (input.projectId || "").trim();
|
|
24
|
+
if (!projectId) {
|
|
25
|
+
throw new CliError("INVALID_USAGE", "Project id is required. Use --project <project-id>.");
|
|
26
|
+
}
|
|
27
|
+
if (!isValidProjectId(projectId)) {
|
|
28
|
+
throw new CliError("INVALID_USAGE", "Project id must match /^[a-z0-9][a-z0-9-]*$/.");
|
|
29
|
+
}
|
|
30
|
+
const projectRoot = path.join(input.workspace.repoRoot, input.workspace.config.projectsDir, projectId);
|
|
31
|
+
if (pathExists(projectRoot)) {
|
|
32
|
+
throw new CliError("INVALID_USAGE", `Project already exists: ${projectRoot}`);
|
|
33
|
+
}
|
|
34
|
+
const manuscriptDir = path.join(projectRoot, input.workspace.config.chapterDir);
|
|
35
|
+
const spineDir = path.join(projectRoot, input.workspace.config.spineDir);
|
|
36
|
+
const notesDir = path.join(projectRoot, input.workspace.config.notesDir);
|
|
37
|
+
const distDir = path.join(projectRoot, input.workspace.config.distDir);
|
|
38
|
+
ensureDirectory(manuscriptDir);
|
|
39
|
+
ensureDirectory(spineDir);
|
|
40
|
+
ensureDirectory(notesDir);
|
|
41
|
+
ensureDirectory(distDir);
|
|
42
|
+
const projectJsonPath = path.join(projectRoot, "stego-project.json");
|
|
43
|
+
writeTextFile(projectJsonPath, `${JSON.stringify({
|
|
44
|
+
id: projectId,
|
|
45
|
+
title: input.title?.trim() || toDisplayTitle(projectId),
|
|
46
|
+
requiredMetadata: ["status"],
|
|
47
|
+
compileStructure: {
|
|
48
|
+
levels: [
|
|
49
|
+
{
|
|
50
|
+
key: "chapter",
|
|
51
|
+
label: "Chapter",
|
|
52
|
+
titleKey: "chapter_title",
|
|
53
|
+
injectHeading: true,
|
|
54
|
+
headingTemplate: "{label} {value}: {title}",
|
|
55
|
+
pageBreak: "between-groups"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}, null, 2)}\n`);
|
|
60
|
+
const projectPackagePath = path.join(projectRoot, "package.json");
|
|
61
|
+
writeTextFile(projectPackagePath, `${JSON.stringify({
|
|
62
|
+
name: `stego-project-${projectId}`,
|
|
63
|
+
private: true,
|
|
64
|
+
scripts: {
|
|
65
|
+
new: "npx --no-install stego new",
|
|
66
|
+
"spine:new": "npx --no-install stego spine new",
|
|
67
|
+
"spine:new-category": "npx --no-install stego spine new-category",
|
|
68
|
+
lint: "npx --no-install stego lint",
|
|
69
|
+
validate: "npx --no-install stego validate",
|
|
70
|
+
build: "npx --no-install stego build",
|
|
71
|
+
"check-stage": "npx --no-install stego check-stage",
|
|
72
|
+
export: "npx --no-install stego export"
|
|
73
|
+
}
|
|
74
|
+
}, null, 2)}\n`);
|
|
75
|
+
const starterManuscriptPath = path.join(manuscriptDir, "100-hello-world.md");
|
|
76
|
+
writeTextFile(starterManuscriptPath, `---
|
|
77
|
+
status: draft
|
|
78
|
+
chapter: 1
|
|
79
|
+
chapter_title: Hello World
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# Hello World
|
|
83
|
+
|
|
84
|
+
Start writing here.
|
|
85
|
+
`);
|
|
86
|
+
const charactersDir = path.join(spineDir, "characters");
|
|
87
|
+
ensureDirectory(charactersDir);
|
|
88
|
+
const charactersCategoryPath = path.join(charactersDir, "_category.md");
|
|
89
|
+
writeTextFile(charactersCategoryPath, `---
|
|
90
|
+
label: Characters
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# Characters
|
|
94
|
+
|
|
95
|
+
`);
|
|
96
|
+
const charactersEntryPath = path.join(charactersDir, "example-character.md");
|
|
97
|
+
writeTextFile(charactersEntryPath, "# Example Character\n\n");
|
|
98
|
+
const projectExtensionsPath = ensureProjectExtensionsRecommendations(projectRoot);
|
|
99
|
+
let projectSettingsPath;
|
|
100
|
+
if (input.enableProseFont) {
|
|
101
|
+
projectSettingsPath = writeProseEditorSettingsForProject(projectRoot);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
projectId,
|
|
105
|
+
projectPath: path.relative(input.workspace.repoRoot, projectRoot),
|
|
106
|
+
files: [
|
|
107
|
+
path.relative(input.workspace.repoRoot, projectJsonPath),
|
|
108
|
+
path.relative(input.workspace.repoRoot, projectPackagePath),
|
|
109
|
+
path.relative(input.workspace.repoRoot, starterManuscriptPath),
|
|
110
|
+
path.relative(input.workspace.repoRoot, charactersCategoryPath),
|
|
111
|
+
path.relative(input.workspace.repoRoot, charactersEntryPath),
|
|
112
|
+
path.relative(input.workspace.repoRoot, projectExtensionsPath),
|
|
113
|
+
...(projectSettingsPath ? [path.relative(input.workspace.repoRoot, projectSettingsPath)] : [])
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function parseProjectOutputFormat(raw) {
|
|
118
|
+
if (!raw || raw === "text") {
|
|
119
|
+
return "text";
|
|
120
|
+
}
|
|
121
|
+
if (raw === "json") {
|
|
122
|
+
return "json";
|
|
123
|
+
}
|
|
124
|
+
throw new CliError("INVALID_USAGE", "Invalid --format value. Use 'text' or 'json'.");
|
|
125
|
+
}
|
|
126
|
+
export function parseProseFontMode(raw) {
|
|
127
|
+
const normalized = (raw || "prompt").trim().toLowerCase();
|
|
128
|
+
if (normalized === "yes" || normalized === "true" || normalized === "y") {
|
|
129
|
+
return "yes";
|
|
130
|
+
}
|
|
131
|
+
if (normalized === "no" || normalized === "false" || normalized === "n") {
|
|
132
|
+
return "no";
|
|
133
|
+
}
|
|
134
|
+
if (normalized === "prompt" || normalized === "ask") {
|
|
135
|
+
return "prompt";
|
|
136
|
+
}
|
|
137
|
+
throw new CliError("INVALID_USAGE", "Invalid --prose-font value. Use 'yes', 'no', or 'prompt'.");
|
|
138
|
+
}
|
|
139
|
+
function ensureProjectExtensionsRecommendations(projectRoot) {
|
|
140
|
+
const vscodeDir = path.join(projectRoot, ".vscode");
|
|
141
|
+
const extensionsPath = path.join(vscodeDir, "extensions.json");
|
|
142
|
+
ensureDirectory(vscodeDir);
|
|
143
|
+
let existingRecommendations = [];
|
|
144
|
+
if (pathExists(extensionsPath)) {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(fs.readFileSync(extensionsPath, "utf8"));
|
|
147
|
+
if (Array.isArray(parsed.recommendations)) {
|
|
148
|
+
existingRecommendations = parsed.recommendations.filter((value) => typeof value === "string");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
existingRecommendations = [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const mergedRecommendations = [
|
|
156
|
+
...new Set([...PROJECT_EXTENSION_RECOMMENDATIONS, ...existingRecommendations])
|
|
157
|
+
];
|
|
158
|
+
writeTextFile(extensionsPath, `${JSON.stringify({ recommendations: mergedRecommendations }, null, 2)}\n`);
|
|
159
|
+
return extensionsPath;
|
|
160
|
+
}
|
|
161
|
+
function writeProseEditorSettingsForProject(projectRoot) {
|
|
162
|
+
const vscodeDir = path.join(projectRoot, ".vscode");
|
|
163
|
+
const settingsPath = path.join(vscodeDir, "settings.json");
|
|
164
|
+
ensureDirectory(vscodeDir);
|
|
165
|
+
let existingSettings = {};
|
|
166
|
+
if (pathExists(settingsPath)) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
169
|
+
if (isPlainObject(parsed)) {
|
|
170
|
+
existingSettings = parsed;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
existingSettings = {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const proseMarkdownSettings = isPlainObject(PROSE_MARKDOWN_EDITOR_SETTINGS["[markdown]"])
|
|
178
|
+
? PROSE_MARKDOWN_EDITOR_SETTINGS["[markdown]"]
|
|
179
|
+
: {};
|
|
180
|
+
const existingMarkdownSettings = isPlainObject(existingSettings["[markdown]"])
|
|
181
|
+
? existingSettings["[markdown]"]
|
|
182
|
+
: {};
|
|
183
|
+
const nextSettings = {
|
|
184
|
+
...existingSettings,
|
|
185
|
+
"[markdown]": {
|
|
186
|
+
...existingMarkdownSettings,
|
|
187
|
+
...proseMarkdownSettings
|
|
188
|
+
},
|
|
189
|
+
"markdown.preview.fontFamily": PROSE_MARKDOWN_EDITOR_SETTINGS["markdown.preview.fontFamily"]
|
|
190
|
+
};
|
|
191
|
+
writeTextFile(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
|
|
192
|
+
return settingsPath;
|
|
193
|
+
}
|
|
194
|
+
function toDisplayTitle(value) {
|
|
195
|
+
return value
|
|
196
|
+
.split(/[-_]+/)
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
199
|
+
.join(" ");
|
|
200
|
+
}
|
|
201
|
+
function isPlainObject(value) {
|
|
202
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
203
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
4
|
+
import { listProjectIds, readJsonFile } from "../infra/project-repo.js";
|
|
5
|
+
export function resolveProjectContext(input) {
|
|
6
|
+
const projectIds = listProjectIds(input.workspace);
|
|
7
|
+
const projectId = resolveProjectIdCandidate(input, projectIds);
|
|
8
|
+
if (!projectId) {
|
|
9
|
+
throw new CliError("PROJECT_NOT_FOUND", "Project id is required. Use --project <project-id>.");
|
|
10
|
+
}
|
|
11
|
+
const projectRoot = path.join(input.workspace.repoRoot, input.workspace.config.projectsDir, projectId);
|
|
12
|
+
const projectJsonPath = path.join(projectRoot, "stego-project.json");
|
|
13
|
+
if (!pathExists(projectRoot) || !pathExists(projectJsonPath)) {
|
|
14
|
+
throw new CliError("PROJECT_NOT_FOUND", `Project not found: ${projectRoot}`);
|
|
15
|
+
}
|
|
16
|
+
let meta;
|
|
17
|
+
try {
|
|
18
|
+
meta = readJsonFile(projectJsonPath);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
throw new CliError("INVALID_CONFIGURATION", `Invalid JSON at ${projectJsonPath}: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
id: projectId,
|
|
26
|
+
root: projectRoot,
|
|
27
|
+
manuscriptDir: path.join(projectRoot, input.workspace.config.chapterDir),
|
|
28
|
+
spineDir: path.join(projectRoot, input.workspace.config.spineDir),
|
|
29
|
+
notesDir: path.join(projectRoot, input.workspace.config.notesDir),
|
|
30
|
+
distDir: path.join(projectRoot, input.workspace.config.distDir),
|
|
31
|
+
meta,
|
|
32
|
+
workspace: input.workspace
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function resolveProjectIdCandidate(input, ids) {
|
|
36
|
+
if (input.explicitProjectId) {
|
|
37
|
+
return input.explicitProjectId;
|
|
38
|
+
}
|
|
39
|
+
const fromEnv = readEnvProjectId(input.env);
|
|
40
|
+
if (fromEnv) {
|
|
41
|
+
return fromEnv;
|
|
42
|
+
}
|
|
43
|
+
const inferredFromCwd = inferProjectIdFromCwd(input.cwd, input.workspace);
|
|
44
|
+
if (inferredFromCwd) {
|
|
45
|
+
return inferredFromCwd;
|
|
46
|
+
}
|
|
47
|
+
return ids.length === 1 ? ids[0] : null;
|
|
48
|
+
}
|
|
49
|
+
function readEnvProjectId(env) {
|
|
50
|
+
const value = env.STEGO_PROJECT || env.WRITING_PROJECT;
|
|
51
|
+
if (!value) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const normalized = value.trim();
|
|
55
|
+
return normalized.length > 0 ? normalized : null;
|
|
56
|
+
}
|
|
57
|
+
function inferProjectIdFromCwd(cwd, workspace) {
|
|
58
|
+
const projectsRoot = path.resolve(workspace.repoRoot, workspace.config.projectsDir);
|
|
59
|
+
const relative = path.relative(projectsRoot, path.resolve(cwd));
|
|
60
|
+
if (!relative || relative === "." || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const projectId = relative.split(path.sep)[0];
|
|
64
|
+
if (!projectId) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const projectJsonPath = path.join(projectsRoot, projectId, "stego-project.json");
|
|
68
|
+
return pathExists(projectJsonPath) ? projectId : null;
|
|
69
|
+
}
|
|
70
|
+
function pathExists(filePath) {
|
|
71
|
+
return Boolean(filePath) && fs.existsSync(filePath);
|
|
72
|
+
}
|